diff options
Diffstat (limited to 'workflow-designer-ui')
32 files changed, 1294 insertions, 55 deletions
diff --git a/workflow-designer-ui/src/main/frontend/.gitignore b/workflow-designer-ui/src/main/frontend/.gitignore index d2d69816..3491fa20 100644 --- a/workflow-designer-ui/src/main/frontend/.gitignore +++ b/workflow-designer-ui/src/main/frontend/.gitignore @@ -4,7 +4,7 @@ /node_modules /node-install .idea/ - +.vscode/ # testing /coverage diff --git a/workflow-designer-ui/src/main/frontend/index.html b/workflow-designer-ui/src/main/frontend/index.html index c3a6f3b6..09d2d0a9 100644 --- a/workflow-designer-ui/src/main/frontend/index.html +++ b/workflow-designer-ui/src/main/frontend/index.html @@ -2,9 +2,7 @@ <html> <head> <base href="/"> - <meta charset="utf-8"> - <link rel="stylesheet" href="https://unpkg.com/bpmn-js@2.1.0/dist/assets/diagram-js.css" /> - <link rel="stylesheet" href="https://unpkg.com/bpmn-js@2.1.0/dist/assets/bpmn-font/css/bpmn.css" /> + <meta charset="utf-8"> <title>SDC Workflow App</title> </head> <body> diff --git a/workflow-designer-ui/src/main/frontend/package.json b/workflow-designer-ui/src/main/frontend/package.json index 529d9096..bd224ba2 100644 --- a/workflow-designer-ui/src/main/frontend/package.json +++ b/workflow-designer-ui/src/main/frontend/package.json @@ -23,6 +23,7 @@ "dateformat": "^3.0.3", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", + "file-saver": "^1.3.8", "http-proxy-middleware": "^0.17.4", "lodash": "^3.0.1", "md5": "^2.2.1", diff --git a/workflow-designer-ui/src/main/frontend/resources/scss/components/_notifications.scss b/workflow-designer-ui/src/main/frontend/resources/scss/components/_notifications.scss index cc44cfbc..855c372e 100644 --- a/workflow-designer-ui/src/main/frontend/resources/scss/components/_notifications.scss +++ b/workflow-designer-ui/src/main/frontend/resources/scss/components/_notifications.scss @@ -1,5 +1,6 @@ .workflow-notifications-container { position: absolute; + z-index: 99999; &.position-top-right { right: 30px; top: 50px; diff --git a/workflow-designer-ui/src/main/frontend/resources/scss/features/_composition.scss b/workflow-designer-ui/src/main/frontend/resources/scss/features/_composition.scss index 7ab294a2..a159a4b7 100644 --- a/workflow-designer-ui/src/main/frontend/resources/scss/features/_composition.scss +++ b/workflow-designer-ui/src/main/frontend/resources/scss/features/_composition.scss @@ -4,12 +4,43 @@ .bpmn-container { flex-basis: 100%; - height: 100%; + flex-grow: 1 } - - .properties-panel { - &, .bpp-properties-panel { - height: 100%; + .bpmn-sidebar { + height: 100%; + .properties-panel { + &, .bpp-properties-panel { + height: 100%; + } + } + .composition-buttons { + position: fixed; + background-color: #fafafa; + left: 265px; + bottom: 46px; + border: 1px solid lightgray; + width: 189px; + display: flex; + flex-direction: row; + justify-content: space-around; + height: 57px; + align-items: center; + padding: 10px; + .divider { + height: 35px; + border: 1px solid $silver; + } + .diagram-btn { + + &:hover { + fill: $blue; + cursor: pointer; + } + .svg-icon { + width: 25px; + height: 23px; + } + } } } } diff --git a/workflow-designer-ui/src/main/frontend/resources/scss/style.scss b/workflow-designer-ui/src/main/frontend/resources/scss/style.scss index 95828ae2..49278565 100644 --- a/workflow-designer-ui/src/main/frontend/resources/scss/style.scss +++ b/workflow-designer-ui/src/main/frontend/resources/scss/style.scss @@ -1,3 +1,5 @@ +@import '../../node_modules/bpmn-js/dist/assets/diagram-js.css'; +@import '../../node_modules/bpmn-js/dist/assets/bpmn-font/css/bpmn.css'; @import 'common'; @import '../../node_modules/sdc-ui/lib/css/style.css'; @import 'components'; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js new file mode 100644 index 00000000..d2c273c4 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js @@ -0,0 +1,47 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { connect } from 'react-redux'; +import { I18n } from 'react-redux-i18n'; +import { updateComposition } from './compositionActions'; +import CompositionView from './CompositionView'; +import { showErrorModalAction } from '../../../shared/modal/modalWrapperActions'; +import { getComposition } from './compositionSelectors'; +import { getWorkflowName } from '../../workflow/workflowSelectors'; + +function mapStateToProps(state) { + return { + composition: getComposition(state), + name: getWorkflowName(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + compositionUpdate: composition => + dispatch(updateComposition(composition)), + showErrorModal: msg => + dispatch( + showErrorModalAction({ + title: I18n.t('workflow.composition.bpmnError'), + body: msg, + withButtons: true, + closeButtonText: 'Ok' + }) + ) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(CompositionView); diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js index ba0351b6..d549456f 100644 --- a/workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 +*http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,78 +14,151 @@ * limitations under the License. */ import React, { Component } from 'react'; -import { connect } from 'react-redux'; - -import BpmnModeler from 'bpmn-js/lib/Modeler'; -// import propertiesPanelModule from 'bpmn-js-properties-panel'; +import fileSaver from 'file-saver'; +import CustomModeler from './custom-modeler'; +import propertiesPanelModule from 'bpmn-js-properties-panel'; import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'; -import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda'; - +import camundaModuleDescriptor from 'camunda-bpmn-moddle/resources/camunda'; import newDiagramXML from './newDiagram.bpmn'; +import PropTypes from 'prop-types'; +import CompositionButtons from './components/CompositionButtonsPanel'; class CompositionView extends Component { + static propTypes = { + compositionUpdate: PropTypes.func, + showErrorModal: PropTypes.func, + composition: PropTypes.string, + name: PropTypes.string + }; constructor() { super(); this.generatedId = 'bpmn-container' + Date.now(); + this.fileInput = React.createRef(); + this.state = { + diagram: false + }; } componentDidMount() { - this.modeler = new BpmnModeler({ + const { composition } = this.props; + + this.modeler = new CustomModeler({ propertiesPanel: { - parent: '#js-properties-navigationSideBar' + parent: '#js-properties-panel' }, additionalModules: [ - //TODO:: need to fix - // propertiesPanelModule, + propertiesPanelModule, propertiesProviderModule ], moddleExtensions: { - camunda: camundaModdleDescriptor + camunda: camundaModuleDescriptor } }); window.modeler = this.modeler; this.modeler.attachTo('#' + this.generatedId); - // let diagramXML = - // '<?xml version="1.0" encoding="UTF-8"?>\r\n<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"\r\n xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"\r\n xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"\r\n xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"\r\n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\r\n expressionLanguage="http://www.w3.org/1999/XPath"\r\n typeLanguage="http://www.w3.org/2001/XMLSchema"\r\n targetNamespace=""\r\n xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">\r\n<collaboration id="sid-c0e745ff-361e-4afb-8c8d-2a1fc32b1424">\r\n <participant id="sid-87F4C1D6-25E1-4A45-9DA7-AD945993D06F" name="Customer" processRef="sid-C3803939-0872-457F-8336-EAE484DC4A04">\r\n </participant>\r\n</collaboration>\r\n<process id="sid-C3803939-0872-457F-8336-EAE484DC4A04" isClosed="false" isExecutable="false" name="Customer" processType="None">\r\n <extensionElements/>\r\n <laneSet id="sid-b167d0d7-e761-4636-9200-76b7f0e8e83a">\r\n <lane id="sid-57E4FE0D-18E4-478D-BC5D-B15164E93254">\r\n <flowNodeRef>sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138</flowNodeRef>\r\n <flowNodeRef>sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26</flowNodeRef>\r\n <flowNodeRef>SCAN_OK</flowNodeRef>\r\n <flowNodeRef>sid-E49425CF-8287-4798-B622-D2A7D78EF00B</flowNodeRef>\r\n <flowNodeRef>sid-E433566C-2289-4BEB-A19C-1697048900D2</flowNodeRef>\r\n <flowNodeRef>sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9</flowNodeRef>\r\n </lane>\r\n </laneSet>\r\n <startEvent id="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138" name="Notices QR code">\r\n <outgoing>sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD</outgoing>\r\n </startEvent>\r\n <task completionQuantity="1" id="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26" isForCompensation="false" name="Scan QR code" startQuantity="1">\r\n <incoming>sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D</incoming>\r\n <outgoing>sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A</outgoing>\r\n </task>\r\n <exclusiveGateway gatewayDirection="Diverging" id="SCAN_OK" name="Scan successful? ">\r\n <incoming>sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A</incoming>\r\n <outgoing>sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB</outgoing>\r\n <outgoing>sid-337A23B9-A923-4CCE-B613-3E247B773CCE</outgoing>\r\n </exclusiveGateway>\r\n <task completionQuantity="1" id="sid-E49425CF-8287-4798-B622-D2A7D78EF00B" isForCompensation="false" name="Open product information in mobile app" startQuantity="1">\r\n <incoming>sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB</incoming>\r\n <outgoing>sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C</outgoing>\r\n </task>\r\n <endEvent id="sid-E433566C-2289-4BEB-A19C-1697048900D2" name="Is informed">\r\n <incoming>sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C</incoming>\r\n </endEvent>\r\n <exclusiveGateway gatewayDirection="Converging" id="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9">\r\n <incoming>sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD</incoming>\r\n <incoming>sid-337A23B9-A923-4CCE-B613-3E247B773CCE</incoming>\r\n <outgoing>sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D</outgoing>\r\n </exclusiveGateway>\r\n <sequenceFlow id="sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD" sourceRef="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138" targetRef="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9"/>\r\n <sequenceFlow id="sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A" sourceRef="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26" targetRef="SCAN_OK"/>\r\n <sequenceFlow id="sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C" sourceRef="sid-E49425CF-8287-4798-B622-D2A7D78EF00B" targetRef="sid-E433566C-2289-4BEB-A19C-1697048900D2"/>\r\n <sequenceFlow id="sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB" name="No" sourceRef="SCAN_OK" targetRef="sid-E49425CF-8287-4798-B622-D2A7D78EF00B"/>\r\n <sequenceFlow id="sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D" sourceRef="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9" targetRef="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26"/>\r\n <sequenceFlow id="sid-337A23B9-A923-4CCE-B613-3E247B773CCE" name="Yes" sourceRef="SCAN_OK" targetRef="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9"/>\r\n</process>\r\n<bpmndi:BPMNDiagram id="sid-74620812-92c4-44e5-949c-aa47393d3830">\r\n <bpmndi:BPMNPlane bpmnElement="sid-c0e745ff-361e-4afb-8c8d-2a1fc32b1424" id="sid-cdcae759-2af7-4a6d-bd02-53f3352a731d">\r\n <bpmndi:BPMNShape bpmnElement="sid-87F4C1D6-25E1-4A45-9DA7-AD945993D06F" id="sid-87F4C1D6-25E1-4A45-9DA7-AD945993D06F_gui" isHorizontal="true">\r\n <omgdc:Bounds height="500.0" width="933.0" x="42.5" y="75.0"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n <omgdc:Bounds height="59.142852783203125" width="12.000000000000014" x="47.49999999999999" y="170.42857360839844"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNShape bpmnElement="sid-57E4FE0D-18E4-478D-BC5D-B15164E93254" id="sid-57E4FE0D-18E4-478D-BC5D-B15164E93254_gui" isHorizontal="true">\r\n <omgdc:Bounds height="250.0" width="903.0" x="72.5" y="75.0"/>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNShape bpmnElement="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138" id="sid-D7F237E8-56D0-4283-A3CE-4F0EFE446138_gui">\r\n <omgdc:Bounds height="30.0" width="30.0" x="150.0" y="165.0"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n <omgdc:Bounds height="22.0" width="46.35714340209961" x="141.8214282989502" y="197.0"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNShape bpmnElement="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26" id="sid-52EB1772-F36E-433E-8F5B-D5DFD26E6F26_gui">\r\n <omgdc:Bounds height="80.0" width="100.0" x="352.5" y="140.0"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n <omgdc:Bounds height="12.0" width="84.0" x="360.5" y="172.0"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNShape bpmnElement="SCAN_OK" id="SCAN_OK_gui" isMarkerVisible="true">\r\n <omgdc:Bounds height="40.0" width="40.0" x="550.0" y="160.0"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n <omgdc:Bounds height="12.0" width="102.0" x="521.0" y="127.0"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNShape bpmnElement="sid-E49425CF-8287-4798-B622-D2A7D78EF00B" id="sid-E49425CF-8287-4798-B622-D2A7D78EF00B_gui">\r\n <omgdc:Bounds height="80.0" width="100.0" x="687.5" y="140.0"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n <omgdc:Bounds height="36.0" width="83.14285278320312" x="695.9285736083984" y="162.0"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNShape bpmnElement="sid-E433566C-2289-4BEB-A19C-1697048900D2" id="sid-E433566C-2289-4BEB-A19C-1697048900D2_gui">\r\n <omgdc:Bounds height="28.0" width="28.0" x="865.0" y="166.0"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n <omgdc:Bounds height="11.0" width="62.857147216796875" x="847.5714263916016" y="196.0"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNShape bpmnElement="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9" id="sid-5134932A-1863-4FFA-BB3C-A4B4078B11A9_gui" isMarkerVisible="true">\r\n <omgdc:Bounds height="40.0" width="40.0" x="240.0" y="160.0"/>\r\n </bpmndi:BPMNShape>\r\n <bpmndi:BPMNEdge bpmnElement="sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A" id="sid-EE8A7BA0-5D66-4F8B-80E3-CC2751B3856A_gui">\r\n <omgdi:waypoint x="452.5" y="180"/>\r\n <omgdi:waypoint x="550.0" y="180"/>\r\n </bpmndi:BPMNEdge>\r\n <bpmndi:BPMNEdge bpmnElement="sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB" id="sid-8B820AF5-DC5C-4618-B854-E08B71FB55CB_gui">\r\n <omgdi:waypoint x="590.0" y="180"/>\r\n <omgdi:waypoint x="687.5" y="180"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n <omgdc:Bounds height="12.048704338048935" width="16.32155963195521" x="597.8850936986571" y="155"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNEdge>\r\n <bpmndi:BPMNEdge bpmnElement="sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD" id="sid-7B791A11-2F2E-4D80-AFB3-91A02CF2B4FD_gui">\r\n <omgdi:waypoint x="180.0" y="180"/>\r\n <omgdi:waypoint x="240.0" y="180"/>\r\n </bpmndi:BPMNEdge>\r\n <bpmndi:BPMNEdge bpmnElement="sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D" id="sid-4DC479E5-5C20-4948-BCFC-9EC5E2F66D8D_gui">\r\n <omgdi:waypoint x="280.0" y="180"/>\r\n <omgdi:waypoint x="352.5" y="180"/>\r\n </bpmndi:BPMNEdge>\r\n <bpmndi:BPMNEdge bpmnElement="sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C" id="sid-57EB1F24-BD94-479A-BF1F-57F1EAA19C6C_gui">\r\n <omgdi:waypoint x="787.5" y="180.0"/>\r\n <omgdi:waypoint x="865.0" y="180.0"/>\r\n </bpmndi:BPMNEdge>\r\n <bpmndi:BPMNEdge bpmnElement="sid-337A23B9-A923-4CCE-B613-3E247B773CCE" id="sid-337A23B9-A923-4CCE-B613-3E247B773CCE_gui">\r\n <omgdi:waypoint x="570.5" y="200.0"/>\r\n <omgdi:waypoint x="570.5" y="269.0"/>\r\n <omgdi:waypoint x="260.5" y="269.0"/>\r\n <omgdi:waypoint x="260.5" y="200.0"/>\r\n <bpmndi:BPMNLabel labelStyle="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n <omgdc:Bounds height="21.4285888671875" width="12.0" x="550" y="205"/>\r\n </bpmndi:BPMNLabel>\r\n </bpmndi:BPMNEdge>\r\n </bpmndi:BPMNPlane>\r\n <bpmndi:BPMNLabelStyle id="sid-e0502d32-f8d1-41cf-9c4a-cbb49fecf581">\r\n <omgdc:Font isBold="false" isItalic="false" isStrikeThrough="false" isUnderline="false" name="Arial" size="11.0"/>\r\n </bpmndi:BPMNLabelStyle>\r\n <bpmndi:BPMNLabelStyle id="sid-84cb49fd-2f7c-44fb-8950-83c3fa153d3b">\r\n <omgdc:Font isBold="false" isItalic="false" isStrikeThrough="false" isUnderline="false" name="Arial" size="12.0"/>\r\n </bpmndi:BPMNLabelStyle>\r\n</bpmndi:BPMNDiagram>\r\n</definitions>\r\n\r\n'; - this.importXML(newDiagramXML); + this.setDiagram(composition ? composition : newDiagramXML); + var eventBus = this.modeler.get('eventBus'); + eventBus.on('element.out', () => { + this.exportDiagramToStore(); + }); } - importXML(xml) { + setDiagram = diagram => { + this.setState( + { + diagram + }, + this.importXML + ); + }; + + importXML = () => { + const { diagram } = this.state; let modeler = this.modeler; - this.modeler.importXML(xml, function(err) { + this.modeler.importXML(diagram, err => { if (err) { - return console.error('could not import BPMN file'); + //TDOD add i18n + return this.props.showErrorModal('could not import diagram'); } let canvas = modeler.get('canvas'); canvas.zoom('fit-viewport'); }); - } + }; - exportDiagram() { + exportDiagramToStore = () => { this.modeler.saveXML({ format: true }, (err, xml) => { if (err) { - return console.error('could not save diagram'); + //TODO add i18n + return this.props.showErrorModal('could not save diagram'); } - console.log('Exported diagram: ', xml); + return this.props.compositionUpdate(xml); }); - } + }; + + exportDiagram = () => { + const { name, showErrorModal } = this.props; + this.modeler.saveXML({ format: true }, (err, xml) => { + if (err) { + //TODO add i18n + return showErrorModal('could not save diagram'); + } + const blob = new Blob([xml], { type: 'text/html;charset=utf-8' }); + fileSaver.saveAs(blob, `${name}-diagram.bpmn`); + }); + }; + + loadNewDiagram = () => { + this.setDiagram(newDiagramXML); + }; + + uploadDiagram = () => { + this.fileInput.current.click(); + }; + + handleFileInputChange = filesList => { + const file = filesList[0]; + const reader = new FileReader(); + reader.onloadend = event => { + var xml = event.target.result; + this.setDiagram(xml); + this.fileInput.value = ''; + }; + reader.readAsText(file); + }; render() { return ( <div className="composition-view content"> - <div className="bpmn-container" id={this.generatedId} /> - <div className="properties-panel" id="js-properties-panel" /> + <input + ref={this.fileInput} + onChange={e => this.handleFileInputChange(e.target.files)} + id="file-input" + accept=".bpmn, .xml" + type="file" + name="file-input" + style={{ display: 'none' }} + /> + <div + onBlur={() => { + this.exportDiagramToStore(); + }} + className="bpmn-container" + id={this.generatedId} + /> + <div className="bpmn-sidebar"> + <div + className="properties-panel" + id="js-properties-panel" + /> + <CompositionButtons + onClean={this.loadNewDiagram} + onDownload={this.exportDiagram} + onUpload={this.uploadDiagram} + /> + </div> </div> ); } } -function mapStateToProps() { - return {}; -} - -function mapDispatchToProps() { - return {}; -} - -export default connect(mapStateToProps, mapDispatchToProps)(CompositionView); +export default CompositionView; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js new file mode 100644 index 00000000..2fc6618c --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js @@ -0,0 +1,33 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import SVGIcon from 'sdc-ui/lib/react/SVGIcon'; + +const CompositionButton = ({ onClick, name, title }) => ( + <div onClick={onClick} className="diagram-btn"> + <SVGIcon title={title} name={name} /> + </div> +); + +CompositionButton.propTypes = { + onClick: PropTypes.func, + className: PropTypes.string, + name: PropTypes.string, + title: PropTypes.string +}; + +export default CompositionButton; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js new file mode 100644 index 00000000..add64902 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js @@ -0,0 +1,52 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import CompositionButton from './CompositionButton'; + +const Divider = () => <div className="divider" />; + +const CompositionButtons = ({ onClean, onUpload, onDownload }) => ( + <div className="composition-buttons"> + <CompositionButton + data-test-id="composition-clear-btn" + onClick={onClean} + name="trashO" + title="clear" + /> + <Divider /> + <CompositionButton + data-test-id="composition-download-btn" + onClick={onDownload} + name="download" + title="download" + /> + <Divider /> + <CompositionButton + data-test-id="composition-download-upload" + onClick={onUpload} + name="upload" + title="upload" + /> + </div> +); + +CompositionButtons.propTypes = { + onClean: PropTypes.func, + onUpload: PropTypes.func, + onDownload: PropTypes.func +}; +export default CompositionButtons; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js new file mode 100644 index 00000000..3f1755dd --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js @@ -0,0 +1,21 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { SET_COMPOSITION } from './compositionConstants'; + +export const updateComposition = payload => ({ + type: SET_COMPOSITION, + payload +}); diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js new file mode 100644 index 00000000..74cab0cb --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js @@ -0,0 +1,16 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const SET_COMPOSITION = 'composition/SET_COMPOSITION'; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js new file mode 100644 index 00000000..9c707362 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js @@ -0,0 +1,27 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { SET_COMPOSITION } from './compositionConstants'; + +export default (state = {}, action) => { + switch (action.type) { + case SET_COMPOSITION: + return { + diagram: action.payload + }; + default: + return state; + } +}; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js new file mode 100644 index 00000000..7e28ca64 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js @@ -0,0 +1,17 @@ +/* +* Copyright © 2018 European Support Limited +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +*http://www.apache.org/licenses/LICENSE-2.0 +* + * Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +export const getComposition = state => + state && state.currentVersion && state.currentVersion.composition.diagram; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js new file mode 100644 index 00000000..0f2ba528 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js @@ -0,0 +1,43 @@ +import inherits from 'inherits';
+
+import ContextPadProvider from 'bpmn-js/lib/features/context-pad/ContextPadProvider';
+
+import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';
+
+import { assign, bind } from 'min-dash';
+
+export default function CustomContextPadProvider(injector, connect, translate) {
+ injector.invoke(ContextPadProvider, this);
+
+ var cached = bind(this.getContextPadEntries, this);
+
+ this.getContextPadEntries = function(element) {
+ var actions = cached(element);
+
+ var businessObject = element.businessObject;
+
+ function startConnect(event, element, autoActivate) {
+ connect.start(event, element, autoActivate);
+ }
+
+ if (isAny(businessObject, ['custom:triangle', 'custom:circle'])) {
+ assign(actions, {
+ connect: {
+ group: 'connect',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Connect using custom connection'),
+ action: {
+ click: startConnect,
+ dragstart: startConnect
+ }
+ }
+ });
+ }
+
+ return actions;
+ };
+}
+
+inherits(CustomContextPadProvider, ContextPadProvider);
+
+CustomContextPadProvider.$inject = ['injector', 'connect', 'translate'];
diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js new file mode 100644 index 00000000..01d4d278 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js @@ -0,0 +1,101 @@ +import { assign } from 'min-dash'; + +import inherits from 'inherits'; + +import BpmnElementFactory from 'bpmn-js/lib/features/modeling/ElementFactory'; +import { DEFAULT_LABEL_SIZE } from 'bpmn-js/lib/util/LabelUtil'; + +/** + * A custom factory that knows how to create BPMN _and_ custom elements. + */ +export default function CustomElementFactory(bpmnFactory, moddle) { + BpmnElementFactory.call(this, bpmnFactory, moddle); + + var self = this; + + /** + * Create a diagram-js element with the given type (any of shape, connection, label). + * + * @param {String} elementType + * @param {Object} attrs + * + * @return {djs.model.Base} + */ + this.create = function(elementType, attrs) { + var type = attrs.type; + + if (elementType === 'label') { + return self.baseCreate( + elementType, + assign({ type: 'label' }, DEFAULT_LABEL_SIZE, attrs) + ); + } + + // add type to businessObject if custom + if (/^custom:/.test(type)) { + if (!attrs.businessObject) { + attrs.businessObject = { + type: type + }; + + if (attrs.id) { + assign(attrs.businessObject, { + id: attrs.id + }); + } + } + + // add width and height if shape + if (!/:connection$/.test(type)) { + assign(attrs, self._getCustomElementSize(type)); + } + + if (!('$instanceOf' in attrs.businessObject)) { + // ensure we can use ModelUtil#is for type checks + Object.defineProperty(attrs.businessObject, '$instanceOf', { + value: function(type) { + return this.type === type; + } + }); + } + + return self.baseCreate(elementType, attrs); + } + + return self.createBpmnElement(elementType, attrs); + }; +} + +inherits(CustomElementFactory, BpmnElementFactory); + +CustomElementFactory.$inject = ['bpmnFactory', 'moddle']; + +/** + * Returns the default size of custom shapes. + * + * The following example shows an interface on how + * to setup the custom shapes's dimensions. + * + * @example + * + * var shapes = { + * triangle: { width: 40, height: 40 }, + * rectangle: { width: 100, height: 20 } + * }; + * + * return shapes[type]; + * + * + * @param {String} type + * + * @return {Dimensions} a {width, height} object representing the size of the element + */ +CustomElementFactory.prototype._getCustomElementSize = function(type) { + var shapes = { + __default: { width: 100, height: 80 }, + 'custom:triangle': { width: 40, height: 40 }, + 'custom:circle': { width: 140, height: 140 } + }; + + return shapes[type] || shapes.__default; +}; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js new file mode 100644 index 00000000..a8adb2fd --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js @@ -0,0 +1,151 @@ +import { assign } from 'min-dash'; + +/** + * A palette that allows you to create BPMN _and_ custom elements. + */ +export default function PaletteProvider( + palette, + create, + elementFactory, + spaceTool, + lassoTool, + handTool, + globalConnect, + translate +) { + this._create = create; + this._elementFactory = elementFactory; + this._spaceTool = spaceTool; + this._lassoTool = lassoTool; + this._handTool = handTool; + this._globalConnect = globalConnect; + this._translate = translate; + + palette.registerProvider(this); +} + +PaletteProvider.$inject = [ + 'palette', + 'create', + 'elementFactory', + 'spaceTool', + 'lassoTool', + 'handTool', + 'globalConnect', + 'translate' +]; + +PaletteProvider.prototype.getPaletteEntries = function() { + var actions = {}, + create = this._create, + elementFactory = this._elementFactory, + spaceTool = this._spaceTool, + lassoTool = this._lassoTool, + handTool = this._handTool, + globalConnect = this._globalConnect, + translate = this._translate; + + function createAction(type, group, className, title, options) { + function createListener(event) { + var shape = elementFactory.createShape( + assign({ type: type }, options) + ); + + if (options) { + shape.businessObject.di.isExpanded = options.isExpanded; + } + + create.start(event, shape); + } + + var shortType = type.replace(/^bpmn:/, ''); + + return { + group: group, + className: className, + title: title || 'Create ' + shortType, + action: { + dragstart: createListener, + click: createListener + } + }; + } + + assign(actions, { + 'hand-tool': { + group: 'tools', + className: 'bpmn-icon-hand-tool', + title: translate('Activate the hand tool'), + action: { + click: function(event) { + handTool.activateHand(event); + } + } + }, + 'lasso-tool': { + group: 'tools', + className: 'bpmn-icon-lasso-tool', + title: translate('Activate the lasso tool'), + action: { + click: function(event) { + lassoTool.activateSelection(event); + } + } + }, + 'space-tool': { + group: 'tools', + className: 'bpmn-icon-space-tool', + title: translate('Activate the create/remove space tool'), + action: { + click: function(event) { + spaceTool.activateSelection(event); + } + } + }, + 'global-connect-tool': { + group: 'tools', + className: 'bpmn-icon-connection-multi', + title: translate('Activate the global connect tool'), + action: { + click: function(event) { + globalConnect.toggle(event); + } + } + }, + 'tool-separator': { + group: 'tools', + separator: true + }, + 'create.start-event': createAction( + 'bpmn:StartEvent', + 'event', + 'bpmn-icon-start-event-none' + ), + 'create.intermediate-event': createAction( + 'bpmn:IntermediateThrowEvent', + 'event', + 'bpmn-icon-intermediate-event-none', + translate('Create Intermediate/Boundary Event') + ), + 'create.end-event': createAction( + 'bpmn:EndEvent', + 'event', + 'bpmn-icon-end-event-none' + ), + 'create.exclusive-gateway': createAction( + 'bpmn:ExclusiveGateway', + 'gateway', + 'bpmn-icon-gateway-none', + translate('Create Gateway') + ), + 'create.task': createAction('bpmn:Task', 'activity', 'bpmn-icon-task'), + 'create.subprocess-expanded': createAction( + 'bpmn:SubProcess', + 'activity', + 'bpmn-icon-subprocess-expanded', + translate('Create expanded SubProcess'), + { isExpanded: true } + ) + }); + return actions; +}; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js new file mode 100644 index 00000000..f397fed9 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js @@ -0,0 +1,176 @@ +import inherits from 'inherits'; + +import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; + +import { componentsToPath, createLine } from 'diagram-js/lib/util/RenderUtil'; + +import { + append as svgAppend, + attr as svgAttr, + create as svgCreate +} from 'tiny-svg'; + +/** + * A renderer that knows how to render custom elements. + */ +export default function CustomRenderer(eventBus, styles) { + BaseRenderer.call(this, eventBus, 2000); + + var computeStyle = styles.computeStyle; + + this.drawTriangle = function(p, side) { + var halfSide = side / 2, + points, + attrs; + + points = [halfSide, 0, side, side, 0, side]; + + attrs = computeStyle(attrs, { + stroke: '#3CAA82', + strokeWidth: 2, + fill: '#3CAA82' + }); + + var polygon = svgCreate('polygon'); + + svgAttr(polygon, { + points: points + }); + + svgAttr(polygon, attrs); + + svgAppend(p, polygon); + + return polygon; + }; + + this.getTrianglePath = function(element) { + var x = element.x, + y = element.y, + width = element.width, + height = element.height; + + var trianglePath = [ + ['M', x + width / 2, y], + ['l', width / 2, height], + ['l', -width, 0], + ['z'] + ]; + + return componentsToPath(trianglePath); + }; + + this.drawCircle = function(p, width, height) { + var cx = width / 2, + cy = height / 2; + + var attrs = computeStyle(attrs, { + stroke: '#4488aa', + strokeWidth: 4, + fill: 'white' + }); + + var circle = svgCreate('circle'); + + svgAttr(circle, { + cx: cx, + cy: cy, + r: Math.round((width + height) / 4) + }); + + svgAttr(circle, attrs); + + svgAppend(p, circle); + + return circle; + }; + + this.getCirclePath = function(shape) { + var cx = shape.x + shape.width / 2, + cy = shape.y + shape.height / 2, + radius = shape.width / 2; + + var circlePath = [ + ['M', cx, cy], + ['m', 0, -radius], + ['a', radius, radius, 0, 1, 1, 0, 2 * radius], + ['a', radius, radius, 0, 1, 1, 0, -2 * radius], + ['z'] + ]; + + return componentsToPath(circlePath); + }; + + this.drawCustomConnection = function(p, element) { + var attrs = computeStyle(attrs, { + stroke: '#ff471a', + strokeWidth: 2 + }); + + return svgAppend(p, createLine(element.waypoints, attrs)); + }; + + this.getCustomConnectionPath = function(connection) { + var waypoints = connection.waypoints.map(function(p) { + return p.original || p; + }); + + var connectionPath = [['M', waypoints[0].x, waypoints[0].y]]; + + waypoints.forEach(function(waypoint, index) { + if (index !== 0) { + connectionPath.push(['L', waypoint.x, waypoint.y]); + } + }); + + return componentsToPath(connectionPath); + }; +} + +inherits(CustomRenderer, BaseRenderer); + +CustomRenderer.$inject = ['eventBus', 'styles']; + +CustomRenderer.prototype.canRender = function(element) { + return /^custom:/.test(element.type); +}; + +CustomRenderer.prototype.drawShape = function(p, element) { + var type = element.type; + + if (type === 'custom:triangle') { + return this.drawTriangle(p, element.width); + } + + if (type === 'custom:circle') { + return this.drawCircle(p, element.width, element.height); + } +}; + +CustomRenderer.prototype.getShapePath = function(shape) { + var type = shape.type; + + if (type === 'custom:triangle') { + return this.getTrianglePath(shape); + } + + if (type === 'custom:circle') { + return this.getCirclePath(shape); + } +}; + +CustomRenderer.prototype.drawConnection = function(p, element) { + var type = element.type; + + if (type === 'custom:connection') { + return this.drawCustomConnection(p, element); + } +}; + +CustomRenderer.prototype.getConnectionPath = function(connection) { + var type = connection.type; + + if (type === 'custom:connection') { + return this.getCustomConnectionPath(connection); + } +}; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js new file mode 100644 index 00000000..1dce143d --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js @@ -0,0 +1,136 @@ +import { reduce } from 'min-dash'; + +import inherits from 'inherits'; + +import { is } from 'bpmn-js/lib/util/ModelUtil'; + +import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; + +var HIGH_PRIORITY = 1500; + +function isCustom(element) { + return element && /^custom:/.test(element.type); +} + +/** + * Specific rules for custom elements + */ +export default function CustomRules(eventBus) { + RuleProvider.call(this, eventBus); +} + +inherits(CustomRules, RuleProvider); + +CustomRules.$inject = ['eventBus']; + +CustomRules.prototype.init = function() { + /** + * Can shape be created on target container? + */ + function canCreate(shape, target) { + // only judge about custom elements + if (!isCustom(shape)) { + return; + } + + // allow creation on processes + return ( + is(target, 'bpmn:Process') || + is(target, 'bpmn:Participant') || + is(target, 'bpmn:Collaboration') + ); + } + + /** + * Can source and target be connected? + */ + function canConnect(source, target) { + // only judge about custom elements + if (!isCustom(source) && !isCustom(target)) { + return; + } + + // allow connection between custom shape and task + if (isCustom(source)) { + if (is(target, 'bpmn:Task')) { + return { type: 'custom:connection' }; + } else { + return false; + } + } else if (isCustom(target)) { + if (is(source, 'bpmn:Task')) { + return { type: 'custom:connection' }; + } else { + return false; + } + } + } + + this.addRule('elements.move', HIGH_PRIORITY, function(context) { + var target = context.target, + shapes = context.shapes; + + var type; + + // do not allow mixed movements of custom / BPMN shapes + // if any shape cannot be moved, the group cannot be moved, too + var allowed = reduce( + shapes, + function(result, s) { + if (type === undefined) { + type = isCustom(s); + } + + if (type !== isCustom(s) || result === false) { + return false; + } + + return canCreate(s, target); + }, + undefined + ); + + // reject, if we have at least one + // custom element that cannot be moved + return allowed; + }); + + this.addRule('shape.create', HIGH_PRIORITY, function(context) { + var target = context.target, + shape = context.shape; + + return canCreate(shape, target); + }); + + this.addRule('shape.resize', HIGH_PRIORITY, function(context) { + var shape = context.shape; + + if (isCustom(shape)) { + // cannot resize custom elements + return false; + } + }); + + this.addRule('connection.create', HIGH_PRIORITY, function(context) { + var source = context.source, + target = context.target; + + return canConnect(source, target); + }); + + this.addRule('connection.reconnectStart', HIGH_PRIORITY, function(context) { + var connection = context.connection, + source = context.hover || context.source, + target = connection.target; + + return canConnect(source, target, connection); + }); + + this.addRule('connection.reconnectEnd', HIGH_PRIORITY, function(context) { + var connection = context.connection, + source = connection.source, + target = context.hover || context.target; + + return canConnect(source, target, connection); + }); +}; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js new file mode 100644 index 00000000..532c24f3 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js @@ -0,0 +1,136 @@ +import inherits from 'inherits'; + +import { pick, assign } from 'min-dash'; + +import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; + +import { + add as collectionAdd, + remove as collectionRemove +} from 'diagram-js/lib/util/Collections'; + +/** + * A handler responsible for updating the custom element's businessObject + * once changes on the diagram happen. + */ +export default function CustomUpdater(eventBus, bpmnjs) { + CommandInterceptor.call(this, eventBus); + + function updateCustomElement(e) { + var context = e.context, + shape = context.shape, + businessObject = shape.businessObject; + + if (!isCustom(shape)) { + return; + } + + var parent = shape.parent; + + var customElements = bpmnjs._customElements; + + // make sure element is added / removed from bpmnjs.customElements + if (!parent) { + collectionRemove(customElements, businessObject); + } else { + collectionAdd(customElements, businessObject); + } + + // save custom element position + assign(businessObject, pick(shape, ['x', 'y'])); + } + + function updateCustomConnection(e) { + var context = e.context, + connection = context.connection, + source = connection.source, + target = connection.target, + businessObject = connection.businessObject; + + var parent = connection.parent; + + var customElements = bpmnjs._customElements; + + // make sure element is added / removed from bpmnjs.customElements + if (!parent) { + collectionRemove(customElements, businessObject); + } else { + collectionAdd(customElements, businessObject); + } + + // update waypoints + assign(businessObject, { + waypoints: copyWaypoints(connection) + }); + + if (source && target) { + assign(businessObject, { + source: source.id, + target: target.id + }); + } + } + + this.executed( + ['shape.create', 'shape.move', 'shape.delete'], + ifCustomElement(updateCustomElement) + ); + + this.reverted( + ['shape.create', 'shape.move', 'shape.delete'], + ifCustomElement(updateCustomElement) + ); + + this.executed( + [ + 'connection.create', + 'connection.reconnectStart', + 'connection.reconnectEnd', + 'connection.updateWaypoints', + 'connection.delete', + 'connection.layout', + 'connection.move' + ], + ifCustomElement(updateCustomConnection) + ); + + this.reverted( + [ + 'connection.create', + 'connection.reconnectStart', + 'connection.reconnectEnd', + 'connection.updateWaypoints', + 'connection.delete', + 'connection.layout', + 'connection.move' + ], + ifCustomElement(updateCustomConnection) + ); +} + +inherits(CustomUpdater, CommandInterceptor); + +CustomUpdater.$inject = ['eventBus', 'bpmnjs']; + +/////// helpers /////////////////////////////////// + +function copyWaypoints(connection) { + return connection.waypoints.map(function(p) { + return { x: p.x, y: p.y }; + }); +} + +function isCustom(element) { + return element && /custom:/.test(element.type); +} + +function ifCustomElement(fn) { + return function(event) { + var context = event.context, + element = context.shape || context.connection; + + if (isCustom(element)) { + fn(event); + } + }; +} diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js new file mode 100644 index 00000000..f1085390 --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js @@ -0,0 +1,22 @@ +import CustomElementFactory from './CustomElementFactory'; +import CustomRenderer from './CustomRenderer'; +import CustomPalette from './CustomPalette'; +import CustomRules from './CustomRules'; +import CustomUpdater from './CustomUpdater'; +import CustomContextPadProvider from './CustomContextPadProvider'; + +export default { + __init__: [ + 'customRenderer', + 'paletteProvider', + 'customRules', + 'customUpdater', + 'contextPadProvider' + ], + elementFactory: ['type', CustomElementFactory], + customRenderer: ['type', CustomRenderer], + paletteProvider: ['type', CustomPalette], + customRules: ['type', CustomRules], + customUpdater: ['type', CustomUpdater], + contextPadProvider: ['type', CustomContextPadProvider] +}; diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js new file mode 100644 index 00000000..86fbff6a --- /dev/null +++ b/workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js @@ -0,0 +1,99 @@ +import Modeler from 'bpmn-js/lib/Modeler'; + +import { assign, isArray } from 'min-dash'; + +import inherits from 'inherits'; + +import CustomModule from './custom'; + +export default function CustomModeler(options) { + Modeler.call(this, options); + + this._customElements = []; +} + +inherits(CustomModeler, Modeler); + +CustomModeler.prototype._modules = [].concat(CustomModeler.prototype._modules, [ + CustomModule +]); + +/** + * Add a single custom element to the underlying diagram + * + * @param {Object} customElement + */ +CustomModeler.prototype._addCustomShape = function(customElement) { + this._customElements.push(customElement); + + var canvas = this.get('canvas'), + elementFactory = this.get('elementFactory'); + + var customAttrs = assign({ businessObject: customElement }, customElement); + + var customShape = elementFactory.create('shape', customAttrs); + + return canvas.addShape(customShape); +}; + +CustomModeler.prototype._addCustomConnection = function(customElement) { + this._customElements.push(customElement); + + var canvas = this.get('canvas'), + elementFactory = this.get('elementFactory'), + elementRegistry = this.get('elementRegistry'); + + var customAttrs = assign({ businessObject: customElement }, customElement); + + var connection = elementFactory.create( + 'connection', + assign(customAttrs, { + source: elementRegistry.get(customElement.source), + target: elementRegistry.get(customElement.target) + }), + elementRegistry.get(customElement.source).parent + ); + + return canvas.addConnection(connection); +}; + +/** + * Add a number of custom elements and connections to the underlying diagram. + * + * @param {Array<Object>} customElements + */ +CustomModeler.prototype.addCustomElements = function(customElements) { + if (!isArray(customElements)) { + throw new Error('argument must be an array'); + } + + var shapes = [], + connections = []; + + customElements.forEach(function(customElement) { + if (isCustomConnection(customElement)) { + connections.push(customElement); + } else { + shapes.push(customElement); + } + }); + + // add shapes before connections so that connections + // can already rely on the shapes being part of the diagram + shapes.forEach(this._addCustomShape, this); + + connections.forEach(this._addCustomConnection, this); +}; + +/** + * Get custom elements with their current status. + * + * @return {Array<Object>} custom elements on the diagram + */ +CustomModeler.prototype.getCustomElements = function() { + return this._customElements; +}; + +function isCustomConnection(element) { + return element.type === 'custom:connection'; +} diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js b/workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js index 7848b1f0..4e591c9b 100644 --- a/workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js +++ b/workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js @@ -41,6 +41,21 @@ const Api = { } ); }, + fetchVersionArtifact: ({ workflowId, versionId }) => { + return RestfulAPIUtil.fetch( + `${baseUrl(workflowId)}/${versionId}/artifact` + ); + }, + updateVersionArtifact: ({ workflowId, versionId, payload }) => { + let formData = new FormData(); + var blob = new Blob([payload], { type: 'text/xml' }); + formData.append('fileToUpload', blob); + + return RestfulAPIUtil.put( + `${baseUrl(workflowId)}/${versionId}/artifact`, + formData + ); + }, certifyVersion: ({ workflowId, versionId }) => { return RestfulAPIUtil.post( `${baseUrl(workflowId)}/${versionId}/state`, diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js b/workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js index aa2ea8d0..a3ead31f 100644 --- a/workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js +++ b/workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js @@ -20,15 +20,18 @@ import { getOutputs } from 'features/version/inputOutput/inputOutputSelectors'; import { getVersionInfo } from 'features/version/general/generalSelectors'; +import { getComposition } from 'features/version/composition/compositionSelectors'; export const getVersionsList = state => state && state.workflow.versions; export const getSavedObjParams = createSelector( getOutputs, getInputs, + getComposition, getVersionInfo, - (outputs, inputs, general) => ({ + (outputs, inputs, composition, general) => ({ outputs, inputs, + composition, ...general }) ); diff --git a/workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js b/workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js index 9ef88f9b..78b82ab1 100644 --- a/workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js +++ b/workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js @@ -32,14 +32,24 @@ import { notificationActions } from 'shared/notifications/notificationsActions'; import { versionState } from 'features/version/versionConstants'; import overviewApi from '../workflow/overview/overviewApi'; import { versionListFetchAction } from '../workflow/overview/overviewConstansts'; +import { updateComposition } from 'features/version/composition/compositionActions'; function* fetchVersion(action) { try { const data = yield call(versionApi.fetchVersion, action.payload); const { inputs, outputs, ...rest } = data; + let composition = false; + + if (rest.hasArtifact) { + composition = yield call( + versionApi.fetchVersionArtifact, + action.payload + ); + } yield all([ put(setWorkflowVersionAction(rest)), - put(setInputsOutputs({ inputs, outputs })) + put(setInputsOutputs({ inputs, outputs })), + put(updateComposition(composition)) ]); } catch (error) { yield put(genericNetworkErrorAction(error)); @@ -62,7 +72,22 @@ function* watchSubmitVersion(action) { function* watchUpdateVersion(action) { try { - yield call(versionApi.updateVersion, action.payload); + //const { composition, ...versionData } = action.payload; + const { + workflowId, + params: { composition, ...versionData } + } = action.payload; + yield call(versionApi.updateVersion, { + workflowId, + params: versionData + }); + if (composition) { + yield call(versionApi.updateVersionArtifact, { + workflowId, + versionId: versionData.id, + payload: composition + }); + } yield put( notificationActions.showSuccess({ title: 'Update Workflow Version', diff --git a/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js b/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js index 4db8f49d..87600be2 100644 --- a/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js +++ b/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js @@ -23,7 +23,7 @@ describe('Overview reducer', () => { total: 2, size: 0, page: 0, - results: [ + items: [ { id: '99adf5bc36764628b8018033d285b591', name: '1.0', @@ -91,6 +91,6 @@ describe('Overview reducer', () => { expect( overviewReducer([], versionListFetchAction(versionResponse)) - ).toEqual([...versionResponse.results]); + ).toEqual([...versionResponse.items]); }); }); diff --git a/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js b/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js index 720d410c..bab482a4 100644 --- a/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js +++ b/workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js @@ -19,7 +19,7 @@ import { FETCH_VERSION_LIST } from 'features/workflow/overview/overviewConstanst export default function overviewReducer(state = [], action) { switch (action.type) { case FETCH_VERSION_LIST: - return [...action.payload.results]; + return [...action.payload.items]; default: return state; } diff --git a/workflow-designer-ui/src/main/frontend/src/i18n/languages.json b/workflow-designer-ui/src/main/frontend/src/i18n/languages.json index 986cf128..5fd444a6 100644 --- a/workflow-designer-ui/src/main/frontend/src/i18n/languages.json +++ b/workflow-designer-ui/src/main/frontend/src/i18n/languages.json @@ -59,6 +59,9 @@ "confirmDlete": "Are you sure you want to delete \"%{name}\"?", "alreadyExists": "Already exists", "invalidCharacters": "Alphanumeric and underscore only" + }, + "composition": { + "bpmnError" : "BPMN.IO Error" } }, "version": { diff --git a/workflow-designer-ui/src/main/frontend/src/rootReducers.js b/workflow-designer-ui/src/main/frontend/src/rootReducers.js index 9dbef266..9fab8140 100644 --- a/workflow-designer-ui/src/main/frontend/src/rootReducers.js +++ b/workflow-designer-ui/src/main/frontend/src/rootReducers.js @@ -25,14 +25,15 @@ import loader from 'shared/loader/LoaderReducer'; import modal from 'shared/modal/modalWrapperReducer'; import overviewReducer from 'features/workflow/overview/overviewReducer'; import workflowReducer from 'features/workflow/workflowReducer'; - +import compositionReducer from 'features/version/composition/compositionReducer'; export default combineReducers({ i18n: i18nReducer, catalog, notifications: notificationsReducer, currentVersion: combineReducers({ general: versionReducer, - inputOutput + inputOutput, + composition: compositionReducer }), workflow: combineReducers({ data: workflowReducer, diff --git a/workflow-designer-ui/src/main/frontend/src/routes.js b/workflow-designer-ui/src/main/frontend/src/routes.js index 79abe303..ea433cca 100644 --- a/workflow-designer-ui/src/main/frontend/src/routes.js +++ b/workflow-designer-ui/src/main/frontend/src/routes.js @@ -19,7 +19,7 @@ import Version from 'features/version/Version'; import GeneralView from 'features/version/general/General'; import OverviewView from 'features/workflow/overview/Overview'; import InputOutput from 'features/version/inputOutput/InputOutput'; -import CompositionView from 'features/version/composition/CompositionView'; +import Composition from 'features/version/composition/Composition'; export const routes = [ { @@ -41,7 +41,7 @@ export const routes = [ }, { path: '/composition', - component: CompositionView, + component: Composition, i18nName: 'workflow.sideBar.composition', id: 'COMPOSITION' } diff --git a/workflow-designer-ui/src/main/frontend/webpack.config.js b/workflow-designer-ui/src/main/frontend/webpack.config.js index 23f53343..fa4e5d83 100644 --- a/workflow-designer-ui/src/main/frontend/webpack.config.js +++ b/workflow-designer-ui/src/main/frontend/webpack.config.js @@ -127,8 +127,13 @@ module.exports = (env, argv) => { include: srcPath }, { - test: /\.woff|\.woff2$/, - loader: 'file-loader' + test: /\.(eot|svg|ttf|woff|woff2)(\?.*)?$/, + use: [ + { + loader: 'file-loader', + options: {} + } + ] } ] }, diff --git a/workflow-designer-ui/src/main/frontend/yarn.lock b/workflow-designer-ui/src/main/frontend/yarn.lock index 7e31f62b..dbfc8bbe 100644 --- a/workflow-designer-ui/src/main/frontend/yarn.lock +++ b/workflow-designer-ui/src/main/frontend/yarn.lock @@ -4450,6 +4450,10 @@ file-loader@^1.1.11: loader-utils "^1.0.2" schema-utils "^0.4.5" +file-saver@^1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" + filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" |