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