composition readonly implementation
[sdc/sdc-workflow-designer.git] / workflow-designer-ui / src / main / frontend / src / features / version / composition / CompositionView.js
1 /*
2 * Copyright © 2018 European Support Limited
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *http://www.apache.org/licenses/LICENSE-2.0
9 *
10  * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 import React, { Component } from 'react';
17 import fileSaver from 'file-saver';
18 import isEqual from 'lodash.isequal';
19 import CustomModeler from './custom-modeler';
20 import propertiesPanelModule from 'bpmn-js-properties-panel';
21 import propertiesProviderModule from './custom-properties-provider/provider/camunda';
22 import camundaModuleDescriptor from './custom-properties-provider/descriptors/camunda';
23 import newDiagramXML from './newDiagram.bpmn';
24 import PropTypes from 'prop-types';
25 import CompositionButtons from './components/CompositionButtonsPanel';
26 import { setElementInputsOutputs } from './bpmnUtils.js';
27 import { I18n } from 'react-redux-i18n';
28 import {
29     PROCESS_DEFAULT_ID,
30     COMPOSITION_ERROR_COLOR,
31     COMPOSITION_VALID_COLOR,
32     CAMUNDA_PANEL_INPUTS_NAMES
33 } from './compositionConstants';
34 import readOnly from './readOnly';
35
36 function setStatusToElement(type, status, parent) {
37     let elements = parent.getElementsByTagName(type);
38     for (let item of elements) {
39         if (item.name !== 'selectedExtensionElement') {
40             item.readOnly = status;
41             item.disabled = status;
42         }
43     }
44 }
45
46 function disablePanelInputs(status) {
47     let panel = document.getElementById('js-properties-panel');
48
49     if (panel) {
50         setStatusToElement('input', status, panel);
51         setStatusToElement('button', status, panel);
52         setStatusToElement('select', status, panel);
53
54         CAMUNDA_PANEL_INPUTS_NAMES.map(name => {
55             const div = document.getElementById(name);
56             if (div) {
57                 div.setAttribute('contenteditable', !status);
58             }
59         });
60     }
61 }
62 class CompositionView extends Component {
63     static propTypes = {
64         compositionUpdate: PropTypes.func,
65         showErrorModal: PropTypes.func,
66         composition: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
67         name: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
68         versionName: PropTypes.string,
69         inputOutput: PropTypes.object,
70         activities: PropTypes.array,
71         validationUpdate: PropTypes.func,
72         errors: PropTypes.array,
73         isReadOnly: PropTypes.bool
74     };
75
76     constructor() {
77         super();
78         this.generatedId = 'bpmn-container' + Date.now();
79         this.fileInput = React.createRef();
80         this.bpmnContainer = React.createRef();
81         this.selectedElement = false;
82         this.state = {
83             diagram: false
84         };
85         this.versionChanged = false;
86     }
87     componentDidUpdate(prevProps) {
88         const { errors, isReadOnly, versionName, composition } = this.props;
89         if (!isEqual(prevProps.errors, errors)) {
90             errors.map(item => {
91                 this.modeling.setColor([item.element], {
92                     fill: item.isValid
93                         ? COMPOSITION_VALID_COLOR
94                         : COMPOSITION_ERROR_COLOR
95                 });
96             });
97         }
98         if (prevProps.isReadOnly !== isReadOnly) {
99             this.modeler.get('readOnly').readOnly(isReadOnly);
100             disablePanelInputs(isReadOnly);
101         }
102
103         if (prevProps.versionName !== versionName) {
104             this.versionChanged = true;
105         }
106         if (
107             !isEqual(prevProps.composition, composition) &&
108             this.versionChanged
109         ) {
110             this.setDiagramToBPMN(composition);
111             this.versionChanged = false;
112         }
113     }
114     componentDidMount() {
115         const {
116             composition,
117             activities,
118             inputOutput,
119             validationUpdate,
120             isReadOnly
121         } = this.props;
122
123         const readOnlyModule = {
124             __init__: ['readOnly'],
125             readOnly: ['type', readOnly]
126         };
127         this.modeler = new CustomModeler({
128             propertiesPanel: {
129                 parent: '#js-properties-panel'
130             },
131             additionalModules: [
132                 propertiesPanelModule,
133                 propertiesProviderModule,
134                 readOnlyModule
135             ],
136             moddleExtensions: {
137                 camunda: camundaModuleDescriptor
138             },
139             workflow: {
140                 activities: activities,
141                 getActivityInputsOutputs: this.getActivityInputsOutputs,
142                 workflowInputOutput: inputOutput,
143                 validationUpdate: validationUpdate
144             }
145         });
146
147         this.modeler.attachTo('#' + this.generatedId);
148         this.setDiagramToBPMN(composition ? composition : newDiagramXML);
149         this.modeler.on('element.out', () => this.exportDiagramToStore());
150         this.modeler.on('element.click', this.handleCompositionStatus);
151         this.bpmnContainer.current.click();
152         this.modeling = this.modeler.get('modeling');
153         this.modeler.get('readOnly').readOnly(isReadOnly);
154     }
155     handleCompositionStatus = () => {
156         disablePanelInputs(this.props.isReadOnly);
157     };
158     getActivityInputsOutputs = selectedValue => {
159         const selectedActivity = this.props.activities.find(
160             el => el.name === selectedValue
161         );
162
163         if (selectedActivity) {
164             const inputsOutputs = {
165                 inputs: selectedActivity.inputs,
166                 outputs: selectedActivity.outputs
167             };
168             return inputsOutputs;
169         } else return { inputs: [], outputs: [] };
170     };
171
172     setDiagramToBPMN = diagram => {
173         let modeler = this.modeler;
174         this.modeler.importXML(diagram, err => {
175             if (err) {
176                 return this.props.showErrorModal(
177                     I18n.t('workflow.composition.importErrorMsg')
178                 );
179             }
180             const canvas = modeler.get('canvas');
181             canvas.zoom('fit-viewport');
182             const { businessObject } = canvas._rootElement;
183
184             this.setDefaultIdAndName(businessObject);
185             setElementInputsOutputs(
186                 businessObject,
187                 this.props.inputOutput,
188                 this.modeler.get('moddle')
189             );
190             disablePanelInputs(this.props.isReadOnly);
191         });
192     };
193     setDefaultIdAndName = businessObject => {
194         const { name = '' } = this.props;
195         if (!businessObject.name) {
196             businessObject.name = name;
197         }
198
199         if (businessObject.id === PROCESS_DEFAULT_ID) {
200             businessObject.id = name.toLowerCase().replace(/\s/g, '_');
201         }
202     };
203     exportDiagramToStore = () => {
204         this.modeler.saveXML({ format: true }, (err, xml) => {
205             if (err) {
206                 return this.props.showErrorModal(
207                     I18n.t('workflow.composition.saveErrorMsg')
208                 );
209             }
210             return this.props.compositionUpdate(xml);
211         });
212     };
213
214     exportDiagram = () => {
215         const { name, showErrorModal, versionName } = this.props;
216         this.modeler.saveXML({ format: true }, (err, xml) => {
217             if (err) {
218                 return showErrorModal(
219                     I18n.t('workflow.composition.exportErrorMsg')
220                 );
221             }
222             const blob = new Blob([xml], { type: 'text/html;charset=utf-8' });
223             fileSaver.saveAs(
224                 blob,
225                 `${name.replace(/\s/g, '').toLowerCase()}-${versionName}.bpmn`
226             );
227         });
228     };
229
230     loadNewDiagram = () => {
231         this.setDiagramToBPMN(newDiagramXML);
232     };
233
234     uploadDiagram = () => {
235         this.fileInput.current.click();
236     };
237
238     handleFileInputChange = filesList => {
239         const file = filesList[0];
240         const reader = new FileReader();
241         reader.onloadend = event => {
242             var xml = event.target.result;
243             this.setDiagramToBPMN(xml);
244             this.fileInput.value = '';
245         };
246         reader.readAsText(file);
247     };
248
249     render() {
250         return (
251             <div className="composition-view content">
252                 <input
253                     ref={this.fileInput}
254                     onChange={e => this.handleFileInputChange(e.target.files)}
255                     id="file-input"
256                     accept=".bpmn, .xml"
257                     type="file"
258                     name="file-input"
259                     style={{ display: 'none' }}
260                 />
261                 <div
262                     ref={this.bpmnContainer}
263                     className="bpmn-container"
264                     id={this.generatedId}
265                 />
266                 <div className="bpmn-sidebar">
267                     <div
268                         className="properties-panel"
269                         id="js-properties-panel"
270                     />
271                     <CompositionButtons
272                         isReadOnly={this.props.isReadOnly}
273                         onClean={this.loadNewDiagram}
274                         onDownload={this.exportDiagram}
275                         onUpload={this.uploadDiagram}
276                     />
277                 </div>
278             </div>
279         );
280     }
281 }
282
283 export default CompositionView;