composition save & load 83/58583/3
authorStanislav Vishnevetskiy <shlomo-stanisla.vishnevetskiy@amdocs.com>
Thu, 2 Aug 2018 10:36:01 +0000 (13:36 +0300)
committerStanislav Vishnevetskiy <shlomo-stanisla.vishnevetskiy@amdocs.com>
Thu, 2 Aug 2018 10:36:14 +0000 (13:36 +0300)
Issue-ID: SDC-1591
Change-Id: I5b347a3ca1737b95045097051da462ac4fb2d781
Signed-off-by: Stanislav Vishnevetskiy <shlomo-stanisla.vishnevetskiy@amdocs.com>
32 files changed:
workflow-designer-ui/src/main/frontend/.gitignore
workflow-designer-ui/src/main/frontend/index.html
workflow-designer-ui/src/main/frontend/package.json
workflow-designer-ui/src/main/frontend/resources/scss/components/_notifications.scss
workflow-designer-ui/src/main/frontend/resources/scss/features/_composition.scss
workflow-designer-ui/src/main/frontend/resources/scss/style.scss
workflow-designer-ui/src/main/frontend/src/features/version/composition/Composition.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/CompositionView.js
workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButton.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/components/CompositionButtonsPanel.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionActions.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionConstants.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionReducer.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/compositionSelectors.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomContextPadProvider.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomElementFactory.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomPalette.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRenderer.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomRules.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/CustomUpdater.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/custom/index.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/composition/custom-modeler/index.js [new file with mode: 0644]
workflow-designer-ui/src/main/frontend/src/features/version/versionApi.js
workflow-designer-ui/src/main/frontend/src/features/version/versionController/versionControllerSelectors.js
workflow-designer-ui/src/main/frontend/src/features/version/versionSaga.js
workflow-designer-ui/src/main/frontend/src/features/workflow/overview/__tests__/overviewReducer-test.js
workflow-designer-ui/src/main/frontend/src/features/workflow/overview/overviewReducer.js
workflow-designer-ui/src/main/frontend/src/i18n/languages.json
workflow-designer-ui/src/main/frontend/src/rootReducers.js
workflow-designer-ui/src/main/frontend/src/routes.js
workflow-designer-ui/src/main/frontend/webpack.config.js
workflow-designer-ui/src/main/frontend/yarn.lock

index c3a6f3b..09d2d0a 100644 (file)
@@ -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>
index 529d909..bd224ba 100644 (file)
@@ -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",
index 7ab294a..a159a4b 100644 (file)
@@ -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;
+                               }
+                       }
                }
        }
 }
index 95828ae..4927856 100644 (file)
@@ -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 (file)
index 0000000..d2c273c
--- /dev/null
@@ -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);
index ba0351b..d549456 100644 (file)
@@ -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,
 * 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&#10;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?&#10;">\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 (file)
index 0000000..2fc6618
--- /dev/null
@@ -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 (file)
index 0000000..add6490
--- /dev/null
@@ -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 (file)
index 0000000..3f1755d
--- /dev/null
@@ -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 (file)
index 0000000..74cab0c
--- /dev/null
@@ -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 (file)
index 0000000..9c70736
--- /dev/null
@@ -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 (file)
index 0000000..7e28ca6
--- /dev/null
@@ -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 (file)
index 0000000..0f2ba52
--- /dev/null
@@ -0,0 +1,43 @@
+import inherits from 'inherits';\r
+\r
+import ContextPadProvider from 'bpmn-js/lib/features/context-pad/ContextPadProvider';\r
+\r
+import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil';\r
+\r
+import { assign, bind } from 'min-dash';\r
+\r
+export default function CustomContextPadProvider(injector, connect, translate) {\r
+    injector.invoke(ContextPadProvider, this);\r
+\r
+    var cached = bind(this.getContextPadEntries, this);\r
+\r
+    this.getContextPadEntries = function(element) {\r
+        var actions = cached(element);\r
+\r
+        var businessObject = element.businessObject;\r
+\r
+        function startConnect(event, element, autoActivate) {\r
+            connect.start(event, element, autoActivate);\r
+        }\r
+\r
+        if (isAny(businessObject, ['custom:triangle', 'custom:circle'])) {\r
+            assign(actions, {\r
+                connect: {\r
+                    group: 'connect',\r
+                    className: 'bpmn-icon-connection-multi',\r
+                    title: translate('Connect using custom connection'),\r
+                    action: {\r
+                        click: startConnect,\r
+                        dragstart: startConnect\r
+                    }\r
+                }\r
+            });\r
+        }\r
+\r
+        return actions;\r
+    };\r
+}\r
+\r
+inherits(CustomContextPadProvider, ContextPadProvider);\r
+\r
+CustomContextPadProvider.$inject = ['injector', 'connect', 'translate'];\r
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 (file)
index 0000000..01d4d27
--- /dev/null
@@ -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 (file)
index 0000000..a8adb2f
--- /dev/null
@@ -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 (file)
index 0000000..f397fed
--- /dev/null
@@ -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 (file)
index 0000000..1dce143
--- /dev/null
@@ -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 (file)
index 0000000..532c24f
--- /dev/null
@@ -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 (file)
index 0000000..f108539
--- /dev/null
@@ -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 (file)
index 0000000..86fbff6
--- /dev/null
@@ -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';
+}
index 7848b1f..4e591c9 100644 (file)
@@ -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`,
index aa2ea8d..a3ead31 100644 (file)
@@ -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
     })
 );
index 9ef88f9..78b82ab 100644 (file)
@@ -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',
index 4db8f49..87600be 100644 (file)
@@ -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]);
     });
 });
index 720d410..bab482a 100644 (file)
@@ -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;
     }
index 986cf12..5fd444a 100644 (file)
@@ -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": {
index 9dbef26..9fab814 100644 (file)
@@ -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,
index 79abe30..ea433cc 100644 (file)
@@ -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'
             }
index 23f5334..fa4e5d8 100644 (file)
@@ -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: {}
+                        }
+                    ]
                 }
             ]
         },
index 7e31f62..dbfc8bb 100644 (file)
@@ -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"