a2c7f5122ad64af371d17728811958165b29235e
[sdc.git] /
1 /*!
2  * Copyright © 2016-2017 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
13  * or implied. See the License for the specific language governing
14  * permissions and limitations under the License.
15  */
16
17 import React from 'react';
18 import PropTypes from 'prop-types';
19 import Select from 'react-select';
20 import { DragSource, DropTarget } from 'react-dnd';
21
22 import Common from '../../../../../../common/Common';
23
24 import Icon from '../../../../../icons/Icon';
25 import iconDelete from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icons/delete.svg';
26 import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icons/handle.svg';
27 import iconNotes from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icons/notes.svg';
28 import iconSettings from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icons/settings.svg';
29 import iconRequestSync from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/request-sync.svg';
30 import iconRequestAsync from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/request-async.svg';
31 import iconResponse from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/response.svg';
32
33 /**
34  * LHS message row view.
35  */
36 class Message extends React.Component {
37
38   // ///////////////////////////////////////////////////////////////////////////////////////////////
39
40   /**
41    * Construct view.
42    * @param props element properties.
43    * @param context react context.
44    */
45   constructor(props, context) {
46     super(props, context);
47
48     this.state = {
49       active: false,
50       name: props.message.name || '',
51     };
52
53     this.combinedOptions = [{
54       value: 'REQUEST_SYNC',
55     }, {
56       value: 'REQUEST_ASYNC',
57     }, {
58       value: 'RESPONSE',
59     }];
60
61     // Bindings.
62
63     this.onChangeName = this.onChangeName.bind(this);
64     this.onBlurName = this.onBlurName.bind(this);
65     this.onChangeType = this.onChangeType.bind(this);
66     this.onChangeFrom = this.onChangeFrom.bind(this);
67     this.onChangeTo = this.onChangeTo.bind(this);
68     this.onClickDelete = this.onClickDelete.bind(this);
69     this.onClickActions = this.onClickActions.bind(this);
70     this.onClickNotes = this.onClickNotes.bind(this);
71     this.onMouseEnter = this.onMouseEnter.bind(this);
72     this.onMouseLeave = this.onMouseLeave.bind(this);
73   }
74
75   // ///////////////////////////////////////////////////////////////////////////////////////////////
76
77   /**
78    * Handle name change.
79    * @param event change event.
80    */
81   onChangeName(event) {
82     this.setState({ name: event.target.value });
83   }
84
85   // ///////////////////////////////////////////////////////////////////////////////////////////////
86
87   /**
88    * Handle name change.
89    * @param event change event.
90    */
91   onBlurName(event) {
92     const options = this.props.application.getOptions();
93     const sanitized = Common.sanitizeText(event.target.value, options, 'message');
94     const props = {
95       id: this.props.message.id,
96       name: sanitized,
97     };
98     this.props.designer.updateMessage(props);
99     this.setState({ name: sanitized });
100   }
101
102   // ///////////////////////////////////////////////////////////////////////////////////////////////
103
104   /**
105    * Handle delete.
106    */
107   onClickDelete() {
108     this.props.designer.deleteMessage(this.props.message.id);
109   }
110
111   // ///////////////////////////////////////////////////////////////////////////////////////////////
112
113   /**
114    * Handle menu click.
115    */
116   onClickActions(event) {
117     this.props.designer.showActions(this.props.message.id, { x: event.pageX, y: event.pageY });
118   }
119
120   // ///////////////////////////////////////////////////////////////////////////////////////////////
121
122   /**
123    * Handle menu click.
124    */
125   onClickNotes() {
126     this.props.designer.showNotes(this.props.message.id);
127   }
128
129   // ///////////////////////////////////////////////////////////////////////////////////////////////
130
131   /**
132    * Handle selection.
133    * @param value selection.
134    */
135   onChangeFrom(value) {
136     if (value.target) {
137       this.updateMessage({ from: value.target.value });
138     } else {
139       this.updateMessage({ from: value.value });
140     }
141   }
142
143   // ///////////////////////////////////////////////////////////////////////////////////////////////
144
145   /**
146    * Handle selection.
147    * @param value selection.
148    */
149   onChangeTo(value) {
150     if (value.target) {
151       this.updateMessage({ to: value.target.value });
152     } else {
153       this.updateMessage({ to: value.value });
154     }
155   }
156
157   // ///////////////////////////////////////////////////////////////////////////////////////////////
158
159   /**
160    * Handle selection.
161    * @param selected selection.
162    */
163   onChangeType(selected) {
164
165     const value = selected.target ? selected.target.value : selected.value;
166     const props = {};
167     if (value.indexOf('RESPONSE') !== -1) {
168       props.type = 'response';
169       props.asynchronous = false;
170     } else {
171       props.type = 'request';
172       props.asynchronous = (value.indexOf('ASYNC') !== -1);
173     }
174
175     this.updateMessage(props);
176   }
177
178   // ///////////////////////////////////////////////////////////////////////////////////////////////
179
180   /**
181    * Handle mouse event.
182    */
183   onMouseEnter() {
184     this.setState({ active: true });
185     this.props.designer.onMouseEnterMessage(this.props.message.id);
186   }
187
188   // ///////////////////////////////////////////////////////////////////////////////////////////////
189
190   /**
191    * Handle mouse event.
192    */
193   onMouseLeave() {
194     this.setState({ active: false });
195     this.props.designer.onMouseLeaveMessage(this.props.message.id);
196   }
197
198   // ///////////////////////////////////////////////////////////////////////////////////////////////
199
200   /**
201    * Update message properties.
202    * @param props properties updates.
203    */
204   updateMessage(props) {
205     const update = {
206       id: this.props.message.id,
207     };
208     for (const k of Object.keys(props)) {
209       update[k] = props[k];
210     }
211     this.props.designer.updateMessage(update);
212   }
213
214   // ///////////////////////////////////////////////////////////////////////////////////////////////
215
216   /**
217    * Render icon.
218    * @param option selection.
219    * @returns {XML}
220    */
221   renderOption(option) {
222     if (option.value === 'RESPONSE') {
223       return <Icon glyph={iconResponse} />;
224     }
225     if (option.value === 'REQUEST_ASYNC') {
226       return <Icon glyph={iconRequestAsync} />;
227     }
228     return <Icon glyph={iconRequestSync} />;
229   }
230
231   // ///////////////////////////////////////////////////////////////////////////////////////////////
232
233   /**
234    * Get request/response and asynchronous combined constant.
235    * @param message message whose properties define spec.
236    * @returns {*}
237    */
238   getMessageSpec(message) {
239     if (message.type === 'response') {
240       return 'RESPONSE';
241     }
242     if (message.asynchronous) {
243       return 'REQUEST_ASYNC';
244     }
245     return 'REQUEST_SYNC';
246   }
247
248   // ///////////////////////////////////////////////////////////////////////////////////////////////
249
250   /**
251    * @returns {*}
252    * @private
253    */
254   renderHTMLSelect() {
255
256     const message = this.props.message;
257     const from = this.props.from;
258     const to = Common.assertNotNull(this.props.to);
259     const messageNotesActiveClass = message.notes && message.notes.length > 0 ? 'asdcs-active' : '';
260     const combinedValue = this.getMessageSpec(message);
261
262     const lifelineOptions = [];
263     for (const lifeline of this.props.model.unwrap().diagram.lifelines) {
264       lifelineOptions.push(<option
265         key={lifeline.id}
266         value={lifeline.id}
267       >
268         {lifeline.name}
269       </option>);
270     }
271
272     const activeClass = (this.state.active || this.props.active) ? 'asdcs-active' : '';
273     const { connectDragSource, connectDropTarget } = this.props;
274     return connectDragSource(connectDropTarget(
275       <div
276         className={`asdcs-designer-message ${activeClass}`}
277         data-id={message.id}
278         onMouseEnter={this.onMouseEnter}
279         onMouseLeave={this.onMouseLeave}
280       >
281
282         <table className="asdcs-designer-layout asdcs-designer-message-row1">
283           <tbody>
284             <tr>
285               <td>
286                 <div className="asdcs-designer-sort asdcs-designer-icon">
287                   <Icon glyph={iconHandle} />
288                 </div>
289               </td>
290               <td>
291                 <div className="asdcs-designer-message-index">{message.index}.</div>
292               </td>
293               <td>
294                 <div className="asdcs-designer-message-name">
295                   <input
296                     type="text"
297                     className="asdcs-editable"
298                     value={this.state.name}
299                     placeholder="Unnamed"
300                     onBlur={this.onBlurName}
301                     onChange={this.onChangeName}
302                   />
303                 </div>
304               </td>
305               <td>
306                 <div className="asdcs-designer-actions">
307                   <div
308                     className="asdcs-designer-settings asdcs-designer-icon"
309                     onClick={this.onClickActions}
310                   >
311                     <Icon glyph={iconSettings} />
312                   </div>
313                   <div
314                     className={`asdcs-designer-notes asdcs-designer-icon ${messageNotesActiveClass}`}
315                     onClick={this.onClickNotes}
316                   >
317                     <Icon glyph={iconNotes} />
318                   </div>
319                   <div
320                     className="asdcs-designer-delete asdcs-designer-icon"
321                     onClick={this.onClickDelete}
322                   >
323                     <Icon glyph={iconDelete} />
324                   </div>
325                 </div>
326               </td>
327             </tr>
328           </tbody>
329         </table>
330
331         <table className="asdcs-designer-layout asdcs-designer-message-row2">
332           <tbody>
333             <tr>
334               <td>
335                 <select
336                   onChange={this.onChangeFrom}
337                   className="asdcs-designer-select-message-from"
338                   value={from.id}
339                   onChange={this.onChangeFrom}
340                 >
341                   options={lifelineOptions}
342                 </select>
343               </td>
344               <td>
345                 <select
346                   onChange={this.onChangeFrom}
347                   className="asdcs-designer-select-message-type"
348                   value={combinedValue}
349                   onChange={this.onChangeType}
350                 >
351                   <option value="REQUEST_SYNC">⇾</option>
352                   <option value="REQUEST_ASYNC">→</option>
353                   <option value="RESPONSE">⇠</option>
354                 </select>
355               </td>
356               <td>
357                 <select
358                   onChange={this.onChangeFrom}
359                   className="asdcs-designer-select-message-to"
360                   value={to.id}
361                   onChange={this.onChangeTo}
362                 >
363                   options={lifelineOptions}
364                 </select>
365               </td>
366             </tr>
367           </tbody>
368         </table>
369
370       </div>
371     ));
372   }
373
374   // ///////////////////////////////////////////////////////////////////////////////////////////////
375
376   /**
377    * Render view.
378    * @returns {*}
379    * @private
380    */
381   renderReactSelect() {
382
383     const message = this.props.message;
384     const from = this.props.from;
385     const to = Common.assertNotNull(this.props.to);
386     const messageNotesActiveClass = message.notes && message.notes.length > 0 ? 'asdcs-active' : '';
387     const combinedValue = this.getMessageSpec(message);
388
389     const lifelineOptions = [];
390     for (const lifeline of this.props.model.unwrap().diagram.lifelines) {
391       lifelineOptions.push({
392         value: lifeline.id,
393         label: lifeline.name,
394       });
395     }
396
397     const activeClass = (this.state.active || this.props.active) ? 'asdcs-active' : '';
398     const { connectDragSource, connectDropTarget } = this.props;
399     return connectDragSource(connectDropTarget(
400
401       <div
402         className={`asdcs-designer-message ${activeClass}`}
403         data-id={message.id}
404         onMouseEnter={this.onMouseEnter}
405         onMouseLeave={this.onMouseLeave}
406       >
407
408         <table className="asdcs-designer-layout asdcs-designer-message-row1">
409           <tbody>
410             <tr>
411               <td>
412                 <div className="asdcs-designer-sort asdcs-designer-icon">
413                   <Icon glyph={iconHandle} />
414                 </div>
415               </td>
416               <td>
417                 <div className="asdcs-designer-message-index">{message.index}.</div>
418               </td>
419               <td>
420                 <div className="asdcs-designer-message-name">
421                   <input
422                     type="text"
423                     className="asdcs-editable"
424                     value={this.state.name}
425                     placeholder="Unnamed"
426                     onBlur={this.onBlurName}
427                     onChange={this.onChangeName}
428                   />
429                 </div>
430               </td>
431               <td>
432                 <div className="asdcs-designer-actions">
433                   <div
434                     className="asdcs-designer-settings asdcs-designer-icon"
435                     onClick={this.onClickActions}
436                   >
437                     <Icon glyph={iconSettings} />
438                   </div>
439                   <div
440                     className={`asdcs-designer-notes asdcs-designer-icon ${messageNotesActiveClass}`}
441                     onClick={this.onClickNotes}
442                   >
443                     <Icon glyph={iconNotes} />
444                   </div>
445                   <div
446                     className="asdcs-designer-delete asdcs-designer-icon"
447                     onClick={this.onClickDelete}
448                   >
449                     <Icon glyph={iconDelete} />
450                   </div>
451                 </div>
452               </td>
453             </tr>
454           </tbody>
455         </table>
456
457         <table className="asdcs-designer-layout asdcs-designer-message-row2">
458           <tbody>
459             <tr>
460               <td>
461                 <Select
462                   className="asdcs-editable-select asdcs-designer-editable-message-from"
463                   openOnFocus
464                   clearable={false}
465                   searchable={false}
466                   value={from.id}
467                   onChange={this.onChangeFrom}
468                   options={lifelineOptions}
469                 />
470               </td>
471               <td>
472                 <Select
473                   className="asdcs-editable-select asdcs-designer-editable-message-type"
474                   openOnFocus
475                   clearable={false}
476                   searchable={false}
477                   value={combinedValue}
478                   onChange={this.onChangeType}
479                   options={this.combinedOptions}
480                   optionRenderer={this.renderOption}
481                   valueRenderer={this.renderOption}
482                 />
483               </td>
484               <td>
485                 <Select
486                   className="asdcs-editable-select asdcs-designer-editable-message-to"
487                   openOnFocus
488                   clearable={false}
489                   searchable={false}
490                   value={to.id}
491                   onChange={this.onChangeTo}
492                   options={lifelineOptions}
493                 />
494               </td>
495
496             </tr>
497           </tbody>
498         </table>
499
500       </div>
501     ));
502   }
503
504   // ///////////////////////////////////////////////////////////////////////////////////////////////
505
506   render() {
507     const options = this.props.application.getOptions();
508     if (options.useHtmlSelect) {
509       return this.renderHTMLSelect();
510     }
511     return this.renderReactSelect();
512   }
513 }
514
515 /**
516  * Declare properties.
517  * @type {{designer: *, message: *, from: *, to: *, model: *, connectDragSource: *}}
518  */
519 Message.propTypes = {
520   application: PropTypes.object.isRequired,
521   designer: PropTypes.object.isRequired,
522   message: PropTypes.object.isRequired,
523   active: PropTypes.bool.isRequired,
524   from: PropTypes.object.isRequired,
525   to: PropTypes.object.isRequired,
526   model: PropTypes.object.isRequired,
527   index: PropTypes.number.isRequired,
528   messages: PropTypes.object.isRequired,
529   connectDragSource: PropTypes.func.isRequired,
530   connectDropTarget: PropTypes.func.isRequired,
531 };
532
533 /** DND. */
534 const source = {
535   beginDrag(props) {
536     return {
537       id: props.id,
538       index: props.index,
539     };
540   },
541 };
542
543 /** DND. */
544 const sourceCollect = function collection(connect, monitor) {
545   return {
546     connectDragSource: connect.dragSource(),
547     isDragging: monitor.isDragging(),
548   };
549 };
550
551
552 /** DND. */
553 const target = {
554   drop(props, monitor, component) {
555     Common.assertNotNull(props);
556     Common.assertNotNull(monitor);
557     const decorated = component.getDecoratedComponentInstance();
558     if (decorated) {
559       const messages = decorated.props.messages;
560       if (messages) {
561         const dragIndex = monitor.getItem().index;
562         const hoverIndex = messages.getHoverIndex();
563         messages.onDrop(dragIndex, hoverIndex);
564       }
565     }
566   },
567   hover(props, monitor, component) {
568     Common.assertNotNull(props);
569     Common.assertNotNull(monitor);
570     if (component) {
571       const decorated = component.getDecoratedComponentInstance();
572       if (decorated) {
573         decorated.props.messages.setHoverIndex(decorated.props.index);
574       }
575     }
576   },
577 };
578
579 /** DND. */
580 function targetCollect(connect, monitor) {
581   return {
582     connectDropTarget: connect.dropTarget(),
583     isOver: monitor.isOver(),
584   };
585 }
586
587 const wrapper = DragSource('message', source, sourceCollect)(Message);
588 export default DropTarget(['message', 'message-new'], target, targetCollect)(wrapper);