├── .eslintrc.js ├── .github ├── dependabot.yml ├── labels.yml ├── release-drafter.yml └── workflows │ ├── auto-merge-dependabot-prs.yml │ ├── labeler.yml │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .prettierrc.js ├── .tool-versions ├── LICENSE ├── README.md ├── RELEASE_PROCESS.md ├── app ├── app.js ├── css │ └── app.css ├── fileOperations.js ├── index.html └── spiffworkflow │ ├── DataObject │ ├── DataObjectHelpers.js │ ├── DataObjectInterceptor.js │ ├── DataObjectLabelEditingProvider.js │ ├── DataObjectRenderer.js │ ├── DataObjectRules.js │ ├── index.js │ └── propertiesPanel │ │ ├── DataObjectArray.js │ │ ├── DataObjectPropertiesProvider.js │ │ ├── DataObjectSelect.jsx │ │ └── DataReferenceGroup.js │ ├── DataStoreReference │ ├── DataStoreHelpers.js │ ├── DataStoreInterceptor.js │ ├── index.js │ └── propertiesPanel │ │ ├── DataStorePropertiesProvider.js │ │ └── DataStoreSelect.js │ ├── InputOutput │ ├── IoInterceptor.js │ ├── IoPalette.js │ ├── IoRules.js │ ├── helpers.js │ ├── index.js │ └── propertiesProvider │ │ ├── InputParametersArray.js │ │ ├── IoGroup.js │ │ ├── IoPropertiesProvider.js │ │ └── OutputParametersArray.js │ ├── callActivity │ ├── CallActivityInterceptor.js │ ├── index.js │ └── propertiesPanel │ │ └── CallActivityPropertiesProvider.js │ ├── conditions │ ├── index.js │ └── propertiesPanel │ │ └── ConditionsPropertiesProvider.js │ ├── constants.js │ ├── errors │ ├── index.js │ └── propertiesPanel │ │ └── ErrorPropertiesProvider.js │ ├── escalations │ ├── index.js │ └── propertiesPanel │ │ └── EscalationPropertiesProvider.js │ ├── eventList.js │ ├── eventSelect.js │ ├── extensions │ ├── contextPad │ │ └── CustomContextPadProvider.js │ ├── extensionHelpers.js │ ├── index.js │ └── propertiesPanel │ │ ├── ExtensionsPropertiesProvider.jsx │ │ ├── ScriptUnitTestArray.js │ │ ├── SpiffExtensionCheckboxEntry.jsx │ │ ├── SpiffExtensionLaunchButton.js │ │ ├── SpiffExtensionSelect.js │ │ ├── SpiffExtensionServiceProperties.js │ │ ├── SpiffExtensionTextArea.jsx │ │ ├── SpiffExtensionTextInput.jsx │ │ └── SpiffScriptGroup.js │ ├── helpers.js │ ├── index.js │ ├── loops │ ├── MultiInstancePropertiesProvider.js │ ├── StandardLoopPropertiesProvider.js │ ├── helpers.js │ └── propertiesPanel │ │ ├── CompletionConditionEntry.js │ │ ├── InputCollectionEntry.js │ │ ├── InputItemEntry.js │ │ ├── IsIOSyncEntry.js │ │ ├── LoopCardinalityEntry.js │ │ ├── OutputCollectionEntry.js │ │ └── OutputItemEntry.js │ ├── messages │ ├── MessageHelpers.js │ ├── MessageInterceptor.js │ ├── index.js │ └── propertiesPanel │ │ ├── MessageCorrelationPropertiesArray.js │ │ ├── MessagesPropertiesProvider.js │ │ ├── elementLevelProvider │ │ ├── CorrelationCheckbox.jsx │ │ ├── CorrelationPropertiesList.js │ │ ├── MatchingConditionArray.js │ │ ├── MatchingCorrelationCheckbox.jsx │ │ ├── MessageJsonSchemaSelect.js │ │ ├── MessageLaunchEditorButton.js │ │ ├── MessagePayload.jsx │ │ ├── MessageSelect.jsx │ │ ├── MessageVariable.jsx │ │ └── TaskEventMessageProvider.js │ │ └── processLevelProvider │ │ ├── CollaborationPropertiesProvider.js │ │ ├── CorrelationKeysArray.js │ │ ├── CorrelationPropertiesArray.js │ │ ├── MessageArray.js │ │ └── MessagePropertiesMultiSelect.js │ ├── moddle │ └── spiffworkflow.json │ └── signals │ ├── index.js │ └── propertiesPanel │ └── SignalPropertiesProvider.js ├── docs └── io.png ├── karma.conf.js ├── package-lock.json ├── package.json ├── resources └── diagram.bpmn ├── test ├── .eslintrc └── spec │ ├── BpmnInputOutputSpec.js │ ├── BusinessRulePropsSpec.js │ ├── CallActivitySpec.js │ ├── ConditionSpec.js │ ├── DataObjectInPoolsSpec.js │ ├── DataObjectInterceptorSpec.js │ ├── DataObjectPropsSpec.js │ ├── DataObjectRulesSpec.js │ ├── DataStoreInterceptorSpec.js │ ├── DataStoreReferenceSpec.js │ ├── IOInterceptorSpec.js │ ├── IoVariablesSpec.js │ ├── MessagesCorrSpec.js │ ├── MessagesSpec.js │ ├── ProcessPropsSpec.js │ ├── ScriptTaskUnitTestSpec.js │ ├── ScriptsPropsSpec.js │ ├── ServiceTaskPropsSpec.js │ ├── UserTaskPropsSpec.js │ ├── bpmn │ ├── basic_message.bpmn │ ├── call_activity.bpmn │ ├── collaboration.bpmn │ ├── conditional_event.bpmn │ ├── data_objects_in_pools.bpmn │ ├── data_store.bpmn │ ├── diagram.bpmn │ ├── empty_diagram.bpmn │ ├── gateway.bpmn │ ├── io_variables.bpmn │ ├── request_new_role.bpmn │ ├── script_task.bpmn │ ├── service.bpmn │ ├── simple_collab.bpmn │ ├── subprocess.bpmn │ ├── two_messages.bpmn │ └── user_form.bpmn │ ├── helpers.js │ └── test.css └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'airbnb', 8 | 'plugin:bpmn-io/es6', 9 | 'plugin:prettier/recommended', 10 | 'plugin:sonarjs/recommended', 11 | 'plugin:import/errors', 12 | 'plugin:import/warnings', 13 | ], 14 | parserOptions: { 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | }, 21 | rules: { 22 | 'jsx-a11y/no-autofocus': 'off', 23 | 'jsx-a11y/label-has-associated-control': 'off', 24 | 'no-console': 'off', 25 | 'no-unused-vars': [ 26 | 'error', 27 | { 28 | destructuredArrayIgnorePattern: '^_', 29 | varsIgnorePattern: '_', 30 | argsIgnorePattern: '^_', 31 | }, 32 | ], 33 | 'import/extensions': [ 34 | 'error', 35 | 'ignorePackages', 36 | { 37 | js: 'never', 38 | jsx: 'never', 39 | ts: 'never', 40 | tsx: 'never', 41 | }, 42 | ], 43 | 44 | // We could try turning these on at some point but do not want to force it now 45 | 'react/react-in-jsx-scope': 'off', 46 | 'react/prop-types': 'off', 47 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }], 48 | 'no-use-before-define': 0, 49 | 'func-names': 'off', 50 | 'react/destructuring-assignment': 'off', 51 | 'import/prefer-default-export': 'off', 52 | 'no-restricted-syntax': 'off', 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: pip 8 | directory: "/.github/workflows" 9 | schedule: 10 | interval: daily 11 | - package-ecosystem: pip 12 | directory: "/docs" 13 | schedule: 14 | interval: daily 15 | - package-ecosystem: pip 16 | directory: "/" 17 | schedule: 18 | interval: daily 19 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Labels names are important as they are used by Release Drafter to decide 3 | # regarding where to record them in changelog or if to skip them. 4 | # 5 | # The repository labels will be automatically configured using this file and 6 | # the GitHub Action https://github.com/marketplace/actions/github-labeler. 7 | - name: breaking 8 | description: Breaking Changes 9 | color: bfd4f2 10 | - name: bug 11 | description: Something isn't working 12 | color: d73a4a 13 | - name: build 14 | description: Build System and Dependencies 15 | color: bfdadc 16 | - name: ci 17 | description: Continuous Integration 18 | color: 4a97d6 19 | - name: dependencies 20 | description: Pull requests that update a dependency file 21 | color: 0366d6 22 | - name: documentation 23 | description: Improvements or additions to documentation 24 | color: 0075ca 25 | - name: duplicate 26 | description: This issue or pull request already exists 27 | color: cfd3d7 28 | - name: enhancement 29 | description: New feature or request 30 | color: a2eeef 31 | - name: github_actions 32 | description: Pull requests that update Github_actions code 33 | color: "000000" 34 | - name: good first issue 35 | description: Good for newcomers 36 | color: 7057ff 37 | - name: help wanted 38 | description: Extra attention is needed 39 | color: 008672 40 | - name: invalid 41 | description: This doesn't seem right 42 | color: e4e669 43 | - name: performance 44 | description: Performance 45 | color: "016175" 46 | - name: python 47 | description: Pull requests that update Python code 48 | color: 2b67c6 49 | - name: question 50 | description: Further information is requested 51 | color: d876e3 52 | - name: refactoring 53 | description: Refactoring 54 | color: ef67c4 55 | - name: removal 56 | description: Removals and Deprecations 57 | color: 9ae7ea 58 | - name: style 59 | description: Style 60 | color: c120e5 61 | - name: testing 62 | description: Testing 63 | color: b1fc6f 64 | - name: wontfix 65 | description: This will not be worked on 66 | color: ffffff 67 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: ":boom: Breaking Changes" 3 | label: "breaking" 4 | - title: ":rocket: Features" 5 | label: "enhancement" 6 | - title: ":fire: Removals and Deprecations" 7 | label: "removal" 8 | - title: ":beetle: Fixes" 9 | label: "bug" 10 | - title: ":racehorse: Performance" 11 | label: "performance" 12 | - title: ":rotating_light: Testing" 13 | label: "testing" 14 | - title: ":construction_worker: Continuous Integration" 15 | label: "ci" 16 | - title: ":books: Documentation" 17 | label: "documentation" 18 | - title: ":hammer: Refactoring" 19 | label: "refactoring" 20 | - title: ":lipstick: Style" 21 | label: "style" 22 | - title: ":package: Dependencies" 23 | labels: 24 | - "dependencies" 25 | - "build" 26 | template: | 27 | ## Changes 28 | 29 | $CHANGES 30 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot-prs.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | workflow_run: 4 | workflows: ["Tests"] 5 | types: 6 | - completed 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | dependabot: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.actor == 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' && github.event_name == 'pull_request' }} 15 | steps: 16 | - name: Dependabot metadata 17 | id: metadata 18 | uses: dependabot/fetch-metadata@v2.2.0 19 | with: 20 | github-token: "${{ secrets.GITHUB_TOKEN }}" 21 | - name: Enable auto-merge for Dependabot PRs 22 | # if: ${{contains(steps.metadata.outputs.dependency-names, 'pytest') && steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 23 | # if: ${{contains(steps.metadata.outputs.dependency-names, 'pytest')}} 24 | # ideally we auto-merge if all checks pass 25 | run: gh pr merge --auto --merge "$PR_URL" 26 | env: 27 | PR_URL: ${{github.event.pull_request.html_url}} 28 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | labeler: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out the repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Run Labeler 17 | uses: crazy-max/ghaction-github-labeler@v5.1.0 18 | with: 19 | skip-delete: true 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM 2 | 3 | # On a published release, run tests and deploy to NPM 4 | on: 5 | workflow_dispatch: 6 | release: 7 | types: [published] 8 | 9 | # Job Setup 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - uses: actions/checkout@v4 #Checkout Repo 16 | - uses: actions/setup-node@v4 #Setup Node 17 | with: 18 | node-version: 18 19 | - run: npm install 20 | - run: npm test 21 | - uses: JS-DevTools/npm-publish@v3 22 | with: 23 | token: ${{ secrets.NPM_TOKEN }} 24 | access: public 25 | - if: steps.publish.outputs.type != 'none' 26 | run: | 27 | echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}" 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | # Run on Pull Requests and pushes to main 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | 11 | # This allows a subsequently queued workflow run to interrupt previous runs 12 | concurrency: 13 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 14 | cancel-in-progress: true 15 | 16 | # Job Setup 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 #Checkout Repo 22 | - uses: actions/setup-node@v4 #Setup Node 23 | - uses: nanasess/setup-chromedriver@v2 #Setup ChromeDriver 24 | - name: Run Karma Tests 25 | run: | 26 | npm ci 27 | npm run test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ 3 | .idea/ 4 | .aider.* 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 22.3.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sartography 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | ## Releases 2 | 3 | Be sure to edit the package.json, and update the version. Releases won't create 4 | a new NPM package unless the version was updated. 5 | A good way to do go about this is with npm version. Which will increment the version in package.json and create a new commit and tag. Here are few examples that you might use, but 6 | there is more information on [NPM Version](https://docs.npmjs.com/cli/v8/commands/npm-version). 7 | 8 | For doing a patch release, you can do 9 | ```bash 10 | npm version patch -m "Upgrade to %s for reasons" 11 | ``` 12 | aside from patch, you can use the keywords `minor`, and `major` (there are some others). 13 | 14 | Once this is complete, log into GitHub and do an offical release of the package. A published release will result in a new published version on NPM (via a GitHub Action) 15 | 16 | -------------------------------------------------------------------------------- /app/fileOperations.js: -------------------------------------------------------------------------------- 1 | // FileSaver isn't really a dependency, we use it here to provide an example. 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import FileSaver from 'file-saver'; 4 | 5 | /** **************************************** 6 | * Below are a few helper methods so we can upload and download files 7 | * easily from the editor for testing purposes. 8 | * ----------------------------------------- 9 | */ 10 | export default function setupFileOperations(bpmnModeler) { 11 | /** 12 | * Just a quick bit of code so we can save the XML that is output. 13 | * Helps for debugging against other libraries (like SpiffWorkflow) 14 | */ 15 | 16 | const btn = document.getElementById('downloadButton'); 17 | btn.addEventListener('click', (_event) => { 18 | saveXML(); 19 | }); 20 | 21 | async function saveXML() { 22 | const { xml } = await bpmnModeler.saveXML({ format: true }); 23 | const blob = new Blob([xml], { type: 'text/xml' }); 24 | FileSaver.saveAs(blob, 'diagram.bpmn'); 25 | } 26 | 27 | /** 28 | * Just a quick bit of code so we can open a local XML file 29 | * Helps for debugging against other libraries (like SpiffWorkflow) 30 | */ 31 | const uploadBtn = document.getElementById('uploadButton'); 32 | uploadBtn.addEventListener('click', (_event) => { 33 | openFile(bpmnModeler); 34 | }); 35 | } 36 | 37 | function clickElem(elem) { 38 | const eventMouse = document.createEvent('MouseEvents'); 39 | eventMouse.initMouseEvent( 40 | 'click', 41 | true, 42 | false, 43 | window, 44 | 0, 45 | 0, 46 | 0, 47 | 0, 48 | 0, 49 | false, 50 | false, 51 | false, 52 | false, 53 | 0, 54 | null 55 | ); 56 | elem.dispatchEvent(eventMouse); 57 | } 58 | 59 | export function openFile(bpmnModeler) { 60 | const readFile = function readFileCallback(e) { 61 | const file = e.target.files[0]; 62 | if (!file) { 63 | return; 64 | } 65 | const reader = new FileReader(); 66 | reader.onload = function onloadCallback(onloadEvent) { 67 | const contents = onloadEvent.target.result; 68 | bpmnModeler.importXML(contents); 69 | document.body.removeChild(fileInput); 70 | }; 71 | reader.readAsText(file); 72 | }; 73 | let fileInput = document.createElement('input'); 74 | fileInput.type = 'file'; 75 | fileInput.style.display = 'none'; 76 | fileInput.onchange = readFile; 77 | document.body.appendChild(fileInput); 78 | clickElem(fileInput); 79 | } 80 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | bpmn-js-spiffworkflow 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 65 |
66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 |
78 | 79 |
80 |
81 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataObject/DataObjectHelpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the moddelElement if it is a process, otherwise, returns the 3 | * 4 | * @param container 5 | */ 6 | 7 | export function findDataObjects(parent, dataObjects) { 8 | if (typeof (dataObjects) === 'undefined') 9 | dataObjects = []; 10 | let process; 11 | if (!parent) { 12 | return []; 13 | } 14 | if (parent.processRef) { 15 | process = parent.processRef; 16 | } else { 17 | process = parent; 18 | if (process.$type === 'bpmn:SubProcess') 19 | findDataObjects(process.$parent, dataObjects); 20 | } 21 | if (typeof (process.flowElements) !== 'undefined') { 22 | for (const element of process.flowElements) { 23 | if (element.$type === 'bpmn:DataObject') 24 | dataObjects.push(element); 25 | } 26 | } 27 | return dataObjects; 28 | } 29 | 30 | export function findDataObject(process, id) { 31 | for (const dataObj of findDataObjects(process)) { 32 | if (dataObj.id === id) { 33 | return dataObj; 34 | } 35 | } 36 | } 37 | 38 | export function findDataObjectReferences(children, dataObjectId) { 39 | if (children == null) { 40 | return []; 41 | } 42 | return children.flatMap((child) => { 43 | if (child.$type == 'bpmn:DataObjectReference' && child.dataObjectRef.id == dataObjectId) 44 | return [child]; 45 | else if (child.$type == 'bpmn:SubProcess') 46 | return findDataObjectReferences(child.get('flowElements'), dataObjectId); 47 | else 48 | return []; 49 | }); 50 | } 51 | 52 | export function findDataObjectReferenceShapes(children, dataObjectId) { 53 | return children.flatMap((child) => { 54 | if (child.type == 'bpmn:DataObjectReference' && child.businessObject.dataObjectRef.id == dataObjectId) 55 | return [child]; 56 | else if (child.type == 'bpmn:SubProcess') 57 | return findDataObjectReferenceShapes(child.children, dataObjectId); 58 | else 59 | return []; 60 | }); 61 | } 62 | 63 | export function idToHumanReadableName(id) { 64 | const words = id.match(/[A-Za-z][a-z]*|[0-9]+/g) || [id]; 65 | return words.map(capitalize).join(' '); 66 | 67 | function capitalize(word) { 68 | return word.charAt(0).toUpperCase() + word.substring(1); 69 | } 70 | } 71 | 72 | export function updateDataObjectReferencesName(parent, nameValue, dataObjectId, commandStack) { 73 | const references = findDataObjectReferenceShapes(parent.children, dataObjectId); 74 | for (const ref of references) { 75 | const stateName = ref.businessObject.dataState && ref.businessObject.dataState.name ? ref.businessObject.dataState.name : ''; 76 | const newName = stateName ? `${nameValue} [${stateName}]` : nameValue; 77 | commandStack.execute('element.updateProperties', { 78 | element: ref, 79 | moddleElement: ref.businessObject, 80 | properties: { 81 | name: newName, 82 | }, 83 | changed: [ref], 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataObject/DataObjectLabelEditingProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { findDataObject, updateDataObjectReferencesName } from './DataObjectHelpers'; 3 | 4 | export default function DataObjectLabelEditingProvider(eventBus, directEditing, commandStack, modeling) { 5 | 6 | let el; 7 | 8 | // listen to dblclick on non-root elements 9 | eventBus.on('element.dblclick', function (event) { 10 | const { element } = event; 11 | if (is(element.businessObject, 'bpmn:DataObjectReference')) { 12 | let label = element.businessObject.name; 13 | label = label.replace(/\s*\[.*?\]\s*$/, ''); 14 | modeling.updateLabel(element, label); 15 | directEditing.activate(element); 16 | el = element; 17 | } 18 | }); 19 | 20 | eventBus.on('directEditing.complete', function (event) { 21 | 22 | const element = el; 23 | 24 | if (element && is(element.businessObject, 'bpmn:DataObjectReference')) { 25 | 26 | setTimeout(() => { 27 | const process = element.parent.businessObject; 28 | const dataObject = findDataObject(process, element.businessObject.dataObjectRef.id); 29 | const dataState = element.businessObject.dataState && element.businessObject.dataState.name; 30 | 31 | let newLabel = element.businessObject.name; 32 | 33 | commandStack.execute('element.updateModdleProperties', { 34 | element, 35 | moddleElement: dataObject, 36 | properties: { 37 | name: newLabel, 38 | }, 39 | }); 40 | 41 | // Update references name 42 | updateDataObjectReferencesName(element.parent, newLabel, dataObject.id, commandStack); 43 | 44 | // Append the data state if it exists 45 | if (dataState) { 46 | newLabel += ` [${dataState}]`; 47 | } 48 | 49 | // Update the label with the data state 50 | modeling.updateLabel(element, newLabel); 51 | el = undefined; 52 | }, 100); 53 | } 54 | }); 55 | } 56 | 57 | DataObjectLabelEditingProvider.$inject = [ 58 | 'eventBus', 59 | 'directEditing', 60 | 'commandStack', 61 | 'modeling' 62 | ]; -------------------------------------------------------------------------------- /app/spiffworkflow/DataObject/DataObjectRenderer.js: -------------------------------------------------------------------------------- 1 | import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; 2 | 3 | import { 4 | attr as svgAttr 5 | } from 'tiny-svg'; 6 | 7 | import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil'; 8 | import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; 9 | import { findDataObject } from './DataObjectHelpers'; 10 | 11 | const HIGH_PRIORITY = 1500; 12 | 13 | /** 14 | * Work in progress -- render data object references in red if they are 15 | * not valid. 16 | */ 17 | export default class DataObjectRenderer extends BaseRenderer { 18 | constructor(eventBus, bpmnRenderer) { 19 | super(eventBus, HIGH_PRIORITY); 20 | this.bpmnRenderer = bpmnRenderer; 21 | } 22 | 23 | canRender(element) { 24 | return isAny(element, [ 'bpmn:DataObjectReference' ]) && !element.labelTarget; 25 | } 26 | 27 | drawShape(parentNode, element) { 28 | const shape = this.bpmnRenderer.drawShape(parentNode, element); 29 | if (is(element, 'bpmn:DataObjectReference')) { 30 | let businessObject = getBusinessObject(element); 31 | let dataObject = businessObject.dataObjectRef; 32 | if (dataObject && dataObject.id) { 33 | let parentObject = businessObject.$parent; 34 | dataObject = findDataObject(parentObject, dataObject.id); 35 | } 36 | if (!dataObject) { 37 | svgAttr(shape, 'stroke', 'red'); 38 | } 39 | return shape; 40 | } 41 | } 42 | } 43 | 44 | DataObjectRenderer.$inject = [ 'eventBus', 'bpmnRenderer' ]; 45 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataObject/DataObjectRules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom Rules for the DataObject - Rules allow you to prevent an 3 | * action from happening in the diagram, such as dropping an element 4 | * where it doesn't belong. 5 | * 6 | * Here we don't allow people to move a data object Reference 7 | * from one parent to another, as we can't move the data objects 8 | * from one parent to another. 9 | * 10 | */ 11 | import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; 12 | import inherits from 'inherits-browser'; 13 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 14 | 15 | export default function DataObjectRules(eventBus) { 16 | RuleProvider.call(this, eventBus); 17 | } 18 | inherits(DataObjectRules, RuleProvider); 19 | const HIGH_PRIORITY = 1500; 20 | 21 | DataObjectRules.prototype.init = function() { 22 | this.addRule('elements.move', HIGH_PRIORITY,function(context) { 23 | let elements = context.shapes; 24 | let target = context.target; 25 | return canDrop(elements, target); 26 | }); 27 | }; 28 | 29 | function canDrop(elements, target) { 30 | for (let element of elements) { 31 | if (is(element, 'bpmn:DataObjectReference') && element.parent && target) { 32 | return target === element.parent; 33 | } 34 | // Intentionally returning null here to allow other rules to fire. 35 | } 36 | } 37 | 38 | DataObjectRules.prototype.canDrop = canDrop; 39 | DataObjectRules.$inject = [ 'eventBus' ]; 40 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataObject/index.js: -------------------------------------------------------------------------------- 1 | import DataObjectInterceptor from './DataObjectInterceptor'; 2 | import DataObjectRules from './DataObjectRules'; 3 | import RulesModule from 'diagram-js/lib/features/rules'; 4 | import DataObjectRenderer from './DataObjectRenderer'; 5 | import DataObjectPropertiesProvider from './propertiesPanel/DataObjectPropertiesProvider'; 6 | import DataObjectLabelEditingProvider from './DataObjectLabelEditingProvider'; 7 | 8 | export default { 9 | __depends__: [ 10 | RulesModule 11 | ], 12 | __init__: [ 'dataInterceptor', 'dataObjectRules', 'dataObjectRenderer', 'dataObjectPropertiesProvider', 'dataObjectLabelEditingProvider' ], 13 | dataInterceptor: [ 'type', DataObjectInterceptor ], 14 | dataObjectRules: [ 'type', DataObjectRules ], 15 | dataObjectRenderer: [ 'type', DataObjectRenderer ], 16 | dataObjectPropertiesProvider: [ 'type', DataObjectPropertiesProvider ], 17 | dataObjectLabelEditingProvider: [ 'type', DataObjectLabelEditingProvider ] 18 | }; 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataObject/propertiesPanel/DataObjectSelect.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { SelectEntry } from '@bpmn-io/properties-panel'; 4 | import { findDataObjects } from '../DataObjectHelpers'; 5 | 6 | /** 7 | * Finds the value of the given type within the extensionElements 8 | * given a type of "spiff:preScript", would find it in this, and return 9 | * the object. 10 | * 11 | * 13 | 14 | 15 | me = "100% awesome" 16 | 17 | 18 | ... 19 | 20 | * 21 | * @returns {string|null|*} 22 | */ 23 | export function DataObjectSelect(props) { 24 | const element = props.element; 25 | const commandStack = props.commandStack; 26 | const debounce = useService('debounceInput'); 27 | 28 | const getValue = () => { 29 | return element.businessObject.dataObjectRef.id; 30 | }; 31 | 32 | const setValue = (value) => { 33 | const businessObject = element.businessObject; 34 | const dataObjects = findDataObjects(businessObject.$parent); 35 | for (const dataObject of dataObjects) { 36 | if (dataObject.$type === 'bpmn:DataObject' && dataObject.id === value) { 37 | commandStack.execute('element.updateModdleProperties', { 38 | element: element, 39 | moddleElement: businessObject, 40 | properties: { 41 | dataObjectRef: dataObject, 42 | }, 43 | }); 44 | 45 | // Construct the new name by : the dataObject name and the current state 46 | const stateName = 47 | businessObject.dataState && businessObject.dataState.name 48 | ? businessObject.dataState.name 49 | : ''; 50 | const newName = stateName 51 | ? `${dataObject.name} [${stateName}]` 52 | : dataObject.name; 53 | // Update the name property of the DataObjectReference 54 | commandStack.execute('element.updateProperties', { 55 | element: element, 56 | properties: { 57 | name: newName, 58 | }, 59 | }); 60 | } 61 | } 62 | }; 63 | 64 | const getOptions = (value) => { 65 | const businessObject = element.businessObject; 66 | const parent = businessObject.$parent; 67 | let dataObjects = findDataObjects(parent); 68 | let options = []; 69 | dataObjects.forEach((dataObj) => { 70 | options.push({ label: dataObj.id, value: dataObj.id }); 71 | }); 72 | return options; 73 | }; 74 | 75 | return ( 76 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataObject/propertiesPanel/DataReferenceGroup.js: -------------------------------------------------------------------------------- 1 | import { ListGroup } from '@bpmn-io/properties-panel'; 2 | import { DataObjectArray } from './DataObjectArray'; 3 | 4 | /** 5 | * Also allows you to select which Data Objects are available 6 | * in the process element. 7 | * @param element The selected process 8 | * @param moddle For updating the underlying xml object 9 | * @returns {[{component: (function(*)), isEdited: *, id: string, element},{component: 10 | * (function(*)), isEdited: *, id: string, element}]} 11 | */ 12 | export default function(element, moddle) { 13 | 14 | const groupSections = []; 15 | const dataObjectArray = { 16 | id: 'editDataObjects', 17 | element, 18 | label: 'Available Data Objects', 19 | component: ListGroup, 20 | ...DataObjectArray({ element, moddle }) 21 | }; 22 | 23 | if (dataObjectArray.items) { 24 | groupSections.push(dataObjectArray); 25 | } 26 | 27 | return groupSections; 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataStoreReference/DataStoreHelpers.js: -------------------------------------------------------------------------------- 1 | export function isDataStoreReferenced(process, dataStoreId) { 2 | const status = process.get('flowElements').some(elem => 3 | elem.$type === 'bpmn:DataStoreReference' && elem.dataStoreRef && elem.dataStoreRef.id === dataStoreId 4 | ); 5 | return status; 6 | } 7 | 8 | export function isDataStoreReferencedV2(definitions, dataStoreId) { 9 | return definitions.get('rootElements').some(elem => 10 | elem.$type === 'bpmn:DataStoreReference' && elem.dataStoreRef && elem.dataStoreRef.id === dataStoreId 11 | ); 12 | } 13 | 14 | export function removeDataStore(definitions, dataStoreId) { 15 | definitions.set('rootElements', definitions.get('rootElements').filter(elem => 16 | !(elem.$type === 'bpmn:DataStore' && elem.id === dataStoreId) 17 | )); 18 | } -------------------------------------------------------------------------------- /app/spiffworkflow/DataStoreReference/DataStoreInterceptor.js: -------------------------------------------------------------------------------- 1 | import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; 2 | import { getDi, is } from 'bpmn-js/lib/util/ModelUtil'; 3 | import { isDataStoreReferenced, removeDataStore } from './DataStoreHelpers'; 4 | 5 | const HIGH_PRIORITY = 1500; 6 | 7 | /** 8 | * 9 | */ 10 | export default class DataStoreInterceptor extends CommandInterceptor { 11 | 12 | constructor(eventBus, bpmnFactory, commandStack, bpmnUpdater) { 13 | super(eventBus); 14 | 15 | /* 16 | * 17 | */ 18 | // bpmnUpdater.updateSemanticParent = (businessObject, parentBusinessObject) => { 19 | // if (is(businessObject, 'bpmn:DataStoreReference')) { 20 | // console.log('updateSemanticParent', businessObject, parentBusinessObject); 21 | // bpmnUpdater.__proto__.updateSemanticParent.call(bpmnUpdater, businessObject, parentBusinessObject); 22 | // } 23 | // }; 24 | 25 | /** 26 | * 27 | */ 28 | // this.preExecute(['shape.create'], HIGH_PRIORITY, function (event) { 29 | // const { context } = event; 30 | // const { shape } = context; 31 | // if (is(shape, 'bpmn:DataStoreReference') && shape.type !== 'label') { 32 | // // event.stopPropagation();* 33 | // console.log('preExecute shape.create', shape, context); 34 | // } 35 | // }); 36 | 37 | /** 38 | * 39 | */ 40 | // this.executed(['shape.create'], HIGH_PRIORITY, function (event) { 41 | // const { context } = event; 42 | // const { shape } = context; 43 | // if (is(shape, 'bpmn:DataStoreReference') && shape.type !== 'label') { 44 | // console.log('executed shape.create', shape, context); 45 | // } 46 | // }); 47 | 48 | /** 49 | * 50 | */ 51 | // this.postExecuted(['shape.create'], HIGH_PRIORITY, function (event) { 52 | // const { context } = event; 53 | // const { shape } = context; 54 | // if (is(shape, 'bpmn:DataStoreReference') && shape.type !== 'label') { 55 | // console.log('postExecuted shape.create', shape, context); 56 | // } 57 | // }); 58 | 59 | /** 60 | * 61 | */ 62 | this.postExecuted(['shape.delete'], HIGH_PRIORITY, function (event) { 63 | const { context } = event; 64 | const { shape } = context; 65 | 66 | if (is(shape, 'bpmn:DataStoreReference') && shape.type !== 'label') { 67 | const definitions = context.oldParent.businessObject.$parent; 68 | const dataStore = shape.businessObject.dataStoreRef; 69 | if (dataStore && !isDataStoreReferenced(context.oldParent.businessObject, dataStore.id)) { 70 | // Remove datastore if it's not linked with another datastore ref 71 | removeDataStore(definitions, dataStore.id); 72 | } 73 | } 74 | }); 75 | } 76 | } 77 | 78 | DataStoreInterceptor.$inject = ['eventBus', 'bpmnFactory', 'commandStack', 'bpmnUpdater']; 79 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataStoreReference/index.js: -------------------------------------------------------------------------------- 1 | import RulesModule from 'diagram-js/lib/features/rules'; 2 | import DataStorePropertiesProvider from './propertiesPanel/DataStorePropertiesProvider'; 3 | import DataStoreInterceptor from './DataStoreInterceptor'; 4 | 5 | export default { 6 | __depends__: [ 7 | RulesModule 8 | ], 9 | __init__: [ 'dataStoreInterceptor', 'dataStorePropertiesProvider' ], 10 | dataStoreInterceptor: [ 'type', DataStoreInterceptor ], 11 | dataStorePropertiesProvider: [ 'type', DataStorePropertiesProvider ] 12 | }; 13 | -------------------------------------------------------------------------------- /app/spiffworkflow/DataStoreReference/propertiesPanel/DataStorePropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { DataStoreSelect, OPTION_TYPE } from './DataStoreSelect'; 3 | 4 | const LOW_PRIORITY = 500; 5 | 6 | export default function DataStorePropertiesProvider( 7 | modeling, 8 | propertiesPanel, 9 | translate, 10 | moddle, 11 | commandStack, 12 | bpmnFactory, 13 | ) { 14 | this.getGroups = function (element) { 15 | return function (groups) { 16 | if (is(element, 'bpmn:DataStoreReference')) { 17 | groups.push( 18 | createCustomDataStoreGroup( 19 | modeling, 20 | element, 21 | translate, 22 | moddle, 23 | commandStack, 24 | bpmnFactory 25 | ) 26 | ); 27 | } 28 | return groups; 29 | }; 30 | }; 31 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 32 | } 33 | 34 | DataStorePropertiesProvider.$inject = [ 35 | 'modeling', 36 | 'propertiesPanel', 37 | 'translate', 38 | 'moddle', 39 | 'commandStack', 40 | 'bpmnFactory', 41 | ]; 42 | 43 | function createCustomDataStoreGroup( 44 | modeling, 45 | element, 46 | translate, 47 | moddle, 48 | commandStack, 49 | bpmnFactory 50 | ) { 51 | 52 | const { businessObject } = element; 53 | 54 | const group = { 55 | label: translate('Custom Data Store Properties'), 56 | id: 'custom-datastore-properties', 57 | entries: [], 58 | }; 59 | 60 | let description = translate('Select a datasource from the list'); 61 | if(businessObject.dataStoreRef){ 62 | const dataStoreId = businessObject.dataStoreRef.id; 63 | const type = businessObject.get('type'); 64 | description = `The selected data store is of type: ${type}`; 65 | } 66 | 67 | // other custom properties as needed 68 | group.entries.push({ 69 | id: 'selectDataStore', 70 | element, 71 | component: DataStoreSelect, 72 | optionType: OPTION_TYPE.data_stores, 73 | moddle, 74 | commandStack, 75 | translate, 76 | name: 'dataStoreRef', 77 | label: translate('Select DataSource'), 78 | description, 79 | modeling, 80 | bpmnFactory, 81 | }); 82 | 83 | return group; 84 | } 85 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/IoPalette.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'min-dash'; 2 | import translate from 'diagram-js/lib/i18n/translate/translate'; 3 | 4 | /** 5 | * Add data inputs and data outputs to the panel. 6 | */ 7 | export default function IoPalette(palette, create, elementFactory,) { 8 | this._create = create; 9 | this._elementFactory = elementFactory; 10 | palette.registerProvider(this); 11 | } 12 | 13 | IoPalette.$inject = [ 14 | 'palette', 15 | 'create', 16 | 'elementFactory' 17 | ]; 18 | 19 | IoPalette.prototype.getPaletteEntries = function() { 20 | 21 | let input_type = 'bpmn:DataInput'; 22 | let output_type = 'bpmn:DataOutput'; 23 | let elementFactory = this._elementFactory, create = this._create; 24 | 25 | function createListener(event, type) { 26 | let shape = elementFactory.createShape(assign({ type: type }, {})); 27 | shape.width = 36; // Fix up the shape dimensions from the defaults. 28 | shape.height = 50; 29 | create.start(event, shape); 30 | } 31 | 32 | function createInputListener(event) { 33 | createListener(event, input_type); 34 | } 35 | 36 | function createOutputListener(event) { 37 | createListener(event, output_type); 38 | } 39 | 40 | return { 41 | 'create.data-input': { 42 | group: 'data-object', 43 | className: 'bpmn-icon-data-input', 44 | title: translate('Create DataInput'), 45 | action: { 46 | dragstart: createInputListener, 47 | click: createInputListener 48 | } 49 | }, 50 | 'create.data-output': { 51 | group: 'data-object', 52 | className: 'bpmn-icon-data-output', 53 | title: translate('Create DataOutput'), 54 | action: { 55 | dragstart: createOutputListener, 56 | click: createOutputListener 57 | } 58 | } 59 | 60 | }; 61 | }; 62 | 63 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/IoRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits'; 2 | import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; 3 | 4 | const HIGH_PRIORITY = 1500; 5 | 6 | /** 7 | * A custom rule provider that will permit Data Inputs and Data 8 | * Outputs to be placed within a process element (something BPMN.io currently denies) 9 | * 10 | * See {@link BpmnRules} for the default implementation 11 | * of BPMN 2.0 modeling rules provided by bpmn-js. 12 | * 13 | * @param {EventBus} eventBus 14 | */ 15 | export default function IoRules(eventBus) { 16 | RuleProvider.call(this, eventBus); 17 | } 18 | 19 | inherits(IoRules, RuleProvider); 20 | 21 | IoRules.$inject = [ 'eventBus' ]; 22 | 23 | IoRules.prototype.init = function() { 24 | this.addRule('shape.create', HIGH_PRIORITY, function(context) { 25 | 26 | let element = context.shape; 27 | let target = context.target; 28 | let position = context.position; 29 | 30 | return canCreate(element, target, position); 31 | }); 32 | }; 33 | 34 | /** 35 | * Allow folks to drop a dataInput or DataOutput only on the top level process. 36 | */ 37 | function canCreate(element, target, position) { 38 | if ([ 'bpmn:DataInput', 'bpmn:DataOutput' ].includes(element.type)) { 39 | if (target.type == 'bpmn:Process') { 40 | return true; 41 | } 42 | } 43 | } 44 | 45 | IoRules.prototype.canCreate = canCreate; 46 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/helpers.js: -------------------------------------------------------------------------------- 1 | 2 | export function createSpecification(bpmnFactory, businessObject, type, newElement) { 3 | let ioSpecification = businessObject.ioSpecification; 4 | if (!ioSpecification) { 5 | ioSpecification = bpmnFactory.create('bpmn:InputOutputSpecification', { 6 | dataInputs: [], 7 | dataOutputs: [], 8 | inputSets: [], 9 | outputSets: [], 10 | }); 11 | 12 | businessObject.ioSpecification = ioSpecification; 13 | } 14 | 15 | if (type === 'input') { 16 | ioSpecification.dataInputs.push(newElement); 17 | if (!ioSpecification.inputSets.length) { 18 | ioSpecification.inputSets.push(bpmnFactory.create('bpmn:InputSet', { dataInputRefs: [newElement] })); 19 | } else { 20 | ioSpecification.inputSets[0].dataInputRefs.push(newElement); 21 | } 22 | } else if (type === 'output') { 23 | ioSpecification.dataOutputs.push(newElement); 24 | if (!ioSpecification.outputSets.length) { 25 | ioSpecification.outputSets.push(bpmnFactory.create('bpmn:OutputSet', { dataOutputRefs: [newElement] })); 26 | } else { 27 | ioSpecification.outputSets[0].dataOutputRefs.push(newElement); 28 | } 29 | } 30 | 31 | return ioSpecification; 32 | } 33 | 34 | export function removeElementFromSpecification(element, entry, type) { 35 | const ioSpecification = element.businessObject.ioSpecification; 36 | if (!ioSpecification) { 37 | console.error('No ioSpecification found for this element.'); 38 | return; 39 | } 40 | 41 | const collection = type === 'input' ? ioSpecification.dataInputs : ioSpecification.dataOutputs; 42 | const setCollection = type === 'input' ? ioSpecification.inputSets : ioSpecification.outputSets; 43 | const index = collection.findIndex(item => item.id === entry.id); 44 | 45 | if (index > -1) { 46 | const [removedElement] = collection.splice(index, 1); 47 | setCollection.forEach(set => { 48 | const refIndex = set[type === 'input' ? 'dataInputRefs' : 'dataOutputRefs'].indexOf(removedElement); 49 | if (refIndex > -1) { 50 | set[type === 'input' ? 'dataInputRefs' : 'dataOutputRefs'].splice(refIndex, 1); 51 | } 52 | }); 53 | } else { 54 | console.error(`No ${type === 'input' ? 'DataInput' : 'DataOutput'} found for id ${entry.id}`); 55 | } 56 | } 57 | 58 | export function updateElementProperties(commandStack, element) { 59 | commandStack.execute('element.updateProperties', { 60 | element: element, 61 | moddleElement: element.businessObject, 62 | properties: {} 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/index.js: -------------------------------------------------------------------------------- 1 | import IoPalette from './IoPalette'; 2 | import IoRules from './IoRules'; 3 | import IoInterceptor from './IoInterceptor'; 4 | import IoPropertiesProvider from './propertiesProvider/IoPropertiesProvider'; 5 | 6 | export default { 7 | __init__: [ 'IoPalette', 'IoRules', 'IoInterceptor', 'IoPropertiesProvider' ], 8 | IoPalette: [ 'type', IoPalette ], 9 | IoRules: [ 'type', IoRules ], 10 | IoInterceptor: [ 'type', IoInterceptor ], 11 | IoPropertiesProvider: [ 'type', IoPropertiesProvider ] 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/propertiesProvider/InputParametersArray.js: -------------------------------------------------------------------------------- 1 | import { useService } from 'bpmn-js-properties-panel'; 2 | import { 3 | isTextFieldEntryEdited, 4 | TextFieldEntry, 5 | } from '@bpmn-io/properties-panel'; 6 | import { createSpecification, removeElementFromSpecification, updateElementProperties } from '../helpers'; 7 | 8 | export function InputParametersArray(props) { 9 | 10 | const { element, moddle, translate, commandStack, bpmnFactory } = props; 11 | const { businessObject } = element; 12 | 13 | const ioSpecification = businessObject.ioSpecification; 14 | 15 | const inputsEntries = (ioSpecification) ? ioSpecification.dataInputs : []; 16 | 17 | const items = (inputsEntries) ? inputsEntries.map((inputEntry, index) => { 18 | const id = `inputEntry-${index}`; 19 | return { 20 | id, 21 | label: translate(inputEntry.name), 22 | entries: InputParamGroup({ 23 | element, 24 | commandStack, 25 | moddle, 26 | translate, 27 | bpmnFactory, 28 | inputEntry 29 | }), 30 | autoFocusEntry: `input-focus-entry`, 31 | remove: removeFactory({ 32 | element, moddle, commandStack, inputEntry 33 | }), 34 | }; 35 | }) : []; 36 | 37 | function add(event) { 38 | const { businessObject } = element; 39 | 40 | const newInputID = moddle.ids.nextPrefixed('DataInput_'); 41 | 42 | // Create a new DataInput 43 | const newInput = bpmnFactory.create('bpmn:DataInput', { id: newInputID, name: newInputID }); 44 | 45 | // Check if ioSpecification already exists 46 | createSpecification(bpmnFactory, businessObject, 'input', newInput) 47 | 48 | // Update the element 49 | updateElementProperties(commandStack, element); 50 | 51 | event.stopPropagation(); 52 | } 53 | 54 | return { items, add }; 55 | } 56 | 57 | function removeFactory(props) { 58 | const { element, bpmnFactory, commandStack, inputEntry } = props; 59 | return function (event) { 60 | event.stopPropagation(); 61 | removeElementFromSpecification(element, inputEntry, 'input'); 62 | updateElementProperties(commandStack, element); 63 | }; 64 | } 65 | 66 | function InputParamGroup(props) { 67 | 68 | const { id, inputEntry, element, moddle, commandStack, translate, bpmnFactory } = props; 69 | 70 | return [ 71 | { 72 | id, 73 | inputEntry, 74 | component: InputParamTextField, 75 | isEdited: isTextFieldEntryEdited, 76 | element, 77 | moddle, 78 | commandStack, 79 | translate, 80 | bpmnFactory 81 | } 82 | ]; 83 | } 84 | 85 | function InputParamTextField(props) { 86 | 87 | const { id, element, inputEntry, moddle, commandStack, translate, bpmnFactory } = props; 88 | 89 | const debounce = useService('debounceInput'); 90 | 91 | const setValue = (value) => { 92 | try { 93 | const ioSpecification = element.businessObject.ioSpecification; 94 | 95 | if (!value || value == '') { 96 | console.error('No value provided for this input.'); 97 | return; 98 | } 99 | 100 | if (!ioSpecification) { 101 | console.error('No ioSpecification found for this element.'); 102 | return; 103 | } 104 | 105 | let existingInput = ioSpecification.dataInputs.find(input => input.id === inputEntry.name || input.name === inputEntry.name); 106 | 107 | if (existingInput) { 108 | existingInput.name = value; 109 | existingInput.id = value; 110 | } else { 111 | console.error(`No DataInput found :> ${inputEntry.name}`); 112 | return; 113 | } 114 | 115 | updateElementProperties(commandStack, element); 116 | 117 | } catch (error) { 118 | console.log('Setting Value Error : ', error); 119 | } 120 | }; 121 | 122 | const getValue = () => { 123 | return inputEntry.name; 124 | }; 125 | 126 | return TextFieldEntry({ 127 | element, 128 | id: `${id}-input`, 129 | label: translate('Input Name'), 130 | getValue, 131 | setValue, 132 | debounce, 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/propertiesProvider/IoGroup.js: -------------------------------------------------------------------------------- 1 | import { ListGroup, DescriptionEntry } from '@bpmn-io/properties-panel'; 2 | import { InputParametersArray } from './InputParametersArray.js'; 3 | import { OutputParametersArray } from './OutputParametersArray.js'; 4 | 5 | export function createIoGroup( 6 | element, 7 | translate, 8 | moddle, 9 | commandStack, 10 | bpmnFactory 11 | ) { 12 | 13 | const group = { 14 | label: translate('Input/Output Management'), 15 | id: 'ioProperties', 16 | entries: [], 17 | }; 18 | 19 | // add description input 20 | group.entries.push({ 21 | id: `infos-textField`, 22 | component: DescriptionEntry, 23 | value: 24 | 'ℹ️ When no specific inputs/outputs are defined, all process variables are accessible.', 25 | element, 26 | translate, 27 | commandStack, 28 | }); 29 | 30 | // add input list component 31 | group.entries.push({ 32 | id: 'inputParameters', 33 | label: translate('Inputs'), 34 | component: ListGroup, 35 | ...InputParametersArray({ 36 | element, 37 | moddle, 38 | translate, 39 | commandStack, 40 | bpmnFactory 41 | }), 42 | }); 43 | 44 | // add output list component 45 | group.entries.push({ 46 | id: 'outputParameters', 47 | label: translate('Outputs'), 48 | component: ListGroup, 49 | ...OutputParametersArray({ 50 | element, 51 | moddle, 52 | translate, 53 | commandStack, 54 | bpmnFactory 55 | }) 56 | }); 57 | 58 | return group; 59 | } 60 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/propertiesProvider/IoPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { createIoGroup } from './IoGroup.js'; 3 | 4 | const LOW_PRIORITY = 500; 5 | 6 | export default function IoPropertiesProvider( 7 | propertiesPanel, 8 | translate, 9 | moddle, 10 | commandStack, 11 | elementRegistry, 12 | bpmnFactory 13 | ) { 14 | this.getGroups = function getGroupsCallback(element) { 15 | return function pushGroup(groups) { 16 | if (isBpmnTask(element)) { 17 | groups.push( 18 | createIoGroup( 19 | element, 20 | translate, 21 | moddle, 22 | commandStack, 23 | bpmnFactory 24 | ) 25 | ); 26 | } 27 | return groups; 28 | }; 29 | }; 30 | 31 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 32 | } 33 | 34 | IoPropertiesProvider.$inject = [ 35 | 'propertiesPanel', 36 | 'translate', 37 | 'moddle', 38 | 'commandStack', 39 | 'elementRegistry', 40 | 'bpmnFactory', 41 | ]; 42 | 43 | function isBpmnTask(element) { 44 | if (!element) { 45 | return false; 46 | } 47 | return ( 48 | is(element, 'bpmn:UserTask') || 49 | is(element, 'bpmn:ScriptTask') || 50 | is(element, 'bpmn:ServiceTask') || 51 | is(element, 'bpmn:SendTask') || 52 | is(element, 'bpmn:ReceiveTask') || 53 | is(element, 'bpmn:ManualTask') 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/spiffworkflow/InputOutput/propertiesProvider/OutputParametersArray.js: -------------------------------------------------------------------------------- 1 | import { useService } from 'bpmn-js-properties-panel'; 2 | import { 3 | isTextFieldEntryEdited, 4 | TextFieldEntry, 5 | } from '@bpmn-io/properties-panel'; 6 | import { createSpecification, removeElementFromSpecification, updateElementProperties } from '../helpers'; 7 | 8 | export function OutputParametersArray(props) { 9 | 10 | const { element, moddle, translate, commandStack, bpmnFactory } = props; 11 | const { businessObject } = element; 12 | 13 | const ioSpecification = businessObject.ioSpecification; 14 | 15 | const outputsEntries = (ioSpecification) ? ioSpecification.dataOutputs : []; 16 | 17 | const items = (outputsEntries) ? outputsEntries.map((outputEntry, index) => { 18 | const id = `outputEntry-${index}`; 19 | return { 20 | id, 21 | label: translate(outputEntry.name), 22 | entries: OutputParamGroup({ 23 | element, 24 | commandStack, 25 | moddle, 26 | translate, 27 | bpmnFactory, 28 | outputEntry 29 | }), 30 | autoFocusEntry: `output-focus-entry`, 31 | remove: removeFactory({ 32 | element, moddle, commandStack, outputEntry 33 | }), 34 | }; 35 | }) : []; 36 | 37 | function add(event) { 38 | const { businessObject } = element; 39 | 40 | const newOutputID = moddle.ids.nextPrefixed('DataOutput_'); 41 | 42 | // Create a new DataOutput 43 | const newOutput = bpmnFactory.create('bpmn:DataOutput', { id: newOutputID, name: newOutputID }); 44 | 45 | // Check if ioSpecification already exists 46 | createSpecification(bpmnFactory, businessObject, 'output', newOutput) 47 | 48 | // Update the element 49 | updateElementProperties(commandStack, element); 50 | 51 | event.stopPropagation(); 52 | } 53 | 54 | return { items, add }; 55 | } 56 | 57 | function removeFactory(props) { 58 | const { element, bpmnFactory, commandStack, outputEntry } = props; 59 | return function (event) { 60 | event.stopPropagation(); 61 | removeElementFromSpecification(element, outputEntry, 'output'); 62 | updateElementProperties(commandStack, element); 63 | }; 64 | } 65 | 66 | function OutputParamGroup(props) { 67 | 68 | const { id, outputEntry, element, moddle, commandStack, translate, bpmnFactory } = props; 69 | 70 | return [ 71 | { 72 | id, 73 | outputEntry, 74 | component: OutputParamTextField, 75 | isEdited: isTextFieldEntryEdited, 76 | element, 77 | moddle, 78 | commandStack, 79 | translate, 80 | bpmnFactory 81 | } 82 | ]; 83 | } 84 | 85 | function OutputParamTextField(props) { 86 | 87 | const { id, element, outputEntry, moddle, commandStack, translate, bpmnFactory } = props; 88 | 89 | const debounce = useService('debounceInput'); 90 | 91 | const setValue = (value) => { 92 | try { 93 | const ioSpecification = element.businessObject.ioSpecification; 94 | 95 | if (!value || value == '') { 96 | console.error('No value provided for this input.'); 97 | return; 98 | } 99 | 100 | if (!ioSpecification) { 101 | console.error('No ioSpecification found for this element.'); 102 | return; 103 | } 104 | 105 | let existingInput = ioSpecification.dataOutputs.find(input => input.id === outputEntry.name || input.name === outputEntry.name); 106 | 107 | if (existingInput) { 108 | existingInput.name = value; 109 | existingInput.id = value; 110 | } else { 111 | console.error(`No DataOutput found :> ${outputEntry.name}`); 112 | return; 113 | } 114 | 115 | updateElementProperties(commandStack, element); 116 | 117 | } catch (error) { 118 | console.log('Setting Value Error : ', error); 119 | } 120 | }; 121 | 122 | const getValue = () => { 123 | return outputEntry.name; 124 | }; 125 | 126 | return TextFieldEntry({ 127 | element, 128 | id: `${id}-output`, 129 | label: translate('Output Name'), 130 | getValue, 131 | setValue, 132 | debounce, 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /app/spiffworkflow/callActivity/CallActivityInterceptor.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | export default function CallActivityInterceptor(eventBus, bpmnUpdater, overlays) { 4 | 5 | let OVERLAY_ID; 6 | 7 | eventBus.on('selection.changed', function (event) { 8 | 9 | var newSelection = event.newSelection; 10 | const element = (newSelection.length > 0) ? newSelection[0] : null; 11 | 12 | (OVERLAY_ID) ? overlays.remove(OVERLAY_ID) : null; 13 | 14 | if (element && is(element.businessObject, 'bpmn:CallActivity') && newSelection.length === 1) { 15 | var ARROW_DOWN_SVG = ' '; 16 | var button = domify(''); 17 | button.addEventListener('click', function () { 18 | const processId = getCalledElementValue(element); 19 | if (!processId || processId === '') { 20 | alert('Please select a process model first'); 21 | return; 22 | } 23 | eventBus.fire('spiff.callactivity.edit', { 24 | element, 25 | processId, 26 | }); 27 | }); 28 | OVERLAY_ID = overlays.add(element.id, 'drilldown', { 29 | position: { 30 | bottom: -10, 31 | right: -8 32 | }, 33 | html: button 34 | }); 35 | } 36 | 37 | }); 38 | } 39 | 40 | function domify(htmlString) { 41 | const template = document.createElement('template'); 42 | template.innerHTML = htmlString.trim(); 43 | return template.content.firstChild; 44 | } 45 | 46 | function getCalledElementValue(element) { 47 | const { calledElement } = element.businessObject; 48 | if (calledElement) { 49 | return calledElement; 50 | } 51 | return ''; 52 | } 53 | 54 | CallActivityInterceptor.$inject = ['eventBus', 'bpmnUpdater', 'overlays']; 55 | 56 | -------------------------------------------------------------------------------- /app/spiffworkflow/callActivity/index.js: -------------------------------------------------------------------------------- 1 | import CallActivityInterceptor from './CallActivityInterceptor'; 2 | import CallActivityPropertiesProvider from './propertiesPanel/CallActivityPropertiesProvider'; 3 | 4 | export default { 5 | __init__: ['callActivityPropertiesProvider'], 6 | callActivityPropertiesProvider: ['type', CallActivityPropertiesProvider], 7 | callActivityInterceptor: [ 'type', CallActivityInterceptor ], 8 | }; 9 | -------------------------------------------------------------------------------- /app/spiffworkflow/conditions/index.js: -------------------------------------------------------------------------------- 1 | import ConditionsPropertiesProvider from './propertiesPanel/ConditionsPropertiesProvider'; 2 | 3 | export default { 4 | __init__: ['conditionsPropertiesProvider'], 5 | conditionsPropertiesProvider: ['type', ConditionsPropertiesProvider], 6 | }; 7 | -------------------------------------------------------------------------------- /app/spiffworkflow/conditions/propertiesPanel/ConditionsPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { 3 | TextAreaEntry, 4 | isTextAreaEntryEdited 5 | } from '@bpmn-io/properties-panel'; 6 | import { useService } from 'bpmn-js-properties-panel'; 7 | 8 | const LOW_PRIORITY = 500; 9 | 10 | export default function ConditionsPropertiesProvider( 11 | propertiesPanel, 12 | translate, 13 | moddle, 14 | commandStack, 15 | _elementRegistry 16 | ) { 17 | this.getGroups = function getGroupsCallback(element) { 18 | return function pushGroup(groups) { 19 | if (is(element, 'bpmn:SequenceFlow')) { 20 | const { source } = element; 21 | if (is(source, 'bpmn:ExclusiveGateway') || is(source, 'bpmn:InclusiveGateway')) { 22 | groups.push( 23 | createConditionsGroup(element, translate, moddle, commandStack) 24 | ); 25 | } 26 | } else if (is(element, 'bpmn:Event')) { 27 | const eventDefinitions = element.businessObject.eventDefinitions; 28 | if (eventDefinitions.filter(ev => is(ev, 'bpmn:ConditionalEventDefinition')).length > 0) { 29 | groups.push( 30 | createConditionsGroup(element, translate, moddle, commandStack) 31 | ); 32 | } 33 | } 34 | return groups; 35 | }; 36 | }; 37 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 38 | } 39 | 40 | ConditionsPropertiesProvider.$inject = [ 41 | 'propertiesPanel', 42 | 'translate', 43 | 'moddle', 44 | 'commandStack', 45 | 'elementRegistry', 46 | ]; 47 | 48 | function createConditionsGroup(element, translate, moddle, commandStack) { 49 | return { 50 | id: 'conditions', 51 | label: translate('Conditions'), 52 | entries: conditionGroup( 53 | element, 54 | moddle, 55 | 'Condition Expression', 56 | 'Expression to Execute', 57 | commandStack 58 | ), 59 | }; 60 | } 61 | 62 | function conditionGroup(element, moddle, label, description, commandStack) { 63 | return [ 64 | { 65 | id: `condition_expression`, 66 | element, 67 | component: ConditionExpressionTextField, 68 | moddle, 69 | label, 70 | description, 71 | commandStack, 72 | isEdited: isTextAreaEntryEdited, 73 | }, 74 | ]; 75 | } 76 | 77 | function ConditionExpressionTextField(props) { 78 | const { element } = props; 79 | const { moddle } = props; 80 | const { label } = props; 81 | const { commandStack } = props; 82 | 83 | const debounce = useService('debounceInput'); 84 | 85 | const getValue = () => { 86 | let conditionExpression; 87 | if (is(element, 'bpmn:SequenceFlow')) { 88 | conditionExpression = element.businessObject.conditionExpression; 89 | } else if (is(element, 'bpmn:Event')) { 90 | const eventDef = element.businessObject.eventDefinitions.find((ev) => 91 | is(ev, 'bpmn:ConditionalEventDefinition') 92 | ); 93 | conditionExpression = eventDef.condition; 94 | } 95 | if (conditionExpression) { 96 | return conditionExpression.body; 97 | } 98 | return ''; 99 | }; 100 | 101 | const setValue = (value) => { 102 | let { conditionExpressionModdleElement } = element.businessObject; 103 | if (!conditionExpressionModdleElement) { 104 | conditionExpressionModdleElement = moddle.create('bpmn:Expression'); 105 | } 106 | conditionExpressionModdleElement.body = value; 107 | if (is(element, 'bpmn:SequenceFlow')) { 108 | element.businessObject.conditionExpression = 109 | conditionExpressionModdleElement; 110 | } else if (is(element, 'bpmn:Event')) { 111 | const eventDef = element.businessObject.eventDefinitions.find((ev) => 112 | is(ev, 'bpmn:ConditionalEventDefinition') 113 | ); 114 | eventDef.condition = conditionExpressionModdleElement; 115 | } 116 | 117 | commandStack.execute('element.updateModdleProperties', { 118 | element, 119 | moddleElement: conditionExpressionModdleElement, 120 | }); 121 | }; 122 | 123 | return TextAreaEntry({ 124 | element, 125 | id: 'condition_expression', 126 | label, 127 | getValue, 128 | setValue, 129 | debounce, 130 | }); 131 | } 132 | -------------------------------------------------------------------------------- /app/spiffworkflow/constants.js: -------------------------------------------------------------------------------- 1 | export const SPIFFWORKFLOW_XML_NAMESPACE = 'spiffworkflow'; 2 | export const SPIFF_ADD_MESSAGE_REQUESTED_EVENT = 'spiff.add_message.requested'; 3 | export const SPIFF_ADD_MESSAGE_RETURNED_EVENT = 'spiff.add_message.returned'; 4 | -------------------------------------------------------------------------------- /app/spiffworkflow/errors/index.js: -------------------------------------------------------------------------------- 1 | import ErrorPropertiesProvider from './propertiesPanel/ErrorPropertiesProvider'; 2 | 3 | export default { 4 | __init__: ['errorPropertiesProvider'], 5 | errorPropertiesProvider: ['type', ErrorPropertiesProvider], 6 | } 7 | -------------------------------------------------------------------------------- /app/spiffworkflow/errors/propertiesPanel/ErrorPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { getRoot } from '../../helpers'; 3 | import { getArrayForType, getListGroupForType } from '../../eventList.js'; 4 | import { hasEventType, 5 | replaceGroup, 6 | getSelectorForType, 7 | getConfigureGroupForType 8 | } from '../../eventSelect.js'; 9 | 10 | const LOW_PRIORITY = 500; 11 | 12 | const eventDetails = { 13 | 'eventType': 'bpmn:Error', 14 | 'eventDefType': 'bpmn:ErrorEventDefinition', 15 | 'referenceType': 'errorRef', 16 | 'idPrefix': 'error', 17 | }; 18 | 19 | export default function ErrorPropertiesProvider( 20 | propertiesPanel, 21 | translate, 22 | moddle, 23 | commandStack, 24 | ) { 25 | 26 | this.getGroups = function (element) { 27 | return function (groups) { 28 | if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) { 29 | const getErrorArray = getArrayForType('bpmn:Error', 'errorRef', 'Error'); 30 | const errorGroup = getListGroupForType('errors', 'Errors', getErrorArray); 31 | groups.push(errorGroup({ element, translate, moddle, commandStack })); 32 | } else if (hasEventType(element, 'bpmn:ErrorEventDefinition')) { 33 | const getErrorSelector = getSelectorForType(eventDetails); 34 | const errorGroup = getConfigureGroupForType(eventDetails, 'Error', true, getErrorSelector); 35 | const group = errorGroup({ element, translate, moddle, commandStack }); 36 | replaceGroup('error', groups, group); 37 | } 38 | return groups; 39 | }; 40 | }; 41 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 42 | } 43 | 44 | ErrorPropertiesProvider.$inject = [ 45 | 'propertiesPanel', 46 | 'translate', 47 | 'moddle', 48 | 'commandStack', 49 | ]; 50 | -------------------------------------------------------------------------------- /app/spiffworkflow/escalations/index.js: -------------------------------------------------------------------------------- 1 | import EscalationPropertiesProvider from './propertiesPanel/EscalationPropertiesProvider'; 2 | 3 | export default { 4 | __init__: ['escalationPropertiesProvider'], 5 | escalationrrorPropertiesProvider: ['type', EscalationPropertiesProvider], 6 | } 7 | -------------------------------------------------------------------------------- /app/spiffworkflow/escalations/propertiesPanel/EscalationPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { getRoot } from '../../helpers'; 3 | import { getArrayForType, getListGroupForType } from '../../eventList.js'; 4 | import { hasEventType, 5 | replaceGroup, 6 | getSelectorForType, 7 | getConfigureGroupForType 8 | } from '../../eventSelect.js'; 9 | 10 | const LOW_PRIORITY = 500; 11 | 12 | const eventDetails = { 13 | 'eventType': 'bpmn:Escalation', 14 | 'eventDefType': 'bpmn:EscalationEventDefinition', 15 | 'referenceType': 'escalationRef', 16 | 'idPrefix': 'escalation', 17 | }; 18 | 19 | export default function EscalationPropertiesProvider( 20 | propertiesPanel, 21 | translate, 22 | moddle, 23 | commandStack, 24 | ) { 25 | 26 | this.getGroups = function (element) { 27 | return function (groups) { 28 | if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) { 29 | const getEscalationArray = getArrayForType('bpmn:Escalation', 'escalationRef', 'Escalation'); 30 | const escalationGroup = getListGroupForType('escalations', 'Escalations', getEscalationArray); 31 | groups.push(escalationGroup({ element, translate, moddle, commandStack })); 32 | } else if (hasEventType(element, 'bpmn:EscalationEventDefinition')) { 33 | const getEscalationSelector = getSelectorForType(eventDetails); 34 | const escalationGroup = getConfigureGroupForType(eventDetails, 'Escalation', true, getEscalationSelector); 35 | const group = escalationGroup({ element, translate, moddle, commandStack }); 36 | replaceGroup('escalation', groups, group); 37 | } 38 | return groups; 39 | }; 40 | }; 41 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 42 | } 43 | 44 | EscalationPropertiesProvider.$inject = [ 45 | 'propertiesPanel', 46 | 'translate', 47 | 'moddle', 48 | 'commandStack', 49 | ]; 50 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/contextPad/CustomContextPadProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from "bpmn-js/lib/util/ModelUtil"; 2 | import { getScriptString, updateScript } from "../propertiesPanel/SpiffScriptGroup"; 3 | 4 | const LOW_PRIORITY = 500; 5 | 6 | export default function CustomContextPadProvider(contextPad, eventBus, commandStack, moddle) { 7 | contextPad.registerProvider(LOW_PRIORITY, this); 8 | 9 | this.getContextPadEntries = function (element) { 10 | return function (entries) { 11 | if (is(element, 'bpmn:ScriptTask')) { 12 | entries['trigger-script'] = { 13 | group: 'connect', 14 | className: 'bpmn-icon-script', 15 | title: 'Open Script Editor', 16 | action: { 17 | click: function (event, element) { 18 | triggerScript(element, 'bpmn:script', eventBus, commandStack, moddle) 19 | } 20 | } 21 | }; 22 | } else if ( 23 | hasPreAndPostScript(element) 24 | ) { 25 | const PreScript = getScriptString(element, 'spiffworkflow:PreScript'); 26 | if (PreScript && PreScript !== '') { 27 | entries['trigger-preScript'] = { 28 | group: 'connect', 29 | className: 'bpmn-icon-pre-script-trigger', 30 | title: 'Open PreScript Editor', 31 | action: { 32 | click: function (event, element) { 33 | triggerScript(element, 'spiffworkflow:PreScript', eventBus, commandStack, moddle) 34 | } 35 | } 36 | }; 37 | } 38 | 39 | const PostScript = getScriptString(element, 'spiffworkflow:PostScript'); 40 | if (PostScript && PostScript !== '') { 41 | entries['trigger-postScript'] = { 42 | group: 'connect', 43 | className: 'bpmn-icon-post-script-trigger', 44 | title: 'Open PostScript Editor', 45 | action: { 46 | click: function (event, element) { 47 | triggerScript(element, 'spiffworkflow:PostScript', eventBus, commandStack, moddle) 48 | } 49 | } 50 | }; 51 | } 52 | } 53 | 54 | return entries; 55 | }; 56 | }; 57 | } 58 | 59 | CustomContextPadProvider.$inject = [ 60 | 'contextPad', 61 | 'eventBus', 62 | 'commandStack', 63 | 'moddle' 64 | ]; 65 | 66 | function hasPreAndPostScript(element) { 67 | return is(element, 'bpmn:Task') || 68 | is(element, 'bpmn:UserTask') || 69 | is(element, 'bpmn:ServiceTask') || 70 | is(element, 'bpmn:SendTask') || 71 | is(element, 'bpmn:ReceiveTask') || 72 | is(element, 'bpmn:ManualTask') || 73 | is(element, 'bpmn:CallActivity') || 74 | is(element, 'bpmn:BusinessRuleTask') || 75 | is(element, 'bpmn:SubProcess'); 76 | } 77 | 78 | function triggerScript(element, type, eventBus, commandStack, moddle) { 79 | const script = getScriptString(element, type); 80 | eventBus.fire('spiff.script.edit', { 81 | element, 82 | scriptType: type, 83 | script, 84 | eventBus, 85 | }); 86 | eventBus.once('spiff.script.update', (event) => { 87 | updateScript( 88 | commandStack, 89 | moddle, 90 | element, 91 | event.scriptType, 92 | event.script 93 | ); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/extensionHelpers.js: -------------------------------------------------------------------------------- 1 | const SPIFF_PARENT_PROP = 'spiffworkflow:Properties'; 2 | const SPIFF_PROP = 'spiffworkflow:Property'; 3 | const PREFIX = 'spiffworkflow:'; 4 | 5 | /** 6 | * 7 | * Spiff Extensions can show up in two distinct ways. The useProperties toggles between them 8 | * 9 | * 1. They might be a top level extension, such as a buisness rule, for example: 10 | * 11 | * 12 | * my_id 13 | * 14 | * 15 | * 2. Or the extension value may exist in a name/value pair inside a Spiffworkflow Properties extension. You would 16 | * do this if you wanted to hide the values from the SpiffWorkflow enginge completely, and pass these values 17 | * through unaltered to your actual application. For Example: 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | 29 | /** 30 | * Returns the string value of the spiff extension with the given name on the provided element. "" 31 | * @param useProperties if set to true, will look inside extensions/spiffworkflow:properties otherwise, just 32 | * looks for a spiffworkflow:[name] and returns that value inside of it. 33 | * @param element 34 | * @param name 35 | */ 36 | export function getExtensionValue(businessObject, name) { 37 | 38 | const useProperties = !name.startsWith(PREFIX); 39 | let extension; 40 | if (useProperties) { 41 | extension = getExtensionProperty(businessObject, name); 42 | } else { 43 | extension = getExtension(businessObject, name); 44 | } 45 | if (extension) { 46 | return extension.value; 47 | } 48 | return ''; 49 | } 50 | 51 | export function setExtensionValue(element, name, value, moddle, commandStack, businessObject) { 52 | 53 | const useProperties = !name.startsWith(PREFIX) 54 | 55 | let businessObjectToUse = businessObject 56 | if (!businessObjectToUse) { 57 | businessObjectToUse = element.businessObject; 58 | } 59 | 60 | // Assure we have extensions 61 | let extensions = businessObjectToUse.extensionElements; 62 | if (!extensions) { 63 | extensions = moddle.create('bpmn:ExtensionElements'); 64 | } 65 | 66 | if (useProperties) { 67 | let properties = getExtension(businessObjectToUse, SPIFF_PARENT_PROP); 68 | let property = getExtensionProperty(businessObjectToUse, name); 69 | if (!properties) { 70 | properties = moddle.create(SPIFF_PARENT_PROP); 71 | extensions.get('values').push(properties); 72 | } 73 | if (!property) { 74 | property = moddle.create(SPIFF_PROP); 75 | properties.get('properties').push(property); 76 | } 77 | property.value = value; 78 | property.name = name; 79 | } else { 80 | let extension = getExtension(businessObjectToUse, name); 81 | if (!extension) { 82 | extension = moddle.create(name); 83 | extensions.get('values').push(extension) 84 | } 85 | extension.value = value; 86 | } 87 | 88 | commandStack.execute('element.updateModdleProperties', { 89 | element, 90 | moddleElement: businessObjectToUse, 91 | properties: { 92 | extensionElements: extensions, 93 | }, 94 | }); 95 | } 96 | 97 | function getExtension(businessObject, name) { 98 | if (!businessObject || !businessObject.extensionElements) { 99 | return null; 100 | } 101 | const extensionElements = businessObject.extensionElements.get('values'); 102 | return extensionElements.filter(function (extensionElement) { 103 | if (extensionElement.$instanceOf(name)) { 104 | return true; 105 | } 106 | })[0]; 107 | } 108 | 109 | 110 | function getExtensionProperty(businessObject, name) { 111 | const parentElement = getExtension(businessObject, SPIFF_PARENT_PROP); 112 | if (parentElement) { 113 | return parentElement.get('properties').filter(function (propertyElement) { 114 | return ( 115 | propertyElement.$instanceOf(SPIFF_PROP) && propertyElement.name === name 116 | ); 117 | })[0]; 118 | } 119 | return null; 120 | } 121 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/index.js: -------------------------------------------------------------------------------- 1 | import CustomContextPadProvider from './contextPad/CustomContextPadProvider'; 2 | import ExtensionsPropertiesProvider from './propertiesPanel/ExtensionsPropertiesProvider'; 3 | 4 | export default { 5 | __init__: [ 'extensionsPropertiesProvider', 'customContextPadProvider' ], 6 | extensionsPropertiesProvider: [ 'type', ExtensionsPropertiesProvider ], 7 | customContextPadProvider: [ 'type', CustomContextPadProvider ], 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionCheckboxEntry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { CheckboxEntry } from '@bpmn-io/properties-panel'; 4 | import { useService } from 'bpmn-js-properties-panel'; 5 | import { getExtensionValue, setExtensionValue } from '../extensionHelpers'; 6 | 7 | /** 8 | * A generic properties' editor for text area. 9 | */ 10 | export function SpiffExtensionCheckboxEntry(props) { 11 | const element = props.element; 12 | const commandStack = props.commandStack, 13 | moddle = props.moddle; 14 | const name = props.name, 15 | label = props.label, 16 | description = props.description; 17 | const debounce = useService('debounceInput'); 18 | 19 | const getValue = () => { 20 | return getExtensionValue(element.businessObject, name); 21 | }; 22 | 23 | const setValue = (value) => { 24 | setExtensionValue(element, name, value, moddle, commandStack); 25 | }; 26 | 27 | return ( 28 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionLaunchButton.js: -------------------------------------------------------------------------------- 1 | import { HeaderButton } from '@bpmn-io/properties-panel'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import {getExtensionValue, setExtensionValue} from '../extensionHelpers'; 4 | 5 | /** 6 | * Sends a notification to the host application saying the user 7 | * would like to edit something. Hosting application can then 8 | * update the value and send it back. 9 | */ 10 | export function SpiffExtensionLaunchButton(props) { 11 | const { element, name, event, listenEvent, listenFunction } = props; 12 | const eventBus = useService('eventBus'); 13 | return HeaderButton({ 14 | className: 'spiffworkflow-properties-panel-button', 15 | id: `launch_editor_button_${name}`, 16 | onClick: () => { 17 | const value = getExtensionValue(element.businessObject, name); 18 | eventBus.fire(event, { 19 | value, 20 | eventBus, 21 | listenEvent, 22 | }); 23 | 24 | // Listen for a response if the listenEvent is provided, and 25 | // set the value to the response 26 | // Optional additional arguments if we should listen for a reponse. 27 | if (listenEvent) { 28 | const { commandStack, moddle } = props; 29 | // Listen for a response, to update the script. 30 | eventBus.once(listenEvent, (response) => { 31 | if(listenFunction) { 32 | listenFunction(element, name, response.value, moddle, commandStack); 33 | } else { 34 | setExtensionValue(element, name, response.value, moddle, commandStack); 35 | } 36 | }); 37 | } 38 | 39 | }, 40 | children: 'Launch Editor', 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect.js: -------------------------------------------------------------------------------- 1 | import { SelectEntry } from '@bpmn-io/properties-panel'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { getExtensionValue, setExtensionValue } from '../extensionHelpers'; 4 | 5 | export const spiffExtensionOptions = {}; 6 | 7 | export const OPTION_TYPE = { 8 | json_schema_files: 'json_schema_files', 9 | dmn_files: 'dmn_files', 10 | }; 11 | 12 | /** 13 | * Allow selecting an option from a list of available options, and setting 14 | * the name and value of a SpiffWorkflow Property to the one selected in the 15 | * dropdown list. 16 | * The list of options must be provided by the containing library - by responding 17 | * to a request passed to the eventBus. 18 | * When needed, the event "spiff.${optionType}.requested" will be fired. 19 | * The response should be sent to "spiff.${optionType}.returned". The response 20 | * event should include an 'options' attribute that is list of labels and values: 21 | * [ { label: 'Product Prices DMN', value: 'Process_16xfaqc' } ] 22 | */ 23 | export function SpiffExtensionSelect(props) { 24 | const { element } = props; 25 | const { commandStack } = props; 26 | const { moddle } = props; 27 | const { label, description } = props; 28 | 29 | const { name } = props; 30 | const { optionType } = props; 31 | 32 | const debounce = useService('debounceInput'); 33 | const eventBus = useService('eventBus'); 34 | 35 | const getValue = () => { 36 | return getExtensionValue(element.businessObject, name); 37 | }; 38 | 39 | const setValue = (value) => { 40 | setExtensionValue(element, name, value, moddle, commandStack); 41 | }; 42 | 43 | const getOptions = () => { 44 | const optionList = []; 45 | optionList.push({ 46 | label: '', 47 | value: '', 48 | }); 49 | if ( 50 | optionType in spiffExtensionOptions && 51 | spiffExtensionOptions[optionType] !== null 52 | ) { 53 | spiffExtensionOptions[optionType].forEach((opt) => { 54 | optionList.push({ 55 | label: opt.label, 56 | value: opt.value, 57 | }); 58 | }); 59 | } 60 | return optionList; 61 | }; 62 | 63 | // always call this code and let the caller determine how to deal with it. 64 | // this is to avoid state loading issues with react where it doesn't clear out the variable. 65 | spiffExtensionOptions[optionType] = null; 66 | requestOptions(eventBus, element, commandStack, optionType); 67 | 68 | return SelectEntry({ 69 | id: `extension_${name}`, 70 | element, 71 | label, 72 | description, 73 | getValue, 74 | setValue, 75 | getOptions, 76 | debounce, 77 | }); 78 | } 79 | 80 | function requestOptions(eventBus, element, commandStack, optionType) { 81 | // Little backwards, but you want to assure you are ready to catch, before you throw 82 | // or you risk a race condition. 83 | eventBus.on(`spiff.${optionType}.returned`, (event) => { 84 | spiffExtensionOptions[optionType] = event.options; 85 | }); 86 | eventBus.fire(`spiff.${optionType}.requested`, { eventBus }); 87 | } 88 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { TextAreaEntry, TextFieldEntry } from '@bpmn-io/properties-panel'; 4 | import { getExtensionValue, setExtensionValue } from '../extensionHelpers'; 5 | 6 | /** 7 | * A generic properties' editor for text area. 8 | */ 9 | export function SpiffExtensionTextArea(props) { 10 | const element = props.element; 11 | const commandStack = props.commandStack, 12 | moddle = props.moddle; 13 | const name = props.name, 14 | label = props.label, 15 | description = props.description, 16 | id = props.id; 17 | const debounce = useService('debounceInput'); 18 | 19 | const getValue = () => { 20 | return getExtensionValue(element.businessObject, name); 21 | }; 22 | 23 | const setValue = (value) => { 24 | setExtensionValue(element, name, value, moddle, commandStack); 25 | }; 26 | 27 | return ( 28 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionTextInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 4 | import { getExtensionValue, setExtensionValue } from '../extensionHelpers'; 5 | 6 | /** 7 | * A generic properties' editor for text input. 8 | * Allows you to provide additional SpiffWorkflow extension properties. Just 9 | * uses whatever name is provide on the property, and adds or updates it as 10 | * needed. 11 | * 12 | 13 | 14 | 15 | 16 | 17 | 18 | * 19 | * @returns {string|null|*} 20 | */ 21 | export function SpiffExtensionTextInput(props) { 22 | const { 23 | element, 24 | commandStack, 25 | moddle, 26 | name, 27 | label, 28 | description 29 | } = props; 30 | const debounce = useService('debounceInput'); 31 | const { businessObject } = element; 32 | 33 | const getValue = () => { 34 | const value = getExtensionValue(businessObject, name); 35 | return value; 36 | }; 37 | 38 | const setValue = (value) => { 39 | setExtensionValue( 40 | element, 41 | name, 42 | value, 43 | moddle, 44 | commandStack, 45 | businessObject, 46 | ); 47 | }; 48 | 49 | return ( 50 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /app/spiffworkflow/helpers.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/5767357/6090676 2 | export function removeFirstInstanceOfItemFromArrayInPlace(arr, value) { 3 | const index = arr.indexOf(value); 4 | if (index > -1) { 5 | arr.splice(index, 1); 6 | } 7 | return arr; 8 | } 9 | 10 | export function removeExtensionElementsIfEmpty(moddleElement) { 11 | if (moddleElement.extensionElements.values.length < 1) { 12 | moddleElement.extensionElements = null; 13 | } 14 | } 15 | 16 | /** 17 | * loops up until it can find the root. 18 | * @param element 19 | */ 20 | export function getRoot(businessObject, moddle) { 21 | // HACK: get the root element. need a more formal way to do this 22 | if (moddle) { 23 | for (const elementId in moddle.ids._seed.hats) { 24 | if (elementId.startsWith('Definitions_')) { 25 | return moddle.ids._seed.hats[elementId]; 26 | } 27 | } 28 | } else { 29 | // todo: Do we want businessObject to be a shape or moddle object? 30 | if (businessObject.$type === 'bpmn:Definitions') { 31 | return businessObject; 32 | } 33 | if (typeof businessObject.$parent !== 'undefined') { 34 | return getRoot(businessObject.$parent); 35 | } 36 | } 37 | return businessObject; 38 | } 39 | 40 | 41 | export function processId(id) { 42 | let trimmedId = id.trim(); 43 | let processedId = trimmedId.replace(/\s+/g, ''); 44 | if (id !== processedId) { 45 | alert('ID should not contain spaces. It has been adjusted.'); 46 | } 47 | return processedId; 48 | } 49 | 50 | export function checkIfServiceTaskHasParameters(extensionElements) { 51 | let hasParameters = false; 52 | extensionElements.values.forEach((item) => { 53 | if ("parameterList" in item && "parameters" in item.parameterList && typeof (item.parameterList.parameters) !== "undefined") { 54 | hasParameters = true 55 | } 56 | }) 57 | return hasParameters; 58 | } 59 | -------------------------------------------------------------------------------- /app/spiffworkflow/index.js: -------------------------------------------------------------------------------- 1 | import RulesModule from 'diagram-js/lib/features/rules'; 2 | import IoPalette from './InputOutput/IoPalette'; 3 | import IoRules from './InputOutput/IoRules'; 4 | import IoInterceptor from './InputOutput/IoInterceptor'; 5 | import DataObjectInterceptor from './DataObject/DataObjectInterceptor'; 6 | import DataObjectRules from './DataObject/DataObjectRules'; 7 | import DataObjectRenderer from './DataObject/DataObjectRenderer'; 8 | import DataObjectPropertiesProvider from './DataObject/propertiesPanel/DataObjectPropertiesProvider'; 9 | import DataObjectLabelEditingProvider from './DataObject/DataObjectLabelEditingProvider'; 10 | import DataStorePropertiesProvider from './DataStoreReference/propertiesPanel/DataStorePropertiesProvider'; 11 | import DataStoreInterceptor from './DataStoreReference/DataStoreInterceptor'; 12 | import ConditionsPropertiesProvider from './conditions/propertiesPanel/ConditionsPropertiesProvider'; 13 | import ExtensionsPropertiesProvider from './extensions/propertiesPanel/ExtensionsPropertiesProvider'; 14 | import MessagesPropertiesProvider from './messages/propertiesPanel/MessagesPropertiesProvider'; 15 | import SignalPropertiesProvider from './signals/propertiesPanel/SignalPropertiesProvider'; 16 | import ErrorPropertiesProvider from './errors/propertiesPanel/ErrorPropertiesProvider'; 17 | import EscalationPropertiesProvider from './escalations/propertiesPanel/EscalationPropertiesProvider'; 18 | import CallActivityPropertiesProvider from './callActivity/propertiesPanel/CallActivityPropertiesProvider'; 19 | import IoPropertiesProvider from './InputOutput/propertiesProvider/IoPropertiesProvider'; 20 | import StandardLoopPropertiesProvider from './loops/StandardLoopPropertiesProvider'; 21 | import MultiInstancePropertiesProvider from './loops/MultiInstancePropertiesProvider'; 22 | import CallActivityInterceptor from './callActivity/CallActivityInterceptor'; 23 | import MessageInterceptor from './messages/MessageInterceptor'; 24 | import CustomContextPadProvider from './extensions/contextPad/CustomContextPadProvider'; 25 | 26 | export default { 27 | __depends__: [RulesModule], 28 | __init__: [ 29 | 'dataObjectInterceptor', 30 | 'dataObjectRules', 31 | 'dataObjectPropertiesProvider', 32 | 'dataObjectLabelEditingProvider', 33 | 'dataStoreInterceptor', 34 | 'dataStorePropertiesProvider', 35 | 'conditionsPropertiesProvider', 36 | 'extensionsPropertiesProvider', 37 | 'customContextPadProvider', 38 | 'messagesPropertiesProvider', 39 | 'messageInterceptor', 40 | 'signalPropertiesProvider', 41 | 'errorPropertiesProvider', 42 | 'escalationPropertiesProvider', 43 | 'callActivityPropertiesProvider', 44 | 'ioPalette', 45 | 'ioRules', 46 | 'ioInterceptor', 47 | 'dataObjectRenderer', 48 | 'multiInstancePropertiesProvider', 49 | 'standardLoopPropertiesProvider', 50 | 'IoPropertiesProvider', 51 | 'callActivityInterceptor' 52 | ], 53 | dataObjectInterceptor: ['type', DataObjectInterceptor], 54 | dataObjectRules: ['type', DataObjectRules], 55 | dataObjectRenderer: ['type', DataObjectRenderer], 56 | dataObjectPropertiesProvider: ['type', DataObjectPropertiesProvider], 57 | dataObjectLabelEditingProvider: ['type', DataObjectLabelEditingProvider], 58 | dataStoreInterceptor: ['type', DataStoreInterceptor], 59 | dataStorePropertiesProvider: ['type', DataStorePropertiesProvider], 60 | conditionsPropertiesProvider: ['type', ConditionsPropertiesProvider], 61 | extensionsPropertiesProvider: ['type', ExtensionsPropertiesProvider], 62 | customContextPadProvider: ['type', CustomContextPadProvider], 63 | signalPropertiesProvider: ['type', SignalPropertiesProvider], 64 | errorPropertiesProvider: ['type', ErrorPropertiesProvider], 65 | escalationPropertiesProvider: ['type', EscalationPropertiesProvider], 66 | messagesPropertiesProvider: ['type', MessagesPropertiesProvider], 67 | messageInterceptor: ['type', MessageInterceptor], 68 | callActivityPropertiesProvider: ['type', CallActivityPropertiesProvider], 69 | ioPalette: ['type', IoPalette], 70 | ioRules: ['type', IoRules], 71 | ioInterceptor: ['type', IoInterceptor], 72 | multiInstancePropertiesProvider: ['type', MultiInstancePropertiesProvider], 73 | standardLoopPropertiesProvider: ['type', StandardLoopPropertiesProvider], 74 | IoPropertiesProvider: ['type', IoPropertiesProvider], 75 | callActivityInterceptor: [ 'type', CallActivityInterceptor ] 76 | }; 77 | -------------------------------------------------------------------------------- /app/spiffworkflow/loops/MultiInstancePropertiesProvider.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable no-param-reassign */ 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 5 | import { isTextFieldEntryEdited, isCheckboxEntryEdited } from '@bpmn-io/properties-panel'; 6 | import { InputItem } from './propertiesPanel/InputItemEntry'; 7 | import { LoopCardinality } from './propertiesPanel/LoopCardinalityEntry'; 8 | import { InputCollection } from './propertiesPanel/InputCollectionEntry'; 9 | import { OutputItem } from './propertiesPanel/OutputItemEntry'; 10 | import { OutputCollection } from './propertiesPanel/OutputCollectionEntry'; 11 | import { CompletionCondition } from './propertiesPanel/CompletionConditionEntry'; 12 | import { IsOutputElSync } from './propertiesPanel/IsIOSyncEntry'; 13 | 14 | const LOW_PRIORITY = 500; 15 | 16 | export default function MultiInstancePropertiesProvider(propertiesPanel) { 17 | this.getGroups = function getGroupsCallback(element) { 18 | return function pushGroup(groups) { 19 | if ( 20 | is(element, 'bpmn:Task') || 21 | is(element, 'bpmn:CallActivity') || 22 | is(element, 'bpmn:SubProcess') 23 | ) { 24 | const group = groups.filter((g) => g.id === 'multiInstance'); 25 | if (group.length === 1) updateMultiInstanceGroup(element, group[0]); 26 | } 27 | return groups; 28 | }; 29 | }; 30 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 31 | } 32 | 33 | MultiInstancePropertiesProvider.$inject = ['propertiesPanel']; 34 | 35 | function updateMultiInstanceGroup(element, group) { 36 | group.entries = MultiInstanceProps({ element }); 37 | group.shouldOpen = true; 38 | } 39 | 40 | function MultiInstanceProps(props) { 41 | const { element } = props; 42 | const { businessObject } = element; 43 | 44 | return [ 45 | { 46 | id: 'loopCardinality', 47 | component: LoopCardinality, 48 | isEdited: isTextFieldEntryEdited, 49 | }, 50 | { 51 | id: 'loopDataInputRef', 52 | component: InputCollection, 53 | isEdited: isTextFieldEntryEdited, 54 | }, 55 | { 56 | id: 'dataInputItem', 57 | component: InputItem, 58 | isEdited: isTextFieldEntryEdited, 59 | }, 60 | { 61 | id: 'isOutputElSynchronized', 62 | component: IsOutputElSync, 63 | isEdited: isCheckboxEntryEdited, 64 | }, 65 | { 66 | id: 'loopDataOutputRef', 67 | component: OutputCollection, 68 | isEdited: isTextFieldEntryEdited, 69 | }, 70 | !businessObject.get('spiffworkflow:isOutputSynced') 71 | ? { 72 | id: 'dataOutputItem', 73 | component: OutputItem, 74 | isEdited: isTextFieldEntryEdited, 75 | } 76 | : {}, 77 | { 78 | id: 'completionCondition', 79 | component: CompletionCondition, 80 | isEdited: isTextFieldEntryEdited, 81 | }, 82 | ]; 83 | } 84 | -------------------------------------------------------------------------------- /app/spiffworkflow/loops/StandardLoopPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { 4 | Group, 5 | TextFieldEntry, 6 | isTextFieldEntryEdited, 7 | CheckboxEntry, 8 | isCheckboxEntryEdited, 9 | } from '@bpmn-io/properties-panel'; 10 | 11 | import { getLoopProperty, setLoopProperty } from './helpers'; 12 | 13 | const LOW_PRIORITY = 500; 14 | 15 | export default function StandardLoopPropertiesProvider(propertiesPanel) { 16 | this.getGroups = function getGroupsCallback(element) { 17 | return function pushGroup(groups) { 18 | if ( 19 | (is(element, 'bpmn:Task') || is(element, 'bpmn:CallActivity') || is(element, 'bpmn:SubProcess')) && 20 | typeof(element.businessObject.loopCharacteristics) !== 'undefined' && 21 | element.businessObject.loopCharacteristics.$type === 'bpmn:StandardLoopCharacteristics' 22 | ) { 23 | const group = { 24 | id: 'standardLoopCharacteristics', 25 | component: Group, 26 | label: 'Standard Loop', 27 | entries: StandardLoopProps(element), 28 | shouldOpen: true, 29 | }; 30 | if (groups.length < 3) 31 | groups.push(group); 32 | else 33 | groups.splice(2, 0, group); 34 | } 35 | return groups; 36 | }; 37 | }; 38 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 39 | } 40 | 41 | StandardLoopPropertiesProvider.$inject = ['propertiesPanel']; 42 | 43 | function StandardLoopProps(props) { 44 | const { element } = props; 45 | return [{ 46 | id: 'loopMaximum', 47 | component: LoopMaximum, 48 | isEdited: isTextFieldEntryEdited 49 | }, { 50 | id: 'loopCondition', 51 | component: LoopCondition, 52 | isEdited: isTextFieldEntryEdited 53 | }, { 54 | id: 'testBefore', 55 | component: TestBefore, 56 | isEdited: isCheckboxEntryEdited 57 | }]; 58 | } 59 | 60 | function LoopMaximum(props) { 61 | const { element } = props; 62 | const debounce = useService('debounceInput'); 63 | const translate = useService('translate'); 64 | const commandStack = useService('commandStack'); 65 | const bpmnFactory = useService('bpmnFactory'); 66 | 67 | const getValue = () => { 68 | return getLoopProperty(element, 'loopMaximum'); 69 | }; 70 | 71 | const setValue = value => { 72 | setLoopProperty(element, 'loopMaximum', value, commandStack); 73 | }; 74 | 75 | return TextFieldEntry({ 76 | element, 77 | id: 'loopMaximum', 78 | label: translate('Loop Maximum'), 79 | getValue, 80 | setValue, 81 | debounce 82 | }); 83 | } 84 | 85 | function TestBefore(props) { 86 | const { element } = props; 87 | const debounce = useService('debounceInput'); 88 | const translate = useService('translate'); 89 | const commandStack = useService('commandStack'); 90 | const bpmnFactory = useService('bpmnFactory'); 91 | 92 | const getValue = () => { 93 | return getLoopProperty(element, 'testBefore'); 94 | }; 95 | 96 | const setValue = value => { 97 | setLoopProperty(element, 'testBefore', value, commandStack); 98 | }; 99 | 100 | return CheckboxEntry({ 101 | element, 102 | id: 'testBefore', 103 | label: translate('Test Before'), 104 | getValue, 105 | setValue, 106 | }); 107 | } 108 | 109 | function LoopCondition(props) { 110 | const { element } = props; 111 | const debounce = useService('debounceInput'); 112 | const translate = useService('translate'); 113 | const commandStack = useService('commandStack'); 114 | const bpmnFactory = useService('bpmnFactory'); 115 | 116 | const getValue = () => { 117 | return getLoopProperty(element, 'loopCondition'); 118 | }; 119 | 120 | const setValue = value => { 121 | const loopCondition = bpmnFactory.create('bpmn:FormalExpression', {body: value}) 122 | setLoopProperty(element, 'loopCondition', loopCondition, commandStack); 123 | }; 124 | 125 | return TextFieldEntry({ 126 | element, 127 | id: 'loopCondition', 128 | label: translate('Loop Condition'), 129 | getValue, 130 | setValue, 131 | debounce 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /app/spiffworkflow/loops/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable no-param-reassign */ 3 | 4 | export function getLoopProperty(element, propertyName) { 5 | const { loopCharacteristics } = element.businessObject; 6 | const prop = loopCharacteristics.get(propertyName); 7 | let value = ''; 8 | if (typeof (prop) !== 'object') { 9 | value = prop; 10 | } else if (typeof (prop) !== 'undefined') { 11 | if (prop.$type === 'bpmn:FormalExpression') 12 | value = prop.get('body'); 13 | else 14 | value = prop.get('id'); 15 | } 16 | return value; 17 | } 18 | 19 | export function setLoopProperty(element, propertyName, value, commandStack) { 20 | const { loopCharacteristics } = element.businessObject; 21 | 22 | if (typeof (value) === 'object') { 23 | value.$parent = loopCharacteristics; 24 | } 25 | 26 | const properties = { [propertyName]: value }; 27 | if (propertyName === 'loopCardinality') properties.loopDataInputRef = undefined; 28 | if (propertyName === 'loopDataInputRef') properties.loopCardinality = undefined; 29 | 30 | commandStack.execute('element.updateModdleProperties', { 31 | element, 32 | moddleElement: loopCharacteristics, 33 | properties, 34 | }); 35 | } 36 | 37 | export function removeLoopProperty(element, propertyName, commandStack) { 38 | const { loopCharacteristics } = element.businessObject; 39 | const properties = { [propertyName]: undefined }; 40 | commandStack.execute('element.updateModdleProperties', { 41 | element, 42 | moddleElement: loopCharacteristics, 43 | properties, 44 | }); 45 | } 46 | 47 | export function setIsIOValue(element, value, commandStack) { 48 | commandStack.execute('element.updateProperties', { 49 | element, 50 | properties: { 51 | 'spiffworkflow:isOutputSynced': value, 52 | }, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /app/spiffworkflow/loops/propertiesPanel/CompletionConditionEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/order */ 4 | 5 | import { useService } from "bpmn-js-properties-panel"; 6 | import { getLoopProperty, removeLoopProperty, setLoopProperty } from "../helpers"; 7 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 8 | 9 | export function CompletionCondition(props) { 10 | const { element } = props; 11 | const debounce = useService('debounceInput'); 12 | const translate = useService('translate'); 13 | const commandStack = useService('commandStack'); 14 | const bpmnFactory = useService('bpmnFactory'); 15 | 16 | const getValue = () => { 17 | return getLoopProperty(element, 'completionCondition'); 18 | }; 19 | 20 | const setValue = (value) => { 21 | if (!value || value === '') { 22 | // If value is empty, remove completionCondition from XML 23 | removeLoopProperty(element, 'completionCondition', commandStack); 24 | return; 25 | } 26 | const completionCondition = bpmnFactory.create('bpmn:FormalExpression', { 27 | body: value, 28 | }); 29 | setLoopProperty( 30 | element, 31 | 'completionCondition', 32 | completionCondition, 33 | commandStack 34 | ); 35 | }; 36 | 37 | return TextFieldEntry({ 38 | element, 39 | id: 'completionCondition', 40 | label: translate('Completion Condition'), 41 | getValue, 42 | setValue, 43 | debounce, 44 | description: 'Stop executing this task when this condition is met', 45 | }); 46 | } -------------------------------------------------------------------------------- /app/spiffworkflow/loops/propertiesPanel/InputCollectionEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/order */ 4 | 5 | import { useService } from "bpmn-js-properties-panel"; 6 | import { getLoopProperty, removeLoopProperty, setLoopProperty } from "../helpers"; 7 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 8 | 9 | export function InputCollection(props) { 10 | const { element } = props; 11 | const debounce = useService('debounceInput'); 12 | const translate = useService('translate'); 13 | const commandStack = useService('commandStack'); 14 | const bpmnFactory = useService('bpmnFactory'); 15 | 16 | const getValue = () => { 17 | return getLoopProperty(element, 'loopDataInputRef'); 18 | }; 19 | 20 | const setValue = (value) => { 21 | if (!value || value === '') { 22 | // If value is empty or undefined, remove loopDataInputRef from XML 23 | removeLoopProperty(element, 'loopDataInputRef', commandStack); 24 | return; 25 | } 26 | const collection = bpmnFactory.create('bpmn:ItemAwareElement', { 27 | id: value, 28 | }); 29 | setLoopProperty(element, 'loopDataInputRef', collection, commandStack); 30 | }; 31 | 32 | return TextFieldEntry({ 33 | element, 34 | id: 'loopDataInputRef', 35 | label: translate('Input Collection'), 36 | getValue, 37 | setValue, 38 | debounce, 39 | description: 'Create an instance for each item in this collection', 40 | }); 41 | } -------------------------------------------------------------------------------- /app/spiffworkflow/loops/propertiesPanel/InputItemEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/order */ 4 | 5 | import { useService } from "bpmn-js-properties-panel"; 6 | import { getLoopProperty, setLoopProperty } from "../helpers"; 7 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 8 | 9 | export function InputItem(props) { 10 | const { element } = props; 11 | const debounce = useService('debounceInput'); 12 | const translate = useService('translate'); 13 | const commandStack = useService('commandStack'); 14 | const bpmnFactory = useService('bpmnFactory'); 15 | 16 | const getValue = () => { 17 | return getLoopProperty(element, 'inputDataItem'); 18 | }; 19 | 20 | const setValue = (value) => { 21 | const item = 22 | typeof value !== 'undefined' && value !== '' 23 | ? bpmnFactory.create('bpmn:DataInput', { id: value, name: value }) 24 | : undefined; 25 | setLoopProperty(element, 'inputDataItem', item, commandStack); 26 | 27 | try { 28 | const { businessObject } = element; 29 | if (businessObject.get('spiffworkflow:isOutputSynced')) { 30 | setLoopProperty(element, 'outputDataItem', item, commandStack); 31 | } 32 | } catch (error) { 33 | console.log('Error caught while set value Input item', error); 34 | } 35 | }; 36 | 37 | return TextFieldEntry({ 38 | element, 39 | id: 'inputDataItem', 40 | label: translate('Input Element'), 41 | getValue, 42 | setValue, 43 | debounce, 44 | description: 'Each item in the collection will be copied to this variable', 45 | }); 46 | } -------------------------------------------------------------------------------- /app/spiffworkflow/loops/propertiesPanel/IsIOSyncEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/order */ 4 | 5 | import { useService } from "bpmn-js-properties-panel"; 6 | import { getLoopProperty, removeLoopProperty, setIsIOValue, setLoopProperty } from "../helpers"; 7 | import { CheckboxEntry } from '@bpmn-io/properties-panel'; 8 | 9 | export function IsOutputElSync(props) { 10 | const { element } = props; 11 | const translate = useService('translate'); 12 | const commandStack = useService('commandStack'); 13 | const bpmnFactory = useService('bpmnFactory'); 14 | 15 | const getValue = () => { 16 | const { businessObject } = element; 17 | return businessObject.get('spiffworkflow:isOutputSynced') 18 | ? businessObject.get('spiffworkflow:isOutputSynced') 19 | : false; 20 | }; 21 | 22 | const setValue = (value) => { 23 | if (value) { 24 | const valIn = getLoopProperty(element, 'inputDataItem'); 25 | const item = 26 | typeof valIn !== 'undefined' && valIn !== '' 27 | ? bpmnFactory.create('bpmn:DataOutput', { id: valIn, name: valIn }) 28 | : undefined; 29 | if(item){ 30 | // If DataInput Item is found and set, add new DataOut with same value 31 | setLoopProperty(element, 'outputDataItem', item, commandStack); 32 | } 33 | } else { 34 | // Remove DataOutput value when isIoSync is disabled 35 | removeLoopProperty(element, 'outputDataItem', commandStack); 36 | } 37 | 38 | setIsIOValue(element, value, commandStack); 39 | }; 40 | 41 | return CheckboxEntry({ 42 | element, 43 | id: 'testBefore', 44 | label: translate('Output Element is Synchronized with Input Element'), 45 | getValue, 46 | setValue, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /app/spiffworkflow/loops/propertiesPanel/LoopCardinalityEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/order */ 4 | 5 | import { useService } from "bpmn-js-properties-panel"; 6 | import { getLoopProperty, removeLoopProperty, setLoopProperty } from "../helpers"; 7 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 8 | 9 | export function LoopCardinality(props) { 10 | const { element } = props; 11 | const debounce = useService('debounceInput'); 12 | const translate = useService('translate'); 13 | const commandStack = useService('commandStack'); 14 | const bpmnFactory = useService('bpmnFactory'); 15 | 16 | const getValue = () => { 17 | return getLoopProperty(element, 'loopCardinality'); 18 | }; 19 | 20 | const setValue = (value) => { 21 | if (!value || value === '') { 22 | // If value is empty or undefined, remove loopCardinality from XML 23 | removeLoopProperty(element, 'loopCardinality', commandStack); 24 | return; 25 | } 26 | const loopCardinality = bpmnFactory.create('bpmn:FormalExpression', { 27 | body: value, 28 | }); 29 | setLoopProperty(element, 'loopCardinality', loopCardinality, commandStack); 30 | }; 31 | 32 | return TextFieldEntry({ 33 | element, 34 | id: 'loopCardinality', 35 | label: translate('Loop Cardinality'), 36 | getValue, 37 | setValue, 38 | debounce, 39 | description: 'Explicitly set the number of instances', 40 | }); 41 | } -------------------------------------------------------------------------------- /app/spiffworkflow/loops/propertiesPanel/OutputCollectionEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/order */ 4 | 5 | import { useService } from "bpmn-js-properties-panel"; 6 | import { getLoopProperty, removeLoopProperty, setLoopProperty } from "../helpers"; 7 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 8 | 9 | export function OutputCollection(props) { 10 | const { element } = props; 11 | const debounce = useService('debounceInput'); 12 | const translate = useService('translate'); 13 | const commandStack = useService('commandStack'); 14 | const bpmnFactory = useService('bpmnFactory'); 15 | 16 | const getValue = () => { 17 | return getLoopProperty(element, 'loopDataOutputRef'); 18 | }; 19 | 20 | const setValue = (value) => { 21 | if (!value || value === '') { 22 | // If value is empty or undefined, remove loopDataOutputRef from XML 23 | removeLoopProperty(element, 'loopDataOutputRef', commandStack); 24 | return; 25 | } 26 | const collection = bpmnFactory.create('bpmn:ItemAwareElement', { 27 | id: value, 28 | }); 29 | setLoopProperty(element, 'loopDataOutputRef', collection, commandStack); 30 | }; 31 | 32 | return TextFieldEntry({ 33 | element, 34 | id: 'loopDataOutputRef', 35 | label: translate('Output Collection'), 36 | getValue, 37 | setValue, 38 | debounce, 39 | description: 'Create or update this collection with the instance results', 40 | }); 41 | } -------------------------------------------------------------------------------- /app/spiffworkflow/loops/propertiesPanel/OutputItemEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/order */ 4 | import { useService } from "bpmn-js-properties-panel"; 5 | import { getLoopProperty, setLoopProperty } from "../helpers"; 6 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 7 | 8 | 9 | export function OutputItem(props) { 10 | const { element } = props; 11 | const debounce = useService('debounceInput'); 12 | const translate = useService('translate'); 13 | const commandStack = useService('commandStack'); 14 | const bpmnFactory = useService('bpmnFactory'); 15 | 16 | const getValue = () => { 17 | return getLoopProperty(element, 'outputDataItem'); 18 | }; 19 | 20 | const setValue = (value) => { 21 | try { 22 | const inVal = getLoopProperty(element, 'inputDataItem'); 23 | if (inVal === value) { 24 | alert( 'You have entered the same value for both Input and Output elements without enabling synchronization. Please confirm if this is intended.' ); 25 | return; 26 | } 27 | } catch (error) { 28 | console.log('Error caught while Set Value OutputItem', error); 29 | } 30 | 31 | const item = 32 | typeof value !== 'undefined' && value !== '' 33 | ? bpmnFactory.create('bpmn:DataOutput', { id: value, name: value }) 34 | : undefined; 35 | setLoopProperty(element, 'outputDataItem', item, commandStack); 36 | }; 37 | 38 | return TextFieldEntry({ 39 | element, 40 | id: 'outputDataItem', 41 | label: translate('Output Element'), 42 | getValue, 43 | setValue, 44 | debounce, 45 | description: 46 | 'The value of this variable will be added to the output collection', 47 | }); 48 | } -------------------------------------------------------------------------------- /app/spiffworkflow/messages/MessageInterceptor.js: -------------------------------------------------------------------------------- 1 | import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; 2 | import { isMessageElement, isMessageRefUsed, setParentCorrelationKeys, syncCorrelationProperties, getRoot} from './MessageHelpers'; 3 | 4 | const HIGH_PRIORITY = 90500; 5 | 6 | export default class MessageInterceptor extends CommandInterceptor { 7 | 8 | constructor(eventBus, bpmnFactory, commandStack, bpmnUpdater, moddle) { 9 | super(eventBus); 10 | 11 | this.postExecuted(['shape.delete'], HIGH_PRIORITY, function (event) { 12 | const { context } = event; 13 | const { shape, rootElement } = context; 14 | const { businessObject } = shape; 15 | 16 | if (isMessageElement(shape)) { 17 | 18 | let oldMessageRef = (businessObject.eventDefinitions) ? businessObject.eventDefinitions[0].messageRef : businessObject.messageRef; 19 | 20 | let definitions = getRoot(rootElement, moddle) 21 | if (!definitions.get('rootElements')) { 22 | definitions.set('rootElements', []); 23 | } 24 | 25 | if (oldMessageRef) { 26 | // Remove previous message in case it's not used anymore 27 | const isOldMessageUsed = isMessageRefUsed(definitions, oldMessageRef.id); 28 | if (!isOldMessageUsed) { 29 | const rootElements = definitions.get('rootElements'); 30 | const oldMessageIndex = rootElements.findIndex( 31 | (element) => 32 | element.$type === 'bpmn:Message' && element.id === oldMessageRef.id, 33 | ); 34 | if (oldMessageIndex !== -1) { 35 | rootElements.splice(oldMessageIndex, 1); 36 | definitions.rootElements = rootElements; 37 | } 38 | } 39 | 40 | // Automatic deletion of previous message correlation properties 41 | syncCorrelationProperties(shape, definitions, moddle); 42 | } 43 | 44 | // Update Correlation key if Process has collaboration 45 | try { 46 | setParentCorrelationKeys(definitions, bpmnFactory, shape, moddle); 47 | } catch (error) { 48 | console.error('Error Caught while synchronizing Correlation key', error); 49 | } 50 | 51 | } 52 | }); 53 | 54 | } 55 | } 56 | 57 | MessageInterceptor.$inject = ['eventBus', 'bpmnFactory', 'commandStack', 'bpmnUpdater', 'moddle']; 58 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/index.js: -------------------------------------------------------------------------------- 1 | import MessageInterceptor from './MessageInterceptor'; 2 | import MessagesPropertiesProvider from './propertiesPanel/MessagesPropertiesProvider'; 3 | 4 | export default { 5 | __init__: ['messagesPropertiesProvider', 'messageInterceptor'], 6 | messagesPropertiesProvider: ['type', MessagesPropertiesProvider], 7 | messageInterceptor: [ 'type', MessageInterceptor ], 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/MessagesPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { isMessageElement } from '../MessageHelpers'; 2 | 3 | import { createMessageGroup } from './elementLevelProvider/TaskEventMessageProvider'; 4 | 5 | const LOW_PRIORITY = 500; 6 | 7 | export default function MessagesPropertiesProvider( 8 | propertiesPanel, 9 | translate, 10 | moddle, 11 | commandStack, 12 | elementRegistry 13 | ) { 14 | this.getGroups = function getGroupsCallback(element) { 15 | return function pushGroup(groups) { 16 | if (isMessageElement(element)) { 17 | const messageIndex = findEntry(groups, 'message'); 18 | if (messageIndex) { 19 | groups.splice(messageIndex, 1); 20 | } 21 | groups.push( 22 | ...createMessageGroup( 23 | element, 24 | translate, 25 | moddle, 26 | commandStack, 27 | elementRegistry 28 | ) 29 | ); 30 | } 31 | return groups; 32 | }; 33 | }; 34 | 35 | function findEntry(entries, entryId) { 36 | let entryIndex = null; 37 | entries.forEach(function (value, index) { 38 | if (value.id === entryId) { 39 | entryIndex = index; 40 | } 41 | }); 42 | return entryIndex; 43 | } 44 | 45 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 46 | } 47 | 48 | MessagesPropertiesProvider.$inject = [ 49 | 'propertiesPanel', 50 | 'translate', 51 | 'moddle', 52 | 'commandStack', 53 | 'elementRegistry', 54 | ]; 55 | 56 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/elementLevelProvider/CorrelationCheckbox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { CheckboxEntry } from '@bpmn-io/properties-panel'; 4 | 5 | /** 6 | * A generic properties' editor for text area. 7 | */ 8 | export function CorrelationCheckboxEntry(props) { 9 | const { element, commandStack, moddle } = props; 10 | const { name, label, description } = props; 11 | const { businessObject } = element; 12 | 13 | const debounce = useService('debounceInput'); 14 | 15 | const getValue = () => { 16 | const value = businessObject.get('isCorrelated') 17 | ? businessObject.get('isCorrelated') 18 | : false; 19 | return value; 20 | }; 21 | 22 | const setValue = (value) => { 23 | commandStack.execute('element.updateProperties', { 24 | element, 25 | properties: { 26 | isCorrelated: value, 27 | }, 28 | }); 29 | }; 30 | 31 | return ( 32 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/elementLevelProvider/MatchingCorrelationCheckbox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { CheckboxEntry } from '@bpmn-io/properties-panel'; 4 | 5 | /** 6 | * A generic properties' editor for text area. 7 | */ 8 | export function MatchingCorrelationCheckboxEntry(props) { 9 | const { element, commandStack, moddle } = props; 10 | const { name, label, description } = props; 11 | const { businessObject } = element; 12 | 13 | const debounce = useService('debounceInput'); 14 | 15 | const getValue = () => { 16 | const isMatchingCorrelation = businessObject.get('spiffworkflow:isMatchingCorrelation'); 17 | const value = isMatchingCorrelation && isMatchingCorrelation !== 'false' 18 | ? true 19 | : false; 20 | return value; 21 | }; 22 | 23 | const setValue = (value) => { 24 | commandStack.execute('element.updateProperties', { 25 | element, 26 | properties: { 27 | 'spiffworkflow:isMatchingCorrelation': value, 28 | }, 29 | }); 30 | }; 31 | 32 | return ( 33 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/elementLevelProvider/MessageLaunchEditorButton.js: -------------------------------------------------------------------------------- 1 | import { HeaderButton } from '@bpmn-io/properties-panel'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { 4 | findCorrelationPropertiesAndRetrievalExpressionsForMessage, 5 | getMessageRefElement, 6 | } from '../../MessageHelpers'; 7 | 8 | /** 9 | * Sends a notification to the host application saying the user 10 | * would like to edit something. Hosting application can then 11 | * update the value and send it back. 12 | */ 13 | export function MessageLaunchEditorButton(props) { 14 | const { element, moddle } = props; 15 | 16 | const sendEvent = 'spiff.message.edit'; 17 | const listenEvent = 'spiff.message.update'; 18 | 19 | const eventBus = useService('eventBus'); 20 | 21 | const messageRef = getMessageRefElement(element); 22 | const correlationProperties = 23 | findCorrelationPropertiesAndRetrievalExpressionsForMessage(element); 24 | const parsedCorrelationProperties = correlationProperties.map((item) => ({ 25 | id: item.correlationPropertyModdleElement.id, 26 | retrievalExpression: 27 | item.correlationPropertyRetrievalExpressionModdleElement.messagePath.body, 28 | })); 29 | 30 | let messageId = null; 31 | if (messageRef && messageRef.id) { 32 | messageId = messageRef.id; 33 | } 34 | if (messageRef && messageRef.name && messageRef.name !== messageId) { 35 | messageId = messageRef.name; 36 | } 37 | return HeaderButton({ 38 | className: 'spiffworkflow-properties-panel-button', 39 | id: `message_launch_message_editor_button`, 40 | onClick: () => { 41 | 42 | // eventBus.fire('spiff.add_message.requested', { eventBus }); 43 | 44 | eventBus.fire(sendEvent, { 45 | value: { 46 | elementId: element.id, 47 | messageId, 48 | correlation_properties: parsedCorrelationProperties, 49 | }, 50 | eventBus, 51 | listenEvent, 52 | }); 53 | 54 | // Listen for a response, to update the script. 55 | eventBus.once(listenEvent, (response) => { 56 | messageRef.id = response.value; 57 | }); 58 | }, 59 | children: 'Open message editor', 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/elementLevelProvider/MessagePayload.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { TextAreaEntry } from '@bpmn-io/properties-panel'; 4 | import { 5 | getMessageElementForShapeElement, 6 | isMessageEvent, 7 | } from '../../MessageHelpers'; 8 | 9 | /** 10 | * Allows the creation, or editing of messagePayload at the bpmn:sendTask level of a BPMN document. 11 | */ 12 | export function MessagePayload(props) { 13 | const shapeElement = props.element; 14 | const { moddle, element, commandStack } = props; 15 | const debounce = useService('debounceInput'); 16 | const messageElement = getMessageElementForShapeElement(shapeElement); 17 | const disabled = !messageElement; 18 | 19 | const getMessagePayloadObject = () => { 20 | if (element) { 21 | const { extensionElements } = isMessageEvent(element) 22 | ? element.businessObject.eventDefinitions[0] 23 | : element.businessObject; 24 | if (extensionElements && extensionElements.get('values')) { 25 | let payloadResp = extensionElements 26 | .get('values') 27 | .filter(function getInstanceOfType(e) { 28 | return e.$instanceOf('spiffworkflow:MessagePayload'); 29 | })[0]; 30 | return payloadResp; 31 | } 32 | } 33 | return null; 34 | }; 35 | 36 | const getValue = () => { 37 | const messagePayloadObject = getMessagePayloadObject(); 38 | if (messagePayloadObject) { 39 | return messagePayloadObject.value; 40 | } else { 41 | // Check : for old models where payload exists on message level 42 | const bo = isMessageEvent(element) 43 | ? element.businessObject.eventDefinitions[0] 44 | : element.businessObject; 45 | 46 | const { messageRef } = bo; 47 | if (messageRef) { 48 | const { extensionElements } = messageRef; 49 | const payloadResp = (extensionElements) ? extensionElements 50 | .get('values') 51 | .filter(function getInstanceOfType(e) { 52 | return e.$instanceOf('spiffworkflow:MessagePayload'); 53 | })[0] : undefined; 54 | 55 | if (payloadResp) { 56 | setValue(payloadResp.value); 57 | return payloadResp.value; 58 | } 59 | } 60 | } 61 | return ''; 62 | }; 63 | 64 | const setValue = (value) => { 65 | 66 | var extensions = isMessageEvent(element) 67 | ? element.businessObject.eventDefinitions[0].get('extensionElements') || 68 | moddle.create('bpmn:ExtensionElements') 69 | : element.businessObject.get('extensionElements') || 70 | moddle.create('bpmn:ExtensionElements'); 71 | 72 | let messagePayloadObject = getMessagePayloadObject(); 73 | if (!messagePayloadObject) { 74 | messagePayloadObject = moddle.create('spiffworkflow:MessagePayload'); 75 | extensions.get('values').push(messagePayloadObject); 76 | } 77 | messagePayloadObject.value = value; 78 | 79 | isMessageEvent(element) 80 | ? element.businessObject.eventDefinitions[0].set( 81 | 'extensionElements', 82 | extensions 83 | ) 84 | : element.businessObject.set('extensionElements', extensions); 85 | 86 | commandStack.execute('element.updateProperties', { 87 | element, 88 | properties: {}, 89 | }); 90 | }; 91 | 92 | return ( 93 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/elementLevelProvider/MessageVariable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { TextFieldEntry } from '@bpmn-io/properties-panel'; 4 | import { 5 | getMessageElementForShapeElement, 6 | isMessageEvent, 7 | } from '../../MessageHelpers'; 8 | 9 | /** 10 | * Allows the creation, or editing of messageVariable at the bpmn:sendTask level of a BPMN document. 11 | */ 12 | export function MessageVariable(props) { 13 | const shapeElement = props.element; 14 | const { moddle, element, commandStack } = props; 15 | const debounce = useService('debounceInput'); 16 | const messageElement = getMessageElementForShapeElement(shapeElement); 17 | const disabled = !messageElement; 18 | 19 | const getMessageVariableObject = () => { 20 | if (element) { 21 | const { extensionElements } = isMessageEvent(element) 22 | ? element.businessObject.eventDefinitions[0] 23 | : element.businessObject; 24 | if (extensionElements && extensionElements.get('values')) { 25 | let variableResp = extensionElements 26 | .get('values') 27 | .filter(function getInstanceOfType(e) { 28 | return e.$instanceOf('spiffworkflow:MessageVariable'); 29 | })[0]; 30 | return variableResp; 31 | } 32 | } 33 | return null; 34 | }; 35 | 36 | const getValue = () => { 37 | const messageVariableObject = getMessageVariableObject(); 38 | if (messageVariableObject) { 39 | return messageVariableObject.value; 40 | } 41 | 42 | // Check : for old models where messageVariable exists on message level 43 | const bo = isMessageEvent(element) 44 | ? element.businessObject.eventDefinitions[0] 45 | : element.businessObject; 46 | 47 | const { messageRef } = bo; 48 | if (messageRef) { 49 | const { extensionElements } = messageRef; 50 | const messageResp = extensionElements 51 | ? extensionElements.get('values').filter(function getInstanceOfType(e) { 52 | return e.$instanceOf('spiffworkflow:MessageVariable'); 53 | })[0] 54 | : undefined; 55 | 56 | if (messageResp) { 57 | setValue(messageResp.value); 58 | return messageResp.value; 59 | } 60 | } 61 | 62 | return ''; 63 | }; 64 | 65 | const setValue = (value) => { 66 | var extensions = isMessageEvent(element) 67 | ? element.businessObject.eventDefinitions[0].get('extensionElements') || 68 | moddle.create('bpmn:ExtensionElements') 69 | : element.businessObject.get('extensionElements') || 70 | moddle.create('bpmn:ExtensionElements'); 71 | 72 | let messageVariableObject = getMessageVariableObject(); 73 | if (!messageVariableObject) { 74 | messageVariableObject = moddle.create('spiffworkflow:MessageVariable'); 75 | extensions.get('values').push(messageVariableObject); 76 | } 77 | messageVariableObject.value = value; 78 | isMessageEvent(element) 79 | ? element.businessObject.eventDefinitions[0].set( 80 | 'extensionElements', 81 | extensions 82 | ) 83 | : element.businessObject.set('extensionElements', extensions); 84 | commandStack.execute('element.updateProperties', { 85 | element, 86 | properties: {}, 87 | }); 88 | }; 89 | 90 | return ( 91 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/processLevelProvider/CollaborationPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { MessageArray } from './MessageArray'; 2 | import { CorrelationKeysArray } from './CorrelationKeysArray'; 3 | import { CorrelationPropertiesArray } from './CorrelationPropertiesArray'; 4 | import { ListGroup } from '@bpmn-io/properties-panel'; 5 | 6 | /** 7 | * Adds a group to the properties panel for the script task that allows you 8 | * to set the script. 9 | * @param element 10 | * @param translate 11 | * @returns The components to add to the properties panel. */ 12 | export function createCollaborationGroup( 13 | element, 14 | translate, 15 | moddle, 16 | commandStack, 17 | elementRegistry 18 | ) { 19 | 20 | const results = [ 21 | { 22 | id: 'messages', 23 | label: translate('Messages'), 24 | isDefault: true, 25 | component: ListGroup, 26 | ...MessageArray({ 27 | element, 28 | moddle, 29 | commandStack, 30 | elementRegistry, 31 | translate, 32 | }), 33 | }, 34 | { 35 | id: 'correlation_properties', 36 | class: 'correlation_properties', 37 | className: 'correlation_properties', 38 | label: translate('Correlation Properties'), 39 | isDefault: true, 40 | component: ListGroup, 41 | ...CorrelationPropertiesArray({ 42 | element, 43 | moddle, 44 | commandStack, 45 | elementRegistry, 46 | translate, 47 | }), 48 | } 49 | ]; 50 | 51 | // Hide Correlation Keys 52 | // if (element.type === 'bpmn:Collaboration') { 53 | // results.push({ 54 | // id: 'correlation_keys', 55 | // label: translate('Correlation Keys'), 56 | // isDefault: true, 57 | // component: ListGroup, 58 | // ...CorrelationKeysArray({ 59 | // element, 60 | // moddle, 61 | // commandStack, 62 | // elementRegistry, 63 | // translate, 64 | // }), 65 | // }) 66 | // } 67 | 68 | return results; 69 | } -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/processLevelProvider/CorrelationKeysArray.js: -------------------------------------------------------------------------------- 1 | import { useService } from 'bpmn-js-properties-panel'; 2 | import { SimpleEntry, TextFieldEntry } from '@bpmn-io/properties-panel'; 3 | import { createNewCorrelationKey, findCorrelationKeys, getRoot } from '../../MessageHelpers'; 4 | import { removeFirstInstanceOfItemFromArrayInPlace } from '../../../helpers'; 5 | 6 | /** 7 | * Provides a list of data objects, and allows you to add / remove data objects, and change their ids. 8 | * @param props 9 | * @constructor 10 | */ 11 | export function CorrelationKeysArray(props) { 12 | const { element, moddle, commandStack } = props; 13 | 14 | const correlationKeyElements = findCorrelationKeys(element.businessObject); 15 | 16 | const items = correlationKeyElements.map((correlationKeyElement, index) => { 17 | const id = `correlationGroup-${index}`; 18 | return { 19 | id, 20 | label: correlationKeyElement.name, 21 | entries: correlationGroup({ 22 | idPrefix: id, 23 | element, 24 | correlationKeyElement, 25 | commandStack, 26 | }), 27 | remove: removeFactory({ 28 | element, 29 | correlationKeyElement, 30 | commandStack, 31 | moddle, 32 | }), 33 | autoFocusEntry: id, 34 | }; 35 | }); 36 | 37 | function add(event) { 38 | event.stopPropagation(); 39 | createNewCorrelationKey(element, moddle, commandStack); 40 | } 41 | 42 | return { items, add }; 43 | } 44 | 45 | function removeFactory(props) { 46 | const { element, correlationKeyElement, moddle, commandStack } = props; 47 | 48 | return function (event) { 49 | event.stopPropagation(); 50 | const currentCorrelationKeyElements = 51 | element.businessObject.get('correlationKeys'); 52 | removeFirstInstanceOfItemFromArrayInPlace( 53 | currentCorrelationKeyElements, 54 | correlationKeyElement 55 | ); 56 | commandStack.execute('element.updateProperties', { 57 | element, 58 | properties: {}, 59 | }); 60 | }; 61 | } 62 | 63 | function correlationGroup(props) { 64 | const { idPrefix, correlationKeyElement, commandStack } = props; 65 | const entries = [ 66 | { 67 | id: `${idPrefix}-key`, 68 | component: CorrelationKeyTextField, 69 | correlationKeyElement, 70 | commandStack, 71 | }, 72 | ]; 73 | (correlationKeyElement.correlationPropertyRef || []).forEach( 74 | (correlationProperty, index) => { 75 | entries.push({ 76 | id: `${idPrefix}-${index}-text`, 77 | component: CorrelationPropertyText, 78 | correlationProperty, 79 | }); 80 | } 81 | ); 82 | return entries; 83 | } 84 | 85 | function CorrelationKeyTextField(props) { 86 | const { id, element, correlationKeyElement, commandStack } = props; 87 | 88 | const debounce = useService('debounceInput'); 89 | const setValue = (value) => { 90 | commandStack.execute('element.updateModdleProperties', { 91 | element, 92 | moddleElement: correlationKeyElement, 93 | properties: { 94 | name: value, 95 | }, 96 | }); 97 | }; 98 | 99 | const getValue = () => { 100 | return correlationKeyElement.name; 101 | }; 102 | 103 | return TextFieldEntry({ 104 | element, 105 | id: `${id}-textField`, 106 | getValue, 107 | setValue, 108 | debounce, 109 | }); 110 | } 111 | 112 | function CorrelationPropertyText(props) { 113 | const { id, parameter, correlationProperty } = props; 114 | const debounce = useService('debounceInput'); 115 | 116 | const getValue = () => { 117 | return correlationProperty.id; 118 | }; 119 | 120 | return SimpleEntry({ 121 | element: parameter, 122 | id: `${id}-textField`, 123 | label: correlationProperty.id, 124 | getValue, 125 | disabled: true, 126 | debounce, 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /app/spiffworkflow/messages/propertiesPanel/processLevelProvider/MessagePropertiesMultiSelect.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { getCorrelationPropertiesIDsFiltredByMessageRef, setMessageRefToListofCorrelationProperties } from '../../MessageHelpers'; 4 | 5 | import NiceSelect from 'nice-select2/dist/js/nice-select2'; 6 | 7 | let niceSelectInputs = {}; 8 | 9 | export function MessagePropertiesMultiSelect(props) { 10 | 11 | const { element, id, moddle, messageElement, commandStack } = props; 12 | 13 | const modeling = useService('modeling'); 14 | const translate = useService('translate'); 15 | const debounce = useService('debounceInput'); 16 | 17 | const setValue = (value) => { 18 | // Add message ref to the selected correlation properties 19 | setMessageRefToListofCorrelationProperties(messageElement, value, element, moddle, commandStack) 20 | }; 21 | 22 | const getOptions = () => { 23 | const correlationProperties = getCorrelationPropertiesIDsFiltredByMessageRef(element.businessObject, moddle, messageElement); 24 | return correlationProperties; 25 | }; 26 | 27 | function handleSelectChange(e) { 28 | const selectedOptions = Array.from(e.target.selectedOptions).map(option => option.value); 29 | setValue(selectedOptions); 30 | } 31 | 32 | const initializeNiceSelect = () => { 33 | const selectElement = document.getElementById(`${id}_select`); 34 | if (!selectElement) return; 35 | 36 | if (niceSelectInputs[id] && niceSelectInputs[id] !== null ) { 37 | niceSelectInputs[id].destroy(); 38 | } 39 | 40 | const opts = { 41 | // data: options, 42 | searchable: true, 43 | placeholder: 'Select message properties', 44 | showSelectedItems: true 45 | }; 46 | 47 | niceSelectInputs[id] = new NiceSelect(selectElement, opts); 48 | updateSelectOptions(selectElement) 49 | 50 | 51 | }; 52 | 53 | function updateSelectOptions(selectElement) { 54 | for (let option of selectElement.options) { 55 | const matchingOption = options.find(opt => opt.value === option.value); 56 | if (matchingOption && matchingOption.selected) { 57 | option.setAttribute('selected', 'true'); 58 | } 59 | } 60 | } 61 | 62 | const renderSelect = (options) => { 63 | return html` 64 | 71 | `; 72 | } 73 | 74 | const options = getOptions(); 75 | 76 | setTimeout(() => { 77 | initializeNiceSelect(); 78 | }, 0); 79 | 80 | return html` 81 |
82 | 83 | ${renderSelect(options)} 84 |
85 | `; 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/spiffworkflow/signals/index.js: -------------------------------------------------------------------------------- 1 | import SignalPropertiesProvider from './propertiesPanel/SignalPropertiesProvider'; 2 | 3 | export default { 4 | __init__: ['signalPropertiesProvider'], 5 | signalPropertiesProvider: ['type', SignalPropertiesProvider], 6 | } 7 | -------------------------------------------------------------------------------- /app/spiffworkflow/signals/propertiesPanel/SignalPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { getRoot } from '../../helpers'; 3 | import { getArrayForType, getListGroupForType } from '../../eventList.js'; 4 | import { hasEventType, 5 | replaceGroup, 6 | getSelectorForType, 7 | getConfigureGroupForType 8 | } from '../../eventSelect.js'; 9 | 10 | const LOW_PRIORITY = 500; 11 | 12 | const eventDetails = { 13 | 'eventType': 'bpmn:Signal', 14 | 'eventDefType': 'bpmn:SignalEventDefinition', 15 | 'referenceType': 'signalRef', 16 | 'idPrefix': 'signal', 17 | }; 18 | 19 | export default function SignalPropertiesProvider( 20 | propertiesPanel, 21 | translate, 22 | moddle, 23 | commandStack, 24 | ) { 25 | 26 | this.getGroups = function (element) { 27 | return function (groups) { 28 | if (is(element, 'bpmn:Process') || is(element, 'bpmn:Collaboration')) { 29 | const getSignalArray = getArrayForType('bpmn:Signal', 'signalRef', 'Signal'); 30 | const signalGroup = getListGroupForType('signals', 'Signals', getSignalArray); 31 | groups.push(signalGroup({ element, translate, moddle, commandStack })); 32 | } else if (hasEventType(element, 'bpmn:SignalEventDefinition')) { 33 | const getSignalSelector = getSelectorForType(eventDetails); 34 | const signalGroup = getConfigureGroupForType(eventDetails, 'Signal', false, getSignalSelector); 35 | const group = signalGroup({ element, translate, moddle, commandStack }); 36 | replaceGroup('signal', groups, group); 37 | } 38 | return groups; 39 | }; 40 | }; 41 | propertiesPanel.registerProvider(LOW_PRIORITY, this); 42 | } 43 | 44 | SignalPropertiesProvider.$inject = [ 45 | 'propertiesPanel', 46 | 'translate', 47 | 'moddle', 48 | 'commandStack', 49 | ]; 50 | -------------------------------------------------------------------------------- /docs/io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sartography/bpmn-js-spiffworkflow/6208169162a5a7375a783f189d19c3e896b75e6d/docs/io.png -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const coverage = process.env.COVERAGE; 2 | const path = require('path'); 3 | const { DefinePlugin, NormalModuleReplacementPlugin } = require('webpack'); 4 | 5 | const basePath = '.'; 6 | const absoluteBasePath = path.resolve(path.join(__dirname, basePath)); 7 | 8 | module.exports = function (karma) { 9 | karma.set({ 10 | frameworks: ['webpack', 'mocha', 'sinon-chai'], 11 | 12 | files: ['test/spec/**/*Spec.js'], 13 | 14 | reporters: ['dots'], 15 | 16 | preprocessors: { 17 | 'test/spec/**/*Spec.js': ['webpack', 'env'], 18 | }, 19 | 20 | browsers: ['ChromeHeadless'], 21 | 22 | browserNoActivityTimeout: 30000, 23 | 24 | singleRun: true, 25 | autoWatch: false, 26 | 27 | webpack: { 28 | mode: 'development', 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.(css|bpmn)$/, 33 | use: 'raw-loader', 34 | }, 35 | { 36 | test: /\.m?jsx?$/, 37 | exclude: /node_modules/, 38 | use: { 39 | loader: 'babel-loader', 40 | options: { 41 | plugins: [ 42 | [ 43 | '@babel/plugin-transform-react-jsx', 44 | { 45 | importSource: '@bpmn-io/properties-panel/preact', 46 | runtime: 'automatic', 47 | }, 48 | ], 49 | ], 50 | }, 51 | }, 52 | }, 53 | { 54 | test: /\.svg$/, 55 | use: ['react-svg-loader'], 56 | }, 57 | ].concat( 58 | coverage 59 | ? { 60 | test: /\.js$/, 61 | use: { 62 | loader: 'istanbul-instrumenter-loader', 63 | options: { esModules: true }, 64 | }, 65 | enforce: 'post', 66 | include: /src\.*/, 67 | exclude: /node_modules/, 68 | } 69 | : [] 70 | ), 71 | }, 72 | plugins: [ 73 | new DefinePlugin({ 74 | // @barmac: process.env has to be defined to make @testing-library/preact work 75 | 'process.env': {}, 76 | }), 77 | new NormalModuleReplacementPlugin(/^preact(\/[^/]+)?$/, function ( 78 | resource 79 | ) { 80 | const replMap = { 81 | 'preact/hooks': path.resolve( 82 | 'node_modules/@bpmn-io/properties-panel/preact/hooks/dist/hooks.module.js' 83 | ), 84 | 'preact/jsx-runtime': path.resolve( 85 | 'node_modules/@bpmn-io/properties-panel/preact/jsx-runtime/dist/jsxRuntime.module.js' 86 | ), 87 | preact: path.resolve( 88 | 'node_modules/@bpmn-io/properties-panel/preact/dist/preact.module.js' 89 | ), 90 | }; 91 | 92 | const replacement = replMap[resource.request]; 93 | 94 | if (!replacement) { 95 | return; 96 | } 97 | 98 | resource.request = replacement; 99 | }), 100 | new NormalModuleReplacementPlugin( 101 | /^preact\/hooks/, 102 | path.resolve( 103 | 'node_modules/@bpmn-io/properties-panel/preact/hooks/dist/hooks.module.js' 104 | ) 105 | ), 106 | ], 107 | resolve: { 108 | extensions: ['', '.js', '.jsx', '.tsx'], 109 | mainFields: ['browser', 'module', 'main'], 110 | alias: { 111 | preact: '@bpmn-io/properties-panel/preact', 112 | react: '@bpmn-io/properties-panel/preact/compat', 113 | 'react-dom': '@bpmn-io/properties-panel/preact/compat', 114 | }, 115 | modules: ['node_modules', absoluteBasePath], 116 | }, 117 | devtool: 'eval-source-map', 118 | }, 119 | }); 120 | }; 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpmn-js-spiffworkflow", 3 | "version": "0.0.8", 4 | "description": "Extensions and modifications of BPMN.js to improve BPMN development for SpiffWorkflow", 5 | "scripts": { 6 | "all": "run-s lint test build", 7 | "build": "webpack --mode production", 8 | "build:watch": "webpack --watch", 9 | "dev": "run-p build:watch serve", 10 | "serve": "sirv public --dev", 11 | "lint": "./node_modules/.bin/eslint app *.js", 12 | "lint:fix": "./node_modules/.bin/eslint --fix app *.js", 13 | "start": "run-s build serve", 14 | "test": "karma start karma.conf.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/sartography/bpmn-js-spiffworkflow" 19 | }, 20 | "keywords": [ 21 | "bpmn", 22 | "spiffworkflow" 23 | ], 24 | "author": { 25 | "name": "Dan Funk (Sartography)", 26 | "url": "https://github.com/danfunk" 27 | }, 28 | "contributors": [ 29 | { 30 | "name": "bpmn.io contributors", 31 | "url": "https://github.com/bpmn-io" 32 | } 33 | ], 34 | "license": "LGPL", 35 | "devDependencies": { 36 | "@babel/core": "^7.18.6", 37 | "@babel/plugin-transform-react-jsx": "^7.17.12", 38 | "@babel/preset-env": "^7.18.6", 39 | "@babel/preset-react": "^7.18.2", 40 | "@testing-library/preact": "^2.0.1", 41 | "@testing-library/preact-hooks": "^1.1.0", 42 | "@types/mocha": "^9.1.1", 43 | "babel-loader": "^8.2.5", 44 | "chai": "^4.3.6", 45 | "copy-webpack-plugin": "^11.0.0", 46 | "eslint": "^8.18.0", 47 | "eslint_d": "^12.2.0", 48 | "eslint-config-airbnb": "^19.0.4", 49 | "eslint-config-prettier": "^8.5.0", 50 | "eslint-plugin-bpmn-io": "^0.14.0", 51 | "eslint-plugin-import": "^2.26.0", 52 | "eslint-plugin-jsx-a11y": "^6.6.0", 53 | "eslint-plugin-prettier": "^4.2.1", 54 | "eslint-plugin-sonarjs": "^0.13.0", 55 | "file-saver": "^2.0.5", 56 | "karma": "^6.3.4", 57 | "karma-chrome-launcher": "^3.1.1", 58 | "karma-coverage": "^2.2.0", 59 | "karma-env-preprocessor": "^0.1.1", 60 | "karma-mocha": "^2.0.1", 61 | "karma-sinon-chai": "^2.0.2", 62 | "karma-webpack": "^5.0.0", 63 | "mocha": "^10.0.0", 64 | "mocha-test-container-support": "^0.2.0", 65 | "npm-run-all": "^4.1.5", 66 | "prettier": "^2.7.1", 67 | "raw-loader": "^4.0.2", 68 | "sinon": "^14.0.0", 69 | "sinon-chai": "^3.7.0", 70 | "sirv-cli": "^2.0.2", 71 | "stringify": "^5.2.0", 72 | "webpack": "^5.73.0", 73 | "webpack-cli": "^4.9.2" 74 | }, 75 | "peerDependencies": { 76 | "bpmn-js": "^17.0.0", 77 | "bpmn-js-properties-panel": "*", 78 | "diagram-js": "*" 79 | }, 80 | "dependencies": { 81 | "inherits": "^2.0.4", 82 | "inherits-browser": "^0.0.1", 83 | "min-dash": "^3.8.1", 84 | "min-dom": "^3.2.1", 85 | "moddle": "^5.0.3", 86 | "nice-select2": "^2.1.0", 87 | "react": "^18.2.0", 88 | "react-dom": "18.2.0", 89 | "tiny-svg": "^2.2.3" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:bpmn-io/mocha" 3 | } -------------------------------------------------------------------------------- /test/spec/BpmnInputOutputSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | query as domQuery, 3 | queryAll as domQueryAll 4 | } from 'min-dom'; 5 | import { bootstrapPropertiesPanel, CONTAINER } from './helpers'; 6 | import inputOutput from '../../app/spiffworkflow/InputOutput'; 7 | import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; 8 | 9 | describe('BPMN Input / Output', function() { 10 | 11 | let xml = require('./bpmn/diagram.bpmn').default; 12 | 13 | beforeEach(bootstrapPropertiesPanel(xml, { 14 | debounceInput: false, 15 | additionalModules: [ 16 | inputOutput, 17 | BpmnPropertiesPanelModule, 18 | BpmnPropertiesProviderModule, 19 | ] 20 | })); 21 | 22 | it('should have a data input and data output in the properties panel', function() { 23 | var paletteElement = domQuery('.djs-palette', CONTAINER); 24 | var entries = domQueryAll('.entry', paletteElement); 25 | expect(entries[11].title).to.equals('Create DataInput'); 26 | expect(entries[12].title).to.equals('Create DataOutput'); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/spec/BusinessRulePropsSpec.js: -------------------------------------------------------------------------------- 1 | import { getBpmnJS } from 'bpmn-js/test/helper'; 2 | 3 | import { 4 | BpmnPropertiesPanelModule, 5 | BpmnPropertiesProviderModule, 6 | } from 'bpmn-js-properties-panel'; 7 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 8 | import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; 9 | import { 10 | bootstrapPropertiesPanel, 11 | changeInput, 12 | expectSelected, 13 | findEntry, 14 | findSelect, 15 | getPropertiesPanel, 16 | } from './helpers'; 17 | import extensions from '../../app/spiffworkflow/extensions'; 18 | 19 | describe('Business Rule Properties Panel', function () { 20 | const xml = require('./bpmn/diagram.bpmn').default; 21 | 22 | beforeEach( 23 | bootstrapPropertiesPanel(xml, { 24 | debounceInput: false, 25 | additionalModules: [ 26 | BpmnPropertiesPanelModule, 27 | BpmnPropertiesProviderModule, 28 | extensions, 29 | ], 30 | moddleExtensions: { 31 | spiffworkflow: spiffModdleExtension, 32 | }, 33 | }) 34 | ); 35 | 36 | const return_files = (event) => { 37 | event.eventBus.fire('spiff.dmn_files.returned', { 38 | options: [ 39 | { label: 'Calculate Pizza Price', value: 'Decision_Pizza_Price' }, 40 | { label: 'Viking Availability', value: 'Decision_Vikings' }, 41 | { label: 'Test Decision', value: 'test_decision' }, 42 | ], 43 | }); 44 | }; 45 | 46 | it('should display a dropdown to select from available decision tables', async function () { 47 | const modeler = getBpmnJS(); 48 | modeler.get('eventBus').once('spiff.dmn_files.requested', return_files); 49 | expectSelected('business_rule_task'); 50 | // THEN - a properties panel exists with a section for editing that script 51 | const entry = findEntry( 52 | 'extension_spiffworkflow:CalledDecisionId', 53 | getPropertiesPanel() 54 | ); 55 | expect(entry, 'No Entry').to.exist; 56 | const selectList = findSelect(entry); 57 | expect(selectList, 'No Select').to.exist; 58 | modeler.get('eventBus').off('spiff.dmn_files.requested', return_files); 59 | }); 60 | 61 | it('should update the spiffworkflow:calledDecisionId tag when you modify the called decision select box', async function () { 62 | // IF - a script tag is selected, and you change the script in the properties panel 63 | const modeler = getBpmnJS(); 64 | modeler.get('eventBus').on('spiff.dmn_files.requested', return_files); 65 | const businessRuleTask = await expectSelected('business_rule_task'); 66 | const entry = findEntry('extension_CalledDecisionId', getPropertiesPanel()); 67 | const selectList = findSelect(entry); 68 | changeInput(selectList, 'Decision_Pizza_Price'); 69 | 70 | // THEN - the decision id in the BPMN Business object / XML is updated as well. 71 | const businessObject = getBusinessObject(businessRuleTask); 72 | expect(businessObject.extensionElements).to.exist; 73 | const element = businessObject.extensionElements.values[0]; 74 | expect(element.value).to.equal('Decision_Pizza_Price'); 75 | modeler.get('eventBus').off('spiff.dmn_files.requested', return_files); 76 | }); 77 | 78 | it('should load up the xml and the value for the called decision should match the xml', async function () { 79 | const modeler = getBpmnJS(); 80 | modeler.get('eventBus').on('spiff.dmn_files.requested', return_files); 81 | const businessRuleTask = await expectSelected('business_rule_task'); 82 | const entry = findEntry('extension_CalledDecisionId', getPropertiesPanel()); 83 | const selectList = findSelect(entry); 84 | expect(selectList.value, 'initial value is wrong').to.equal( 85 | 'test_decision' 86 | ); 87 | 88 | const businessObject = getBusinessObject(businessRuleTask); 89 | expect(businessObject.extensionElements).to.exist; 90 | const element = businessObject.extensionElements.values[0]; 91 | expect(element.value).to.equal('test_decision'); 92 | modeler.get('eventBus').off('spiff.dmn_files.requested', return_files); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/spec/ConditionSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | query as domQuery, 3 | queryAll as domQueryAll 4 | } from 'min-dom'; 5 | import { 6 | bootstrapPropertiesPanel, 7 | expectSelected, 8 | findGroupEntry, 9 | changeInput, 10 | PROPERTIES_PANEL_CONTAINER, 11 | } from './helpers'; 12 | import conditionsPanel from '../../app/spiffworkflow/conditions'; 13 | import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; 14 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 15 | 16 | describe('BPMN Condition', function() { 17 | 18 | let xml = require('./bpmn/conditional_event.bpmn').default; 19 | 20 | beforeEach(bootstrapPropertiesPanel(xml, { 21 | debounceInput: false, 22 | additionalModules: [ 23 | conditionsPanel, 24 | BpmnPropertiesPanelModule, 25 | BpmnPropertiesProviderModule, 26 | ] 27 | })); 28 | 29 | it('should add a condition panel when Conditional Event is selected', async function() { 30 | const shapeElement = await expectSelected('conditional_event'); 31 | const businessObject = getBusinessObject(shapeElement); 32 | const conditions = findGroupEntry('conditions', PROPERTIES_PANEL_CONTAINER); 33 | expect(conditions).to.exist; 34 | 35 | const textInput = domQuery('textarea', conditions); 36 | expect(textInput.value).to.equal('cancel_task_2'); 37 | changeInput(textInput, 'True'); 38 | }); 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /test/spec/DataObjectInPoolsSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | BpmnPropertiesPanelModule, 3 | BpmnPropertiesProviderModule, 4 | } from 'bpmn-js-properties-panel'; 5 | import TestContainer from 'mocha-test-container-support'; 6 | import { 7 | bootstrapPropertiesPanel, 8 | changeInput, 9 | expectSelected, 10 | findEntry, 11 | findSelect, 12 | } from './helpers'; 13 | import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; 14 | import DataObject from '../../app/spiffworkflow/DataObject'; 15 | 16 | describe('Properties Panel for Data Objects', function () { 17 | const xml = require('./bpmn/data_objects_in_pools.bpmn').default; 18 | let container; 19 | 20 | beforeEach(function () { 21 | container = TestContainer.get(this); 22 | }); 23 | 24 | beforeEach( 25 | bootstrapPropertiesPanel(xml, { 26 | container, 27 | debounceInput: false, 28 | additionalModules: [ 29 | DataObject, 30 | BpmnPropertiesPanelModule, 31 | BpmnPropertiesProviderModule, 32 | ], 33 | moddleExtensions: { 34 | spiffworkflow: spiffModdleExtension, 35 | }, 36 | }) 37 | ); 38 | 39 | it('should allow you to select other data objects within the same participant', async function () { 40 | // IF - a data object reference is selected 41 | const doREF = await expectSelected('pool1Do1_REF'); 42 | expect(doREF).to.exist; 43 | 44 | // THEN - a select Data Object section should appear in the properties panel 45 | const entry = findEntry('selectDataObject', container); 46 | const selector = findSelect(entry); 47 | changeInput(selector, 'pool1Do2'); 48 | // then this data reference object now references that data object. 49 | const { businessObject } = doREF; 50 | expect(businessObject.get('dataObjectRef').id).to.equal('pool1Do2'); 51 | }); 52 | 53 | it('should NOT allow you to select data objects within other participants', async function () { 54 | // IF - a data object reference is selected 55 | const doREF = await expectSelected('pool1Do1_REF'); 56 | expect(doREF).to.exist; 57 | 58 | // THEN - a select Data Object section should appear in the properties panel but pool2Do1 should not be an option 59 | const entry = findEntry('selectDataObject', container); 60 | const selector = findSelect(entry); 61 | expect(selector.length).to.equal(2); 62 | expect(selector[0].value === 'pool1Do2'); 63 | expect(selector[1].value === 'pool1Do1'); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /test/spec/DataObjectRulesSpec.js: -------------------------------------------------------------------------------- 1 | import { bootstrapPropertiesPanel } from './helpers'; 2 | import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; 3 | import dataObject from '../../app/spiffworkflow/DataObject'; 4 | import { inject } from 'bpmn-js/test/helper'; 5 | 6 | describe('BPMN Input / Output', function() { 7 | 8 | let xml = require('./bpmn/subprocess.bpmn').default; 9 | 10 | beforeEach(bootstrapPropertiesPanel(xml, { 11 | debounceInput: false, 12 | additionalModules: [ 13 | dataObject, 14 | BpmnPropertiesPanelModule, 15 | BpmnPropertiesProviderModule, 16 | ] 17 | })); 18 | 19 | 20 | it('will prevent dragging data reference to a different process', 21 | inject(function(canvas, modeling, elementRegistry, dataObjectRules) { 22 | 23 | // IF - a data object reference exists on the root element, and a SubProcess Also Exists 24 | let rootShape = canvas.getRootElement(); 25 | const dataObjectRefShape = modeling.createShape({ type: 'bpmn:DataObjectReference' }, 26 | { x: 220, y: 220 }, rootShape); 27 | const subProcessElement = elementRegistry.get('my_subprocess'); 28 | 29 | var canDrop = dataObjectRules.canDrop( 30 | [ dataObjectRefShape ], 31 | subProcessElement 32 | ); 33 | 34 | expect(canDrop).to.be.false; 35 | 36 | })); 37 | }); 38 | -------------------------------------------------------------------------------- /test/spec/DataStoreInterceptorSpec.js: -------------------------------------------------------------------------------- 1 | import { bootstrapPropertiesPanel, expectSelected } from './helpers'; 2 | import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; 3 | import { 4 | getBpmnJS, 5 | inject 6 | } from 'bpmn-js/test/helper'; 7 | import dataStoreInterceptor from '../../app/spiffworkflow/DataStoreReference'; 8 | 9 | describe('DataStore Interceptor', function () { 10 | 11 | let xml = require('./bpmn/data_store.bpmn').default; 12 | 13 | beforeEach(bootstrapPropertiesPanel(xml, { 14 | debounceInput: false, 15 | additionalModules: [ 16 | dataStoreInterceptor, 17 | BpmnPropertiesPanelModule, 18 | BpmnPropertiesProviderModule, 19 | ] 20 | })); 21 | 22 | 23 | it('should delete dataStore in case dataStoreRef is deleted - DataStoreReference element', inject(async function (modeling) { 24 | 25 | const modeler = getBpmnJS(); 26 | 27 | // We Select a DataStoreReference element 28 | const shapeElement = await expectSelected('DataStoreReference_0eqeh4p'); 29 | expect(shapeElement, "I can't find DataStoreReference element").to.exist; 30 | 31 | let definitions = await modeler.getDefinitions(); 32 | let dataStoreExists = definitions.get('rootElements').some(element => 33 | element.$type === 'bpmn:DataStore' && element.id === 'countries' 34 | ); 35 | expect(dataStoreExists, "DataStore 'countries' should be added at the root level").to.be.true; 36 | 37 | // Remove dataStoreReference 38 | await modeler.get('modeling').removeShape(shapeElement); 39 | const nwshapeElement = await expectSelected('DataStoreReference_0eqeh4p'); 40 | expect(nwshapeElement, "I can't find DataStoreReference element").not.to.exist; 41 | 42 | // Check that DataStore foods is removed from the root of the process 43 | definitions = await modeler.getDefinitions(); 44 | dataStoreExists = definitions.get('rootElements').some(element => 45 | element.$type === 'bpmn:DataStore' && element.id === 'countries' 46 | ); 47 | expect(dataStoreExists, "DataStore 'countries' should be removed from the root level").not.to.be.true; 48 | })); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/IOInterceptorSpec.js: -------------------------------------------------------------------------------- 1 | import { bootstrapPropertiesPanel } from './helpers'; 2 | import dataObjectInterceptor from '../../app/spiffworkflow/DataObject'; 3 | import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; 4 | import { 5 | inject, 6 | } from 'bpmn-js/test/helper'; 7 | import { findDataObjects } from '../../app/spiffworkflow/DataObject/DataObjectHelpers'; 8 | import IoInterceptor from '../../app/spiffworkflow/InputOutput/IoInterceptor'; 9 | import InputOutput from '../../app/spiffworkflow/InputOutput'; 10 | 11 | describe('Input/Output Interceptor', function() { 12 | 13 | let xml = require('./bpmn/empty_diagram.bpmn').default; 14 | 15 | beforeEach(bootstrapPropertiesPanel(xml, { 16 | debounceInput: false, 17 | additionalModules: [ 18 | InputOutput, 19 | BpmnPropertiesPanelModule, 20 | BpmnPropertiesProviderModule, 21 | ] 22 | })); 23 | 24 | it('New Data Input should create an IOSpecification with a single dataInput object', inject(function(canvas, modeling) { 25 | 26 | expect(canvas.getRootElement().businessObject.ioSpecification).to.be.undefined; 27 | 28 | // IF - a new dataObjectReference is created 29 | let rootShape = canvas.getRootElement(); 30 | const dataInput = modeling.createShape({ type: 'bpmn:DataInput' }, 31 | { x: 220, y: 220 }, rootShape); 32 | 33 | // THEN - the process should now have an IO Specification 34 | const iospec = canvas.getRootElement().businessObject.ioSpecification; 35 | expect(iospec).to.not.be.null; 36 | expect(iospec.dataInputs.length).to.equal(1); 37 | expect(iospec.inputSets[0].dataInputRefs.length).to.equal(1); 38 | })); 39 | 40 | 41 | it('IOSpecification always contain input sets and output sets if they exist at all.', inject(function(canvas, modeling) { 42 | 43 | expect(canvas.getRootElement().businessObject.ioSpecification).to.be.undefined; 44 | 45 | // IF - a new dataObjectReference is created 46 | let rootShape = canvas.getRootElement(); 47 | const dataInput = modeling.createShape({type: 'bpmn:DataInput'}, 48 | {x: 220, y: 220}, rootShape); 49 | 50 | // THEN - there are inputSets and outputSets 51 | const iospec = canvas.getRootElement().businessObject.ioSpecification; 52 | expect(iospec.inputSets).to.not.be.null; 53 | expect(iospec.outputSets).to.not.be.null; 54 | })); 55 | 56 | it('After removing all input sets, the ioSpecification should be null.', inject(function(canvas, modeling) { 57 | // IF - a new dataObjectReference is created and then deleted. 58 | let rootShape = canvas.getRootElement(); 59 | const dataInput = modeling.createShape({type: 'bpmn:DataInput'}, 60 | {x: 220, y: 220}, rootShape); 61 | modeling.removeShape(dataInput) 62 | expect(canvas.getRootElement().businessObject.ioSpecification).to.be.null; 63 | })); 64 | 65 | it('deleting a data input should remove it from the input set', inject(function(canvas, modeling) { 66 | let rootShape = canvas.getRootElement(); 67 | const dataInput = modeling.createShape({type: 'bpmn:DataInput'}, 68 | {x: 220, y: 220}, rootShape); 69 | const dataOutput = modeling.createShape({type: 'bpmn:DataOutput'}, 70 | {x: 240, y: 220}, rootShape); 71 | modeling.removeShape(dataInput); 72 | const iospec = canvas.getRootElement().businessObject.ioSpecification; 73 | expect(iospec.dataInputs.length).to.equal(0); 74 | expect(iospec.inputSets[0].dataInputRefs.length).to.equal(0); 75 | })); 76 | }); 77 | -------------------------------------------------------------------------------- /test/spec/IoVariablesSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | query as domQuery 3 | } from 'min-dom'; 4 | import { bootstrapPropertiesPanel, CONTAINER, expectSelected, findGroupEntry } from './helpers'; 5 | import inputOutput from '../../app/spiffworkflow/InputOutput'; 6 | import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; 7 | import { fireEvent } from '@testing-library/preact'; 8 | 9 | describe('BPMN Input / Output Variables', function () { 10 | 11 | let xml = require('./bpmn/io_variables.bpmn').default; 12 | 13 | beforeEach(bootstrapPropertiesPanel(xml, { 14 | debounceInput: false, 15 | additionalModules: [ 16 | inputOutput, 17 | BpmnPropertiesPanelModule, 18 | BpmnPropertiesProviderModule 19 | ] 20 | })); 21 | 22 | it('should be able to add a new input to the user task', async function () { 23 | 24 | // We Select a userTask element 25 | const shapeElement = await expectSelected('Activity_1hmit5k'); 26 | expect(shapeElement, "I can't find User Task element").to.exist; 27 | 28 | // Expect shapeElement.businessObject.ioSpecification to be undefined 29 | expect(shapeElement.businessObject.ioSpecification).to.be.undefined; 30 | 31 | // Add new dataInput 32 | const entry = findGroupEntry('inputParameters', CONTAINER); 33 | let addButton = domQuery('.bio-properties-panel-add-entry', entry); 34 | fireEvent.click(addButton); 35 | 36 | expect(shapeElement.businessObject.ioSpecification).not.to.be.undefined; 37 | expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1); 38 | }); 39 | 40 | it('should be able to add a new output to the user task', async function () { 41 | 42 | // We Select a userTask element 43 | const shapeElement = await expectSelected('Activity_1hmit5k'); 44 | expect(shapeElement, "I can't find User Task element").to.exist; 45 | 46 | // Expect shapeElement.businessObject.ioSpecification to be undefined 47 | expect(shapeElement.businessObject.ioSpecification).to.be.undefined; 48 | 49 | // Add new dataOutput 50 | const entry = findGroupEntry('outputParameters', CONTAINER); 51 | let addButton = domQuery('.bio-properties-panel-add-entry', entry); 52 | fireEvent.click(addButton); 53 | 54 | expect(shapeElement.businessObject.ioSpecification).not.to.be.undefined; 55 | expect(shapeElement.businessObject.ioSpecification.dataOutputs.length).to.equal(1); 56 | 57 | }); 58 | 59 | it('should be able to delete an existing input variable from script task', async function () { 60 | 61 | // We Select a scriptTask element 62 | const shapeElement = await expectSelected('Activity_1dkj93x'); 63 | expect(shapeElement, "I can't find Script Task element").to.exist; 64 | expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1); 65 | 66 | const entry = findGroupEntry('inputParameters', CONTAINER); 67 | let removeButton = domQuery('.bio-properties-panel-remove-entry', entry); 68 | fireEvent.click(removeButton); 69 | 70 | expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(0); 71 | expect(shapeElement.businessObject.ioSpecification.dataOutputs.length).to.equal(1); 72 | }) 73 | 74 | 75 | it('should be able to delete an existing output variable from script task', async function () { 76 | 77 | // We Select a scriptTask element 78 | const shapeElement = await expectSelected('Activity_1dkj93x'); 79 | expect(shapeElement, "I can't find Script Task element").to.exist; 80 | expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1); 81 | 82 | const entry = findGroupEntry('outputParameters', CONTAINER); 83 | let removeButton = domQuery('.bio-properties-panel-remove-entry', entry); 84 | fireEvent.click(removeButton); 85 | 86 | expect(shapeElement.businessObject.ioSpecification.dataInputs.length).to.equal(1); 87 | expect(shapeElement.businessObject.ioSpecification.dataOutputs.length).to.equal(0); 88 | }) 89 | 90 | 91 | }); 92 | -------------------------------------------------------------------------------- /test/spec/MessagesCorrSpec.js: -------------------------------------------------------------------------------- 1 | import TestContainer from 'mocha-test-container-support'; 2 | import { 3 | query as domQuery, 4 | queryAll as domQueryAll, 5 | } from 'min-dom'; 6 | import { 7 | BpmnPropertiesPanelModule, 8 | BpmnPropertiesProviderModule, 9 | } from 'bpmn-js-properties-panel'; 10 | import { 11 | bootstrapPropertiesPanel, 12 | expectSelected, 13 | findEntry, 14 | findGroupEntry, 15 | findSelect, 16 | findTextarea, 17 | findInput, 18 | pressButton, 19 | findButtonByClass, 20 | getPropertiesPanel, 21 | changeInput, 22 | findDivByClass 23 | } from './helpers'; 24 | import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; 25 | import messages from '../../app/spiffworkflow/messages'; 26 | import { fireEvent } from '@testing-library/preact'; 27 | import { getBpmnJS, inject } from 'bpmn-js/test/helper'; 28 | import { findCorrelationProperties, findMessageModdleElements } from '../../app/spiffworkflow/messages/MessageHelpers'; 29 | import {spiffExtensionOptions} from "../../app/spiffworkflow/extensions/propertiesPanel/SpiffExtensionSelect"; 30 | 31 | describe('Multiple messages should work', function () { 32 | const xml = require('./bpmn/two_messages.bpmn').default; 33 | let container; 34 | 35 | beforeEach(function () { 36 | container = TestContainer.get(this); 37 | }); 38 | 39 | beforeEach( 40 | bootstrapPropertiesPanel(xml, { 41 | container, 42 | debounceInput: false, 43 | additionalModules: [ 44 | messages, 45 | BpmnPropertiesPanelModule, 46 | BpmnPropertiesProviderModule, 47 | ], 48 | moddleExtensions: { 49 | spiffworkflow: spiffModdleExtension, 50 | }, 51 | }) 52 | ); 53 | 54 | 55 | const new_message_event = (eventBus) => { 56 | eventBus.fire('spiff.add_message.returned', { 57 | elementId: "ActivityA", 58 | name: "messageA", 59 | correlation_properties: 60 | { "new_name": { retrieval_expression: "new_exp" }} 61 | }); 62 | }; 63 | 64 | 65 | it('and it should be possible to change a correlation property name', async function () { 66 | const modeler = getBpmnJS(); 67 | 68 | const sendShape = await expectSelected('ActivityA'); 69 | expect(sendShape, "Can't find Send Task").to.exist; 70 | 71 | const oldName = "old_name" 72 | const newName = "new_name" 73 | 74 | const labels = domQueryAll(`.bio-properties-panel-label`, container); 75 | const oldNameFound = Array.from(labels).some(label => label.textContent.includes(oldName)); 76 | expect(oldNameFound).to.be.true; 77 | 78 | //Update message 79 | new_message_event(modeler.get('eventBus')) 80 | const sendShape2 = await expectSelected('ActivityA'); 81 | 82 | // The old name should no longer be there, but the new name should exist. 83 | const labels2 = domQueryAll(`.bio-properties-panel-label`, container); 84 | const oldNameFound2 = Array.from(labels2).some(label => label.textContent.includes(oldName)); 85 | expect(oldNameFound2).to.be.false; 86 | const newNameFound2 = Array.from(labels2).some(label => label.textContent.includes(newName)); 87 | expect(newNameFound2).to.be.true; 88 | 89 | 90 | }); 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /test/spec/ProcessPropsSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | query as domQuery, 3 | } from 'min-dom'; 4 | import { 5 | bootstrapPropertiesPanel, changeInput, 6 | expectSelected, findEntry, findGroupEntry, findInput 7 | } from './helpers'; 8 | import { BpmnPropertiesPanelModule, BpmnPropertiesProviderModule } from 'bpmn-js-properties-panel'; 9 | import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; 10 | import TestContainer from 'mocha-test-container-support'; 11 | import { fireEvent } from '@testing-library/preact'; 12 | import { findDataObject, findDataObjects } from '../../app/spiffworkflow/DataObject/DataObjectHelpers'; 13 | import dataObject from '../../app/spiffworkflow/DataObject'; 14 | 15 | describe('Properties Panel for a Process', function() { 16 | let xml = require('./bpmn/diagram.bpmn').default; 17 | let container; 18 | 19 | beforeEach(function() { 20 | container = TestContainer.get(this); 21 | }); 22 | 23 | beforeEach(bootstrapPropertiesPanel(xml, { 24 | container, 25 | debounceInput: false, 26 | additionalModules: [ 27 | dataObject, 28 | BpmnPropertiesPanelModule, 29 | BpmnPropertiesProviderModule, 30 | ], 31 | moddleExtensions: { 32 | spiffworkflow: spiffModdleExtension 33 | }, 34 | })); 35 | 36 | it('should allow you to edit the data objects', async function() { 37 | 38 | // IF - a process is selected 39 | await expectSelected('ProcessTest'); 40 | 41 | // THEN - there is a section where you can modify data objects. 42 | let entry = findGroupEntry('editDataObjects', container); 43 | expect(entry).to.exist; 44 | }); 45 | 46 | it('should be possible to change a data objects id', async function() { 47 | 48 | // IF - a process is selected and the id of a data object is changed 49 | const process_svg = await expectSelected('ProcessTest'); 50 | 51 | let newId = 'a_brand_new_id'; 52 | 53 | // ID here is [process id]-dataObj-[data object index]-id 54 | let myDataObjEntry = findEntry('ProcessTest-dataObj-0-id'); 55 | let textBox = findInput('text', myDataObjEntry); 56 | changeInput(textBox, newId); 57 | 58 | // THEN - there is a section where you can modify data objects. 59 | let dataObject = findDataObject(process_svg.businessObject, newId); 60 | expect(dataObject.id).to.equal(newId); 61 | }); 62 | 63 | it('should be possible to remove a data object', async function() { 64 | 65 | // IF - a process is selected and the delete button is clicked. 66 | const process_svg = await expectSelected('ProcessTest'); 67 | const data_id = 'my_data_object'; 68 | let dataObject = findDataObject(process_svg.businessObject, data_id); 69 | expect(dataObject).to.exist; 70 | let myDataObjEntry = findEntry('ProcessTest-dataObj-2'); 71 | let deleteButton = domQuery('.bio-properties-panel-remove-entry', myDataObjEntry); 72 | fireEvent.click(deleteButton); 73 | 74 | // THEN - there should not be a 'my_data_object' anymore. 75 | dataObject = findDataObject(process_svg.businessObject, data_id); 76 | expect(dataObject).to.not.exist; 77 | 78 | }); 79 | 80 | it('should be possible to add a data object', async function() { 81 | 82 | // IF - a process is selected and the add button is clicked. 83 | const process_svg = await expectSelected('ProcessTest'); 84 | let entry = findGroupEntry('editDataObjects', container); 85 | let addButton = domQuery('.bio-properties-panel-add-entry', entry); 86 | fireEvent.click(addButton); 87 | 88 | // THEN - there should now be 4 data objects instead of just 3. 89 | expect(findDataObjects(process_svg.businessObject).length).to.equal(4); 90 | 91 | }); 92 | 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /test/spec/ScriptTaskUnitTestSpec.js: -------------------------------------------------------------------------------- 1 | import TestContainer from 'mocha-test-container-support'; 2 | import { 3 | BpmnPropertiesPanelModule, 4 | BpmnPropertiesProviderModule, 5 | } from 'bpmn-js-properties-panel'; 6 | import { query as domQuery } from 'min-dom'; 7 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 8 | import { 9 | bootstrapPropertiesPanel, 10 | changeInput, 11 | expectSelected, 12 | findGroupEntry, 13 | } from './helpers'; 14 | import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; 15 | import callActivity from '../../app/spiffworkflow/callActivity'; 16 | 17 | describe('Call Activities should work', function () { 18 | const xml = require('./bpmn/call_activity.bpmn').default; 19 | let container; 20 | 21 | beforeEach(function () { 22 | container = TestContainer.get(this); 23 | }); 24 | 25 | beforeEach( 26 | bootstrapPropertiesPanel(xml, { 27 | container, 28 | debounceInput: false, 29 | additionalModules: [ 30 | callActivity, 31 | BpmnPropertiesPanelModule, 32 | BpmnPropertiesProviderModule, 33 | ], 34 | moddleExtensions: { 35 | spiffworkflow: spiffModdleExtension, 36 | }, 37 | }) 38 | ); 39 | 40 | it('should allow you to view the called element section of a Call Activity', async function () { 41 | const shapeElement = await expectSelected('the_call_activity'); 42 | expect(shapeElement, "Can't find Call Activity").to.exist; 43 | const entry = findGroupEntry('called_element', container); 44 | expect(entry).to.exist; 45 | }); 46 | 47 | it('should allow you to edit the called element section of a Call Activity', async function () { 48 | const shapeElement = await expectSelected('the_call_activity'); 49 | expect(shapeElement, "Can't find Call Activity").to.exist; 50 | const businessObject = getBusinessObject(shapeElement); 51 | expect(businessObject.get('calledElement')).to.equal('ProcessIdTBD1'); 52 | 53 | const entry = findGroupEntry('called_element', container); 54 | expect(entry).to.exist; 55 | 56 | const textInput = domQuery('input', entry); 57 | changeInput(textInput, 'newProcessId'); 58 | expect(businessObject.get('calledElement')).to.equal('newProcessId'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/spec/ScriptsPropsSpec.js: -------------------------------------------------------------------------------- 1 | import { query as domQuery } from 'min-dom'; 2 | 3 | import { 4 | BpmnPropertiesPanelModule, 5 | BpmnPropertiesProviderModule, 6 | } from 'bpmn-js-properties-panel'; 7 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 8 | import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; 9 | import { 10 | bootstrapPropertiesPanel, 11 | changeInput, 12 | expectSelected, 13 | findEntry, 14 | PROPERTIES_PANEL_CONTAINER, 15 | } from './helpers'; 16 | import extensions from '../../app/spiffworkflow/extensions'; 17 | 18 | describe('Properties Panel Script Tasks', function () { 19 | const xml = require('./bpmn/diagram.bpmn').default; 20 | 21 | beforeEach( 22 | bootstrapPropertiesPanel(xml, { 23 | debounceInput: false, 24 | additionalModules: [ 25 | extensions, 26 | BpmnPropertiesPanelModule, 27 | BpmnPropertiesProviderModule, 28 | ], 29 | moddleExtensions: { 30 | spiffworkflow: spiffModdleExtension, 31 | }, 32 | }) 33 | ); 34 | 35 | it('should display a script editing panel when a script task is selected', async function () { 36 | // IF - you select a script task 37 | expectSelected('my_script_task'); 38 | 39 | // THEN - a properties panel exists with a section for editing that script 40 | const entry = findEntry( 41 | 'pythonScript_bpmn:script', 42 | PROPERTIES_PANEL_CONTAINER 43 | ); 44 | expect(entry).to.exist; 45 | const scriptInput = domQuery('textarea', entry); 46 | expect(scriptInput).to.exist; 47 | }); 48 | 49 | it('should update the bpmn:script tag when you modify the script field', async function () { 50 | // IF - a script tag is selected, and you change the script in the properties panel 51 | const scriptTask = await expectSelected('my_script_task'); 52 | const entry = findEntry( 53 | 'pythonScript_bpmn:script', 54 | PROPERTIES_PANEL_CONTAINER 55 | ); 56 | const scriptInput = domQuery('textarea', entry); 57 | changeInput(scriptInput, 'x = 100'); 58 | 59 | // THEN - the script tag in the BPMN Business object / XML is updated as well. 60 | const businessObject = getBusinessObject(scriptTask); 61 | expect(businessObject.get('script')).to.equal('x = 100'); 62 | expect(scriptInput.value).to.equal('x = 100'); 63 | }); 64 | 65 | it('should parse the spiffworkflow:prescript tag when you open an existing file', async function () { 66 | await expectSelected('task_confirm'); 67 | const entry = findEntry( 68 | 'pythonScript_spiffworkflow:PreScript', 69 | PROPERTIES_PANEL_CONTAINER 70 | ); 71 | const scriptInput = domQuery('textarea', entry); 72 | expect(scriptInput.value).to.equal('x=1'); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/spec/ServiceTaskPropsSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | BpmnPropertiesPanelModule, 3 | BpmnPropertiesProviderModule, 4 | } from 'bpmn-js-properties-panel'; 5 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 6 | import TestContainer from 'mocha-test-container-support'; 7 | import { getBpmnJS } from 'bpmn-js/test/helper'; 8 | import spiffModdleExtension from '../../app/spiffworkflow/moddle/spiffworkflow.json'; 9 | import { 10 | bootstrapPropertiesPanel, 11 | changeInput, 12 | expectSelected, 13 | findEntry, 14 | findGroupEntry, 15 | findInput, findSelect, 16 | } from './helpers'; 17 | import extensions from '../../app/spiffworkflow/extensions'; 18 | 19 | describe('Properties Panel for Service Tasks', function () { 20 | const diagramXml = require('./bpmn/service.bpmn').default; 21 | let container; 22 | 23 | beforeEach(function () { 24 | container = TestContainer.get(this); 25 | }); 26 | 27 | function preparePropertiesPanelWithXml(xml) { 28 | return bootstrapPropertiesPanel(xml, { 29 | container, 30 | debounceInput: false, 31 | additionalModules: [ 32 | extensions, 33 | BpmnPropertiesPanelModule, 34 | BpmnPropertiesProviderModule, 35 | ], 36 | moddleExtensions: { 37 | spiffworkflow: spiffModdleExtension, 38 | }, 39 | }); 40 | } 41 | 42 | function addServicesToModeler(bpmnModeler) { 43 | /** 44 | * This will inject available services into the modeler which should be 45 | * available as a dropdown list when selecting which service you want to call. 46 | * 47 | */ 48 | bpmnModeler.on('spiff.service_tasks.requested', (event) => { 49 | event.eventBus.fire('spiff.service_tasks.returned', { 50 | serviceTaskOperators: [ 51 | { 52 | id: 'ExampleService', 53 | parameters: [ 54 | { 55 | id: 'name', 56 | type: 'string', 57 | }, 58 | ], 59 | }, 60 | { 61 | id: 'ExampleService2', 62 | parameters: [ 63 | { 64 | id: 'number', 65 | type: 'integer', 66 | }, 67 | ], 68 | }, 69 | ], 70 | }); 71 | }); 72 | } 73 | 74 | it('should display a panel for selecting service', async function () { 75 | await preparePropertiesPanelWithXml(diagramXml)(); 76 | 77 | const modeler = getBpmnJS(); 78 | addServicesToModeler(modeler); 79 | 80 | // IF - you select a service task 81 | const serviceTask = await expectSelected('my_service_task'); 82 | expect(serviceTask).to.exist; 83 | 84 | // THEN - a property panel exists with a section for editing web forms 85 | const group = findGroupEntry('service_task_properties', container); 86 | expect(group).to.exist; 87 | }); 88 | 89 | it('should display a list of services to select from.', async function () { 90 | await preparePropertiesPanelWithXml(diagramXml)(); 91 | const modeler = getBpmnJS(); 92 | addServicesToModeler(modeler); 93 | 94 | // IF - you select a service task 95 | const serviceTask = await expectSelected('my_service_task'); 96 | const group = findGroupEntry('service_task_properties', container); 97 | const entry = findEntry('selectOperatorId', group) 98 | 99 | // THEN - a select list appears and is populated by a list of known services 100 | const selectList = findSelect(entry); 101 | expect(selectList).to.exist; 102 | expect(selectList.options.length).to.equal(2) 103 | expect(selectList.options[0].label).to.equal('ExampleService') 104 | expect(selectList.options[1].label).to.equal('ExampleService2') 105 | }); 106 | 107 | 108 | 109 | }); 110 | -------------------------------------------------------------------------------- /test/spec/bpmn/call_activity.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_1rcteeq 6 | 7 | 8 | 9 | Flow_1rcteeq 10 | Flow_1rid3w7 11 | 12 | 13 | Flow_1rid3w7 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/spec/bpmn/data_objects_in_pools.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/spec/bpmn/data_store.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_0vt1twq 6 | 7 | 8 | Flow_1udyjxo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Flow_0vt1twq 26 | Flow_1udyjxo 27 | 28 | DataStoreReference_0eqeh4p 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /test/spec/bpmn/empty_diagram.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/spec/bpmn/io_variables.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | Flow_0vt1twq 13 | 14 | 15 | Flow_1oukz5y 16 | 17 | 18 | 19 | 20 | Flow_05w3wu8 21 | Flow_1oukz5y 22 | 23 | 24 | 25 | 26 | DataInput_0ab29sz 27 | 28 | 29 | DataOutput_1n1fg4r 30 | 31 | 32 | 33 | 35 | 36 | Flow_0vt1twq 37 | Flow_05w3wu8 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/spec/bpmn/script_task.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_10jwwqy 6 | 7 | 8 | 9 | Flow_0htxke7 10 | 11 | 12 | 13 | 14 | 15 | 16 | {"hey": false} 17 | {"hey": true} 18 | 19 | 20 | {} 21 | {"something_else": true} 22 | 23 | 24 | 25 | Flow_0niwe1y 26 | Flow_0htxke7 27 | if 'hey' in locals(): 28 | hey = True 29 | else: 30 | something_else = True 31 | 32 | 33 | 34 | Flow_10jwwqy 35 | Flow_0niwe1y 36 | hey = False 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/spec/bpmn/service.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_09rugkh 6 | 7 | 8 | 9 | Flow_0kql76n 10 | 11 | 12 | 13 | Flow_09rugkh 14 | Flow_0kql76n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/spec/bpmn/subprocess.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_0bk9zgm 6 | 7 | 8 | 9 | Flow_0bk9zgm 10 | Flow_1lzv56z 11 | 12 | 13 | 14 | Flow_1lzv56z 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/spec/bpmn/two_messages.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | old_name 10 | "a" 11 | 12 | varA 13 | 14 | 15 | 16 | 17 | 18 | old_name 19 | "b" 20 | 21 | varB 22 | 23 | 24 | 25 | 26 | 27 | 28 | old_exp 29 | 30 | 31 | old_exp 32 | 33 | 34 | 35 | old_name 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /test/spec/bpmn/user_form.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_0vt1twq 6 | 7 | 8 | Flow_1udyjxo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Flow_0vt1twq 26 | Flow_1udyjxo 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /test/spec/helpers.js: -------------------------------------------------------------------------------- 1 | import { query as domQuery } from 'min-dom'; 2 | import { act, fireEvent } from '@testing-library/preact'; 3 | 4 | import { 5 | getBpmnJS, 6 | bootstrapBpmnJS, 7 | inject, 8 | insertCSS, 9 | } from 'bpmn-js/test/helper'; 10 | import Modeler from 'bpmn-js/lib/Modeler'; 11 | import TestContainer from 'mocha-test-container-support'; 12 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 13 | import { createMoveEvent } from 'diagram-js/lib/features/mouse/Mouse'; 14 | 15 | export let PROPERTIES_PANEL_CONTAINER; 16 | export let CONTAINER; 17 | 18 | export function bootstrapPropertiesPanel(diagram, options, locals) { 19 | return async function () { 20 | let { container } = options; 21 | if (!container) { 22 | container = TestContainer.get(this); 23 | } 24 | CONTAINER = container; 25 | 26 | insertBpmnStyles(); 27 | const createModeler = bootstrapBpmnJS(Modeler, diagram, options, locals); 28 | await act(() => createModeler.call(this)); 29 | 30 | // (2) clean-up properties panel 31 | clearPropertiesPanelContainer(); 32 | 33 | // (3) attach properties panel 34 | const attachPropertiesPanel = inject(function (propertiesPanel) { 35 | PROPERTIES_PANEL_CONTAINER = document.createElement('div'); 36 | PROPERTIES_PANEL_CONTAINER.classList.add('properties-container'); 37 | 38 | container.appendChild(PROPERTIES_PANEL_CONTAINER); 39 | 40 | return act(() => propertiesPanel.attachTo(PROPERTIES_PANEL_CONTAINER)); 41 | }); 42 | await attachPropertiesPanel(); 43 | }; 44 | } 45 | 46 | export function clearPropertiesPanelContainer() { 47 | if (PROPERTIES_PANEL_CONTAINER) { 48 | PROPERTIES_PANEL_CONTAINER.remove(); 49 | } 50 | } 51 | 52 | export function insertBpmnStyles() { 53 | insertCSS( 54 | 'diagram.css', 55 | require('bpmn-js/dist/assets/diagram-js.css').default 56 | ); 57 | 58 | // @barmac: this fails before bpmn-js@9 59 | insertCSS('bpmn-js.css', require('bpmn-js/dist/assets/bpmn-js.css').default); 60 | 61 | insertCSS( 62 | 'bpmn-font.css', 63 | require('bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css').default 64 | ); 65 | } 66 | 67 | export function expectSelected(id) { 68 | return getBpmnJS().invoke(async function (elementRegistry, selection) { 69 | const element = elementRegistry.get(id); 70 | 71 | await act(() => { 72 | selection.select(element); 73 | }); 74 | 75 | return element; 76 | }); 77 | } 78 | 79 | export function getPropertiesPanel() { 80 | return PROPERTIES_PANEL_CONTAINER; 81 | } 82 | 83 | export function findEntry(id, container) { 84 | return domQuery(`[data-entry-id='${id}']`, container); 85 | } 86 | 87 | export function findGroupEntry(id, container) { 88 | return domQuery(`[data-group-id='group-${id}']`, container); 89 | } 90 | 91 | export function findInput(type, container) { 92 | return domQuery(`input[type='${type}']`, container); 93 | } 94 | 95 | export function findTextarea(id, container) { 96 | return domQuery(`textarea[id='${id}']`, container); 97 | } 98 | 99 | export function findButton(id, container) { 100 | return domQuery(`button[id='${id}']`, container); 101 | } 102 | 103 | export function findButtonByClass(buttonClass, container) { 104 | return domQuery(`button[class='${buttonClass}']`, container); 105 | } 106 | 107 | export function findSelect(container) { 108 | return domQuery('select', container); 109 | } 110 | 111 | export function changeInput(input, value) { 112 | fireEvent.input(input, { target: { value } }); 113 | } 114 | 115 | export function pressButton(button) { 116 | fireEvent.click(button); 117 | } 118 | 119 | export function findDivByClass(divClass, container) { 120 | return domQuery(`div[class='${divClass}']`, container); 121 | } 122 | 123 | /** 124 | * Drags an element from the palette onto the canvas. 125 | * @param id 126 | */ 127 | export function triggerPaletteEntry(id) { 128 | getBpmnJS().invoke(function (palette) { 129 | const entry = palette.getEntries()[id]; 130 | 131 | if (entry && entry.action && entry.action.click) { 132 | entry.action.click(createMoveEvent(0, 0)); 133 | } 134 | }); 135 | } 136 | -------------------------------------------------------------------------------- /test/spec/test.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&display=swap'); 2 | 3 | html, body, .modeler-container, .modeler-container > div { 4 | height: 100%; 5 | margin: 0; 6 | font-family: 'IBM Plex Sans', sans-serif; 7 | } 8 | 9 | .test-container { 10 | height: 800px !important; 11 | } 12 | 13 | .test-content-container { 14 | width: 100%; 15 | height: calc(100% - 24px) !important; 16 | display: flex; 17 | flex: 1; 18 | flex-direction: row; 19 | } 20 | 21 | .modeler-container { 22 | flex: 1; 23 | position: relative; 24 | } 25 | 26 | .modeler-container, .properties-container { 27 | overflow-y: auto; 28 | } 29 | 30 | .properties-container { 31 | position: relative; 32 | flex: none; 33 | height: 100%; 34 | width: 300px; 35 | border-left: solid 1px #cccccc; 36 | } 37 | 38 | .properties-container .bio-properties-panel { 39 | --font-family: 'IBM Plex Sans', sans-serif !important; 40 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: ['./app/app.js'], 6 | }, 7 | output: { 8 | path: `${__dirname}/public`, 9 | filename: 'app.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.m?jsx?$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: 'babel-loader', 18 | options: { 19 | plugins: [ 20 | [ 21 | '@babel/plugin-transform-react-jsx', 22 | { 23 | importSource: '@bpmn-io/properties-panel/preact', 24 | runtime: 'automatic', 25 | }, 26 | ], 27 | ], 28 | }, 29 | }, 30 | }, 31 | { 32 | test: /\.bpmn$/, 33 | use: 'raw-loader', 34 | }, 35 | ], 36 | }, 37 | resolve: { 38 | extensions: ['.js', '.jsx'], 39 | }, 40 | plugins: [ 41 | new CopyWebpackPlugin({ 42 | patterns: [ 43 | { 44 | from: 'assets/**', 45 | to: 'vendor/bpmn-js', 46 | context: 'node_modules/bpmn-js/dist/', 47 | }, 48 | { 49 | from: 'assets/**', 50 | to: 'vendor/bpmn-js-properties-panel', 51 | context: 'node_modules/@bpmn-io/properties-panel/dist/', 52 | }, 53 | { from: '**/*.{html,css}', context: 'app/' }, 54 | ], 55 | }), 56 | ], 57 | mode: 'development', 58 | devtool: 'source-map', 59 | }; 60 | --------------------------------------------------------------------------------