diff options
author | Michael Lando <ml636r@att.com> | 2017-02-19 12:57:33 +0200 |
---|---|---|
committer | Michael Lando <ml636r@att.com> | 2017-02-19 13:47:13 +0200 |
commit | efa037d34be7b1570efdc767c79fad8d4005f10e (patch) | |
tree | cf1036ba2728dea8a61492b678fa91954e629403 /dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components | |
parent | f5f13c4f6b6fe3b4d98e349dfd7db59339803436 (diff) |
Add new code new version
Change-Id: Ic02a76313503b526f17c3df29eb387a29fe6a42a
Signed-off-by: Michael Lando <ml636r@att.com>
Diffstat (limited to 'dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components')
25 files changed, 4533 insertions, 0 deletions
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx new file mode 100644 index 0000000000..20b06922c8 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx @@ -0,0 +1,268 @@ + +import React from 'react'; + +import Common from '../../common/Common'; +import Logger from '../../common/Logger'; +import Diagram from '../diagram/Diagram'; +import Dialog from '../dialog/Dialog'; +import Editor from '../editor/Editor'; +import Export from '../export/Export'; +import Overlay from '../overlay/Overlay'; + +/** + * Application controller, also a view. + */ +export default class Application extends React.Component { + + /** + * Construct application view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + + this.sequencer = Common.assertNotNull(props.sequencer); + this.model = this.sequencer.getModel(); + this.metamodel = this.sequencer.getMetamodel(); + this.options = props.options; + Logger.setLevel(this.options.unwrap().log.level); + + // Bindings. + + this.showInfoDialog = this.showInfoDialog.bind(this); + this.showEditDialog = this.showEditDialog.bind(this); + this.showConfirmDialog = this.showConfirmDialog.bind(this); + this.hideOverlay = this.hideOverlay.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get application options. + * @returns JSON options, see Options.js. + */ + getOptions() { + return this.options.unwrap(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set diagram name. + * @param n diagram (human-readable) name. + */ + setName(n) { + this.diagram.setName(n); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set diagram model. + * @param model diagram instance. + */ + setModel(model) { + + Common.assertNotNull(model); + + this.model = model; + + if (this.editor) { + this.editor.render(); + } + + if (this.diagram) { + this.diagram.render(); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get Model wrapper. + * @returns Model. + */ + getModel() { + return this.model; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get SVG element. + * @returns {*} + */ + getSVG() { + return this.diagram.getSVG(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get top-level widget. Provides the demo toolbar with access to the public API. + * @returns {*} + */ + getSequencer() { + return this.sequencer; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present info dialog. + * @param msg info message. + */ + showInfoDialog(msg) { + this.dialog.showInfoDialog(msg); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present error dialog. + * @param msg error message. + */ + showErrorDialog(msg) { + this.dialog.showErrorDialog(msg); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present confirmation dialog. + * @param msg info message. + * @param cb callback function to be invoked on OK. + */ + showConfirmDialog(msg, cb) { + Common.assertType(cb, 'Function'); + this.dialog.showConfirmDialog(msg, cb); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present edit (textarea) dialog. + * @param msg prompt. + * @param text current edit text. + * @param cb callback function to be invoked on OK, taking the updated text + * as an argument. + */ + showEditDialog(msg, text, cb) { + this.dialog.showEditDialog(msg, text, cb); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select lifeline by ID. + * @param id lifeline ID. + */ + selectLifeline(id) { + if (this.editor) { + this.editor.selectLifeline(id); + } + if (this.diagram) { + this.diagram.selectLifeline(id); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select message by ID. + * @param id message ID. + */ + selectMessage(id) { + if (this.editor) { + this.editor.selectMessage(id); + } + if (this.diagram) { + this.diagram.selectMessage(id); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * (Re)render just the diagram. + */ + renderDiagram() { + this.diagram.redraw(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show overlay between application and modal dialog. + */ + showOverlay() { + if (this.overlay) { + this.overlay.setVisible(true); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Hide overlay between application and modal dialog. + */ + hideOverlay() { + if (this.overlay) { + this.overlay.setVisible(false); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Capture mouse move events, for resize. + * @param event move event. + */ + onMouseMove(event) { + if (this.editor) { + this.editor.onMouseMove(event); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Propagate mouse event to the editor that manages the resize. + */ + onMouseUp() { + if (this.editor) { + this.editor.onMouseUp(); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render current model state. + */ + render() { + + return ( + + <div className="asdcs-control" onMouseMove={this.onMouseMove} onMouseUp={this.onMouseUp}> + + <Editor application={this} ref={(r) => { this.editor = r; }} /> + <Diagram application={this} ref={(r) => { this.diagram = r; }} /> + <Dialog application={this} ref={(r) => { this.dialog = r; }} /> + <Export /> + <Overlay application={this} ref={(r) => { this.overlay = r; }} /> + + </div> + ); + } + +} + +/** React properties. */ +Application.propTypes = { + options: React.PropTypes.object.isRequired, + sequencer: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx new file mode 100644 index 0000000000..f2da7a5a1b --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx @@ -0,0 +1,896 @@ + +import React from 'react'; +import _template from 'lodash/template'; +import _merge from 'lodash/merge'; +import d3 from 'd3'; + +import Common from '../../common/Common'; +import Logger from '../../common/Logger'; +import Popup from './components/popup/Popup'; + +/** + * SVG diagram view. + */ +export default class Diagram extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct React view. + * @param props properties. + * @param context context. + */ + constructor(props, context) { + super(props, context); + + this.application = Common.assertNotNull(props.application); + this.options = this.application.getOptions().diagram; + + this.events = {}; + this.state = { + height: 0, + width: 0, + }; + + this.templates = { + diagram: _template(require('./templates/diagram.html')), + lifeline: _template(require('./templates/lifeline.html')), + message: _template(require('./templates/message.html')), + occurrence: _template(require('./templates/occurrence.html')), + fragment: _template(require('./templates/fragment.html')), + title: _template(require('./templates/title.html')), + }; + + this.handleResize = this.handleResize.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set diagram name. + * @param n name. + */ + setName(n) { + this.svg.select('').text(n); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get SVG from diagram. + * @returns {*|string} + */ + getSVG() { + const svg = this.svg.node().outerHTML; + return svg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" '); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select message by ID. + * @param id message ID. + */ + selectMessage(id) { + const sel = this.svg.selectAll('g.asdcs-diagram-message-container'); + sel.classed('asdcs-active', false); + sel.selectAll('rect.asdcs-diagram-message-bg').attr('filter', null); + if (id) { + const parent = this.svg.select(`g.asdcs-diagram-message-container[data-id="${id}"]`); + parent.classed('asdcs-active', true); + parent.selectAll('rect.asdcs-diagram-message-bg').attr('filter', 'url(#asdcsSvgHighlight)'); + } + this._showNotesPopup(id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select lifeline by ID. + * @param id lifeline ID. + */ + selectLifeline(id) { + const sel = this.svg.selectAll('g.asdcs-diagram-lifeline-container'); + sel.classed('asdcs-active', false); + sel.selectAll('rect').attr('filter', null); + if (id) { + const parent = this.svg.select(`g.asdcs-diagram-lifeline-container[data-id="${id}"]`); + parent.selectAll('rect').attr('filter', 'url(#asdcsSvgHighlight)'); + parent.classed('asdcs-active', true); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle resize, including initial sizing. + */ + handleResize() { + if (this.wrapper) { + const height = this.wrapper.offsetHeight; + const width = this.wrapper.offsetWidth; + if (this.state.height !== height || this.state.width !== width) { + this.setState({ height, width }); + } + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * (Re)render diagram. + */ + render() { + + const model = this.application.getModel(); + const modelJSON = model.unwrap(); + const name = modelJSON.diagram.metadata.name; + const options = this.application.getOptions(); + const titleHeight = options.diagram.title.height; + const titleClass = (titleHeight && titleHeight > 0) ? `height:${titleHeight}` : 'asdcs-hidden'; + + return ( + <div className="asdcs-diagram"> + <div className={`asdcs-diagram-name ${titleClass}`}>{name}</div> + <div className="asdcs-diagram-svg" ref={(r) => { this.wrapper = r; }}></div> + <Popup visible={false} ref={(r) => { this.popup = r; }} /> + </div> + ); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + redraw() { + this.updateSVG(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initial render. + */ + componentDidMount() { + window.addEventListener('resize', this.handleResize); + this.updateSVG(); + + // Insurance: + + setTimeout(() => { + this.handleResize(); + }, 500); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render on update. + */ + componentDidUpdate() { + this.updateSVG(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Redraw SVG diagram. So far it's fast enough that it doesn't seem to matter whether + * it's completely redrawn. + */ + updateSVG() { + + if (!this.svg) { + const svgparams = _merge({}, this.options.svg); + this.wrapper.innerHTML = this.templates.diagram(svgparams); + this.svg = d3.select(this.wrapper).select('svg'); + } + + if (this.state.height === 0) { + + // We'll get a resize event, and the height will be non-zero when it's actually time. + + return; + } + + if (this.state.height && this.state.width) { + const margin = this.options.svg.margin; + const x = -margin; + const y = -margin; + const height = this.state.height + (margin * 2); + const width = this.state.width + (margin * 2); + const viewBox = `${x} ${y} ${width} ${height}`; + this.svg.attr('viewBox', viewBox); + } + + + // If we've already rendered, then save the current scale/translate so that we + // can reapply it after rendering. + + const gContentSelection = this.svg.selectAll('g.asdcs-diagram-content'); + if (gContentSelection.size() === 1) { + const transform = gContentSelection.attr('transform'); + if (transform) { + this.savedTransform = transform; + } + } + + // Empty the document. We're starting again. + + this.svg.selectAll('.asdcs-diagram-content').remove(); + + // Extract the model. + + const model = this.application.getModel(); + if (!model) { + return; + } + const modelJSON = model.unwrap(); + + // Extract dimension options. + + const header = this.options.lifelines.header; + const spacing = this.options.lifelines.spacing; + + // Make separate container elements so that we can control Z order. + + const gContent = this.svg.append('g').attr('class', 'asdcs-diagram-content'); + const gLifelines = gContent.append('g').attr('class', 'asdcs-diagram-lifelines'); + const gCanvas = gContent.append('g').attr('class', 'asdcs-diagram-canvas'); + gCanvas.append('g').attr('class', 'asdcs-diagram-occurrences'); + gCanvas.append('g').attr('class', 'asdcs-diagram-fragments'); + gCanvas.append('g').attr('class', 'asdcs-diagram-messages'); + + // Lifelines ----------------------------------------------------------------------------------- + + const actorsById = {}; + const positionsByMessageId = {}; + const lifelines = []; + for (const actor of modelJSON.diagram.lifelines) { + const x = (header.width / 2) + (lifelines.length * spacing.horizontal); + Diagram._processLifeline(actor, x); + lifelines.push({ x, actor }); + actorsById[actor.id] = actor; + } + + // Messages ------------------------------------------------------------------------------------ + + // Analyze occurrence information. + + const occurrences = model.analyzeOccurrences(); + const fragments = model.analyzeFragments(); + let y = this.options.lifelines.header.height + spacing.vertical; + let messageIndex = 0; + for (const step of modelJSON.diagram.steps) { + if (step.message) { + positionsByMessageId[step.message.id] = positionsByMessageId[step.message.id] || {}; + positionsByMessageId[step.message.id].y = y; + this._drawMessage(gCanvas, step.message, y, actorsById, + positionsByMessageId, ++messageIndex, occurrences, fragments); + } + y += spacing.vertical; + } + + // --------------------------------------------------------------------------------------------- + + // Draw the actual (dashed) lifelines in a background <g>. + + this._drawLifelines(gLifelines, lifelines, y); + + // Initialize mouse event handlers. + + this._initMouseEvents(gLifelines, gCanvas); + + // Scale to fit. + + const bb = gContent.node().getBBox(); + this._initZoom(gContent, bb.width, bb.height); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw message into SVG canvas. + * @param gCanvas container. + * @param message message to be rendered. + * @param y current y position. + * @param actorsById actor lookup. + * @param positionsByMessageId x- and y-position of each message. + * @param messageIndex where we are in the set of messages to be rendered. + * @param oData occurrences info. + * @param fData fragments info. + * @private + */ + _drawMessage(gCanvas, message, y, actorsById, positionsByMessageId, + messageIndex, oData, fData) { + + Common.assertNotNull(oData); + + const request = message.type === 'request'; + const fromActor = request ? actorsById[message.from] : actorsById[message.to]; + const toActor = request ? actorsById[message.to] : actorsById[message.from]; + + if (!fromActor) { + Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'from' not found.`); + return; + } + + if (!toActor) { + Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'to' not found.`); + return; + } + + // Occurrences. -------------------------------------------------------------------------------- + + if (message.occurrence) { + Logger.debug(`Found occurrence for ${message.name}: ${JSON.stringify(message.occurrence)}`); + } + const activeTo = Diagram._calcActive(oData, toActor.id); + this._drawOccurrence(gCanvas, oData, positionsByMessageId, fromActor, message.id); + this._drawOccurrence(gCanvas, oData, positionsByMessageId, toActor, message.id); + const activeFrom = Diagram._calcActive(oData, fromActor.id); + + // Messages. ----------------------------------------------------------------------------------- + + const gMessages = gCanvas.select('g.asdcs-diagram-messages'); + + // Save positions for later. + + const positions = positionsByMessageId[message.id]; + positions.x0 = fromActor.x; + positions.x1 = toActor.x; + + // Calculate. + + const leftToRight = fromActor.x < toActor.x; + const loopback = (message.to === message.from); + const x1 = this._calcMessageX(activeTo, toActor.x, true, leftToRight); + const x0 = loopback ? x1 : this._calcMessageX(activeFrom, fromActor.x, false, leftToRight); + + let messagePath; + if (loopback) { + + // To self. + + messagePath = `M${x1},${y}`; + messagePath = `${messagePath} L${x1 + 200},${y}`; + messagePath = `${messagePath} L${x1 + 200},${y + 50}`; + messagePath = `${messagePath} L${x1},${y + 50}`; + } else { + + // Between lifelines. + + messagePath = `M${x0},${y}`; + messagePath = `${messagePath} L${x1},${y}`; + } + + const styles = Diagram._getMessageStyles(message); + + // Split message over lines. + + const messageWithPrefix = `${messageIndex}. ${message.name}`; + const maxLines = this.options.messages.label.maxLines; + const wrapWords = this.options.messages.label.wrapWords; + const wrapLines = this.options.messages.label.wrapLines; + const messageLines = Common.tokenize(messageWithPrefix, wrapWords, wrapLines, maxLines); + + const messageTxt = this.templates.message({ + id: message.id, + classes: styles.css, + marker: styles.marker, + dasharray: styles.dasharray, + labels: messageLines, + lines: maxLines, + path: messagePath, + index: messageIndex, + x0, x1, y, + }); + + const messageEl = Common.txt2dom(messageTxt); + const gMessage = gMessages.append('g'); + Common.dom2svg(messageEl, gMessage); + + // Set the background's bounding box to that of the text, + // so that they fit snugly. + + const labelBB = gMessage.select('.asdcs-diagram-message-label').node().getBBox(); + gMessage.select('.asdcs-diagram-message-label-bg') + .attr('x', labelBB.x) + .attr('y', labelBB.y) + .attr('height', labelBB.height) + .attr('width', labelBB.width); + + // Fragments. ---------------------------------------------------------------------------------- + + const fragment = fData[message.id]; + if (fragment) { + + // It ends on this message. + + this._drawFragment(gCanvas, fragment, positionsByMessageId); + + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw a single occurrence. + * @param gCanvas container. + * @param oData occurrence data. + * @param positionsByMessageId map of y positions by message ID. + * @param actor wrapper containing lifeline ID (.id), position (.x) and name (.name). + * @param messageId message identifier. + * @private + */ + _drawOccurrence(gCanvas, oData, positionsByMessageId, actor, messageId) { + + Common.assertType(oData, 'Object'); + Common.assertType(positionsByMessageId, 'Object'); + Common.assertType(actor, 'Object'); + Common.assertType(messageId, 'String'); + + const gOccurrences = gCanvas.select('g.asdcs-diagram-occurrences'); + + const oOptions = this.options.lifelines.occurrences; + const oWidth = oOptions.width; + const oHalfWidth = oWidth / 2; + const oForeshortening = oOptions.foreshortening; + const oMarginTop = oOptions.marginTop; + const oMarginBottom = oOptions.marginBottom; + const o = oData[actor.id]; + + const active = Diagram._calcActive(oData, actor.id); + + const x = (actor.x - oHalfWidth) + (active * oWidth); + const positions = positionsByMessageId[messageId]; + const y = positions.y; + + let draw = true; + if (o) { + + if (o.start[messageId]) { + + // Starting, but drawing nothing until we find the end. + + o.active.push(messageId); + draw = false; + + } else if (active > 0) { + + const startMessageId = o.stop[messageId]; + if (startMessageId) { + + // OK, it ends here. Draw the occurrence box. + + o.active.pop(); + const foreshorteningY = active * oForeshortening; + const startY = positionsByMessageId[startMessageId].y; + const height = ((oMarginTop + oMarginBottom) + (y - startY)) - (foreshorteningY * 2); + const oProps = { + x: (actor.x - oHalfWidth) + ((active - 1) * oWidth), + y: ((startY - oMarginTop) + foreshorteningY), + height, + width: oWidth, + }; + + const occurrenceTxt = this.templates.occurrence(oProps); + const occurrenceEl = Common.txt2dom(occurrenceTxt); + Common.dom2svg(occurrenceEl, gOccurrences.append('g')); + + } + draw = false; + } + } + + if (draw) { + + // Seems this is a singleton occurrence. We just draw a wee box around it. + + const height = (oMarginTop + oMarginBottom); + const occurrenceProperties = { x, y: y - oMarginTop, height, width: oWidth }; + const defaultTxt = this.templates.occurrence(occurrenceProperties); + const defaultEl = Common.txt2dom(defaultTxt); + Common.dom2svg(defaultEl, gOccurrences.append('g')); + } + + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw box(es) around fragment(s). + * @param gCanvas container. + * @param fragment fragment definition, corresponding to its final (stop) message. + * @param positionsByMessageId message dimensions. + * @private + */ + _drawFragment(gCanvas, fragment, positionsByMessageId) { + + const optFragments = this.options.fragments; + const gFragments = gCanvas.select('g.asdcs-diagram-fragments'); + const p1 = positionsByMessageId[fragment.stop]; + if (p1 && fragment.start && fragment.start.length > 0) { + + for (const start of fragment.start) { + + const message = this.application.getModel().getMessageById(start); + const bounds = this._calcFragmentBounds(message, fragment, positionsByMessageId); + if (bounds) { + + const maxLines = this.options.fragments.label.maxLines; + const wrapWords = this.options.fragments.label.wrapWords; + const wrapLines = this.options.fragments.label.wrapLines; + const lines = Common.tokenize(message.fragment.guard, wrapWords, wrapLines, maxLines); + + const params = { + id: start, + x: bounds.x0 - optFragments.leftMargin, + y: bounds.y0 - optFragments.topMargin, + height: (bounds.y1 - bounds.y0) + optFragments.heightMargin, + width: (bounds.x1 - bounds.x0) + optFragments.widthMargin, + operator: (message.fragment.operator || 'alt'), + lines, + }; + + const fragmentTxt = this.templates.fragment(params); + const fragmentEl = Common.txt2dom(fragmentTxt); + const gFragment = gFragments.append('g'); + Common.dom2svg(fragmentEl, gFragment); + + const labelBB = gFragment.select('.asdcs-diagram-fragment-guard').node().getBBox(); + gFragment.select('.asdcs-diagram-fragment-guard-bg') + .attr('x', labelBB.x) + .attr('y', labelBB.y) + .attr('height', labelBB.height) + .attr('width', labelBB.width); + + } else { + Logger.warn(`Bad fragment: ${JSON.stringify(fragment)}`); + } + } + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + _calcFragmentBounds(startMessage, fragment, positionsByMessageId) { + if (startMessage) { + const steps = this.application.getModel().unwrap().diagram.steps; + const bounds = { x0: 99999, x1: 0, y0: 99999, y1: 0 }; + let foundStart = false; + let foundStop = false; + for (const step of steps) { + const message = step.message; + if (message) { + if (message.id === startMessage.id) { + foundStart = true; + } + if (foundStart && !foundStop) { + const positions = positionsByMessageId[message.id]; + if (positions) { + bounds.x0 = Math.min(bounds.x0, Math.min(positions.x0, positions.x1)); + bounds.y0 = Math.min(bounds.y0, positions.y); + bounds.x1 = Math.max(bounds.x1, Math.max(positions.x0, positions.x1)); + bounds.y1 = Math.max(bounds.y1, positions.y); + } else { + // This probably means it hasn't been recorded yet, which is fine, because + // we draw fragments from where they END. + foundStop = true; + } + } + + if (message.id === fragment.stop) { + foundStop = true; + } + } + } + return bounds; + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw all lifelines. + * @param gLifelines lifelines container. + * @param lifelines lifelines definitions. + * @param y height. + * @private + */ + _drawLifelines(gLifelines, lifelines, y) { + + const maxLines = this.options.lifelines.header.maxLines; + const wrapWords = this.options.lifelines.header.wrapWords; + const wrapLines = this.options.lifelines.header.wrapLines; + + for (const lifeline of lifelines) { + const lines = Common.tokenize(lifeline.actor.name, wrapWords, wrapLines, maxLines); + const lifelineTxt = this.templates.lifeline({ + x: lifeline.x, + y0: 0, + y1: y, + lines, + rows: maxLines, + headerHeight: this.options.lifelines.header.height, + headerWidth: this.options.lifelines.header.width, + id: lifeline.actor.id, + }); + + const lifelineEl = Common.txt2dom(lifelineTxt); + Common.dom2svg(lifelineEl, gLifelines.append('g')); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initialize all mouse events. + * @param gLifelines lifelines container. + * @param gCanvas top-level canvas container. + * @private + */ + _initMouseEvents(gLifelines, gCanvas) { + + const self = this; + const source = 'asdcs'; + const origin = `${window.location.protocol}//${window.location.host}`; + + let timer; + gLifelines.selectAll('.asdcs-diagram-lifeline-selectable') + .on('mouseenter', function f() { + timer = setTimeout(() => { + self.application.selectLifeline(d3.select(this.parentNode).attr('data-id')); + }, 150); + }) + .on('mouseleave', () => { + clearTimeout(timer); + self.application.selectLifeline(); + }) + .on('click', function f() { + const id = d3.select(this.parentNode).attr('data-id'); + window.postMessage({ source, id, type: 'lifeline' }, origin); + }); + + gLifelines.selectAll('.asdcs-diagram-lifeline-heading-box') + .on('mouseenter', function f() { + timer = setTimeout(() => { + self.application.selectLifeline(d3.select(this.parentNode).attr('data-id')); + }, 150); + }) + .on('mouseleave', () => { + clearTimeout(timer); + self.application.selectLifeline(); + }) + .on('click', function f() { + const id = d3.select(this.parentNode).attr('data-id'); + window.postMessage({ source, id, type: 'lifelineHeader' }, origin); + }); + + gCanvas.selectAll('.asdcs-diagram-message-selectable') + .on('mouseenter', function f() { + self.events.message = { x: d3.event.pageX, y: d3.event.pageY }; + timer = setTimeout(() => { + self.application.selectMessage(d3.select(this.parentNode).attr('data-id')); + }, 200); + }) + .on('mouseleave', () => { + delete self.events.message; + clearTimeout(timer); + self.application.selectMessage(); + }) + .on('click', function f() { + const id = d3.select(this.parentNode).attr('data-id'); + window.postMessage({ source, id, type: 'message' }, origin); + }); + + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get CSS classes to be applied to a message, according to whether request/response + * or synchronous/asynchronous. + * @param message message being rendered. + * @returns CSS class name(s). + * @private + */ + static _getMessageStyles(message) { + + let marker = 'asdcsDiagramArrowSolid'; + let dasharray = ''; + let css = 'asdcs-diagram-message'; + if (message.type === 'request') { + css = `${css} asdcs-diagram-message-request`; + } else { + css = `${css} asdcs-diagram-message-response`; + marker = 'asdcsDiagramArrowOpen'; + dasharray = '30, 10'; + } + + if (message.asynchronous) { + css = `${css} asdcs-diagram-message-asynchronous`; + marker = 'asdcsDiagramArrowOpen'; + } else { + css = `${css} asdcs-diagram-message-synchronous`; + } + + return { css, marker, dasharray }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initialize or reinitialize zoom. This sets the initial zoom in the case of + * a re-rendering, and initializes the eventhandling in all cases. + * + * It does some fairly risky parsing of the 'transform' attribute, assuming that it + * can contain scale() and translate(). But only the zoom handler and us are writing + * the transform values, so that's probably OK. + * + * @param gContent container. + * @param width diagram width. + * @param height diagram height. + * @private + */ + _initZoom(gContent, width, height) { + + const zoomed = function zoomed() { + gContent.attr('transform', + `translate(${d3.event.translate})scale(${d3.event.scale})`); + }; + + const viewWidth = this.state.width || this.options.svg.width; + const viewHeight = this.state.height || this.options.svg.height; + const scaleMinimum = this.options.svg.scale.minimum; + const scaleWidth = viewWidth / width; + const scaleHeight = viewHeight / height; + + let scale = scaleMinimum; + if (this.options.svg.scale.width) { + scale = Math.max(scale, scaleWidth); + } + if (this.options.svg.scale.height) { + scale = Math.min(scale, scaleHeight); + } + + scale = Math.max(scale, scaleMinimum); + + let translate = [0, 0]; + if (this.savedTransform) { + const s = this.savedTransform; + const scaleStart = s.indexOf('scale('); + if (scaleStart !== -1) { + scale = parseFloat(s.substring(scaleStart + 6, s.length - 1)); + } + const translateStart = s.indexOf('translate('); + if (translateStart !== -1) { + const spec = s.substring(translateStart + 10, s.indexOf(')', translateStart)); + const tokens = spec.split(','); + translate = [parseFloat(tokens[0]), parseFloat(tokens[1])]; + } + + gContent.attr('transform', this.savedTransform); + } else { + gContent.attr('transform', `scale(${scale})`); + } + + const zoom = d3.behavior.zoom() + .scale(scale) + .scaleExtent([scaleMinimum, 10]) + .translate(translate) + .on('zoom', zoomed); + this.svg.call(zoom); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Hide from the linter the fact that we're modifying the lifeline. + * @param lifeline to be updated with X position. + * @param x X position. + * @private + */ + static _processLifeline(lifeline, x) { + const actor = lifeline; + actor.id = actor.id || actor.name; + actor.x = x; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Derive active occurrences for lifeline. + * @param oData occurrences data. + * @param lifelineId lifeline to be analyzed. + * @returns {number} + * @private + */ + static _calcActive(oData, lifelineId) { + const o = oData[lifelineId]; + let active = 0; + if (o && o.active) { + active = o.active.length; + } + return active; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Derive the X position of an occurrence on a lifeline, taking into account how + * many occurrences are active. + * @param active active count. + * @param x lifeline X position; basis for offset. + * @param arrow whether this is the arrow (to) end. + * @param leftToRight whether this message goes left-to-right. + * @returns {*} calculated X position for occurrence left-hand side. + * @private + */ + _calcMessageX(active, x, arrow, leftToRight) { + const width = this.options.lifelines.occurrences.width; + const halfWidth = width / 2; + const active0 = Math.max(0, active - 1); + let calculated = x + (active0 * width); + if (arrow) { + // End (ARROW). + if (leftToRight) { + calculated -= halfWidth; + } else { + calculated += halfWidth; + } + } else { + // Start (NOT ARROW). + if (leftToRight) { + calculated += halfWidth; + } else { + calculated -= halfWidth; + } + } + + return calculated; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show popup upon hovering over a messages that has associated notes. + * @param id + * @private + */ + _showNotesPopup(id) { + if (this.popup) { + if (id) { + const message = this.application.getModel().getMessageById(id); + if (message && message.notes && message.notes.length > 0 && this.events.message) { + this.popup.setState({ + visible: true, + left: this.events.message.x - 50, + top: this.events.message.y + 20, + notes: message.notes[0], + }); + } + } else { + this.popup.setState({ visible: false, notes: '' }); + } + } + } +} + + +Diagram.propTypes = { + application: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx new file mode 100644 index 0000000000..08c6da1e76 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx @@ -0,0 +1,94 @@ + + +import React from 'react'; + +import Icon from '../../../icons/Icon'; +import iconEdit from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/edit.svg'; + +/** + * A hover-over popup. It shows notes, but perhaps will be put to other uses. + * @param props React properties. + * @returns {XML} + * @constructor + */ +export default class Popup extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct react view. + * @param props element properties (of which there are none). + * @param context react context. + */ + constructor(props, context) { + super(props, context); + this.state = { + top: 0, + left: 0, + visible: false, + notes: '', + }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render view. + * @returns {XML} + */ + render() { + + // Build CSS + styles to position and configure popup. + + let top = this.state.top; + let left = this.state.left; + + const popupHeight = 200; + const popupWidth = 320; + + let auxCssVertical = 'top'; + let auxCssHorizontal = 'left'; + + if (this.state.top > (window.innerHeight - popupHeight)) { + top -= (popupHeight + 50); + auxCssVertical = 'bottom'; + } + + if (this.state.left > (window.innerWidth - popupWidth)) { + left -= (popupWidth - 80); + auxCssHorizontal = 'right'; + } + + const auxCss = `asdcs-diagram-popup-${auxCssVertical}${auxCssHorizontal}`; + const styles = { + top, + left, + display: (this.state.visible ? 'block' : 'none'), + }; + + // Render element. + + let notes = this.state.notes || ''; + if (notes.length > 255) { + notes = notes.substring(0, 255); + notes = `${notes} ...`; + } + + return ( + <div className={`asdcs-diagram-popup ${auxCss}`} style={styles}> + <div className="asdcs-diagram-popup-header">Notes</div> + <div className="asdcs-diagram-popup-body"> + <div className="asdcs-icon-popup"> + <Icon glyph={iconEdit} /> + </div> + <div className="asdcs-diagram-notes"> + <div className="asdcs-diagram-note"> + {notes} + </div> + </div> + </div> + <div className="asdcs-diagram-popup-footer"></div> + </div> + ); + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html new file mode 100644 index 0000000000..22893ce864 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html @@ -0,0 +1,56 @@ +<svg height="100%" width="100%" viewBox="<%-(x-25)%> <%-(y-25)%> <%-(width+50)%> <%-(height+50)%>" preserveAspectRatio="xMinYMin"> + <defs> + + <filter id="asdcsSvgHighlight" height="50" width="50" x="-25" y="-25"> + <morphology in="SourceAlpha" operator="dilate" radius="25"></morphology> + <feGaussianBlur result="blur" stdDeviation="20"></feGaussianBlur> + <feComposite in2="SourceAlpha" k2="1" k3="-1" operator="arithmetic" result="hlDiff"></feComposite> + <feFlood flood-color="<%-floodColor%>" flood-opacity="1"></feFlood> + <feComposite in2="hlDiff" operator="in"></feComposite> + <feComposite in2="SourceGraphic" operator="over" result="withGlow"></feComposite> + </filter> + + <marker id="asdcsDiagramArrowOpen" + viewBox="0 0 15 15" + refX="12" + refY="4" + markerWidth="15" + markerHeight="20" + orient="auto"> + <path d="M0,0 L12,4 L0,8" class="asdcs-diagram-arrow asdcs-diagram-arrow-open" /> + </marker> + + <marker id="asdcsDiagramArrowClosed" + viewBox="0 0 15 15" + refX="12" + refY="4" + markerWidth="15" + markerHeight="20" + orient="auto"> + <path d="M0,0 L12,4 L0,8 Z" class="asdcs-diagram-arrow asdcs-diagram-arrow-open" /> + </marker> + + <marker id="asdcsDiagramArrowSolid" + viewBox="0 0 15 15" + refX="12" + refY="4" + markerWidth="15" + markerHeight="20" + orient="auto"> + <path d="M0,0 L12,4 L0,8 Z" class="asdcs-diagram-arrow asdcs-diagram-arrow-solid" /> + </marker> + + <!-- + <marker id="asdcsDiagramArrowSolid" + viewBox="0 0 20 20" + refX="20" + refY="6" + markerWidth="20" + markerHeight="20" + orient="auto"> + <path d="M0,0 L18,6 L0,12 Z" class="asdcs-diagram-arrow asdcs-diagram-arrow-solid" /> + </marker> + --> + + </defs> +</svg> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html new file mode 100644 index 0000000000..812f5fcfb8 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html @@ -0,0 +1,18 @@ +<g class="asdcs-diagram-fragment" data-id="<%-id%>" data-type="fragment"> + + <rect x="<%-x%>" y="<%-y%>" height="<%-height%>" width="<%-width%>"></rect> + + <path d="M<%-x%>,<%-(y+80)%> L<%-(x+100)%>,<%-(y+80)%> L<%-(x+120)%>,<%-(y+60)%> L<%-(x+120)%>,<%-y%>"/> + + <text x="<%-(x+20)%>" y="<%-(y+50)%>" class="asdcs-diagram-fragment-operation"><%-operator%></text> + + <rect class="asdcs-diagram-fragment-guard-bg" + x="0" y="0" height="0" width="0" + rx="5" ry="5" ></rect> + + <text class="asdcs-diagram-fragment-guard" x="<%-(x+160)%>" y="<%-(y+10)%>"><% + for (var lineIndex = 0; lineIndex < lines.length ; lineIndex++) { + %><tspan x="<%-(x+160)%>" dy="40px"><%- lines[lineIndex] %></tspan><% + }%></text> + +</g> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html new file mode 100644 index 0000000000..cd01d42c5a --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html @@ -0,0 +1,19 @@ +<g class="asdcs-diagram-lifeline-container" data-id="<%-id%>" data-type="lifeline"> + + <rect x="<%-(x-(headerWidth/2))%>" y="<%-(y0)%>" width="<%-headerWidth%>" height="<%-headerHeight%>" class="asdcs-diagram-lifeline-heading-box"></rect> + + <text x="<%-x%>" y="<%-(headerHeight * ((rows-lines.length)/10))%>" class="asdcs-diagram-lifeline-heading-label"><% + for (var linesIndex = 0; linesIndex < lines.length && linesIndex < rows ; linesIndex++) { + %><tspan x="<%-x%>" dy="<%-(headerHeight/rows)-5%>px"><%- lines[linesIndex] %></tspan><% + } + %></text> + + <rect x="<%-(x-5)%>" y="<%-(y0+headerHeight)%>" width="10" height="<%-(y1-(y0+headerHeight))%>" class="asdcs-diagram-lifeline-bg"></rect> + + <path d="M<%-x%>,<%-(y0+headerHeight)%> L<%-x%>,<%-y1%>" + class="asdcs-diagram-lifeline-selectable"></path> + + <path d="M<%-x%>,<%-(y0+headerHeight)%> L<%-x%>,<%-y1%>" + class="asdcs-diagram-lifeline"></path> + +</g> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html new file mode 100644 index 0000000000..bd4c33a016 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html @@ -0,0 +1,29 @@ +<g class="asdcs-diagram-message-container" data-id="<%-id%>" data-type="lifeline"> + + <% + var delta = 40; + var x = (x0 + x1) / 2; + var y0 = y - ((labels.length + 1) * delta); + %> + + <rect class="asdcs-diagram-message-label-bg" + x="<%-x%>" + y="<%-y0%>" + height="10" + width="10" + rx="10" ry="10" ></rect> + + <text class="asdcs-diagram-message-label" x="<%-x%>" y="<%-y0%>"><% + for (var labelIndex = 0; labelIndex < labels.length && labelIndex < lines ; labelIndex++) { + %><tspan x="<%-x%>" dy="<%-delta%>px"><%- labels[labelIndex] %></tspan><% + }%></text> + + + <rect x="<%-Math.min(x0,x1)%>" y="<%-(y-5)%>" width="<%-Math.abs(x1-x0)%>" height="10" class="asdcs-diagram-message-bg"></rect> + + <path class="asdcs-diagram-message-selectable" d="<%-path%>"></path> + + <path class="<%-classes%>" marker-end="url(#<%-marker%>)" stroke-dasharray="<%-dasharray%>" + data-id="<%-id%>" data-type="message" d="<%-path%>"></path> + +</g> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html new file mode 100644 index 0000000000..0af9ff3d68 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html @@ -0,0 +1,7 @@ +<g> + <rect class="asdcs-diagram-occurrence" + x="<%-x%>" + y="<%-y%>" + width="<%-width%>" + height="<%-height%>" /> +</g> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html new file mode 100644 index 0000000000..b7a5d68a6d --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html @@ -0,0 +1,3 @@ +<g class="asdcs-diagram-title"> + <text x="<%-x%>" y="<%-y%>"><%-title%></text> +</g> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx new file mode 100644 index 0000000000..4429d80bc6 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx @@ -0,0 +1,222 @@ + + +import React from 'react'; + +import Icon from '../icons/Icon'; +import iconQuestion from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/question.svg'; +import iconExclaim from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/exclaim.svg'; +import iconInfo from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/info.svg'; +import iconEdit from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/edit.svg'; +import iconClose from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/close.svg'; + +/** + * Multi-purpose dialog. Rendered into the page on initialization, and then + * configured, shown and hidden as required. + */ +export default class Dialog extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + */ + constructor(props, context) { + + super(props, context); + + this.MODE = { + INFO: { + icon: 'asdcs-icon-info', + heading: 'Information', + }, + ERROR: { + icon: 'asdcs-icon-exclaim', + heading: 'Error', + }, + EDIT: { + icon: 'asdcs-icon-edit', + heading: 'Edit', + edit: true, + confirm: true, + }, + CONFIRM: { + icon: 'asdcs-icon-question', + heading: 'Confirm', + confirm: true, + }, + }; + + this.state = { + mode: this.MODE.INFO, + message: '', + text: '', + visible: false, + }; + + // Bindings. + + this.onClickOK = this.onClickOK.bind(this); + this.onClickCancel = this.onClickCancel.bind(this); + this.onChangeText = this.onChangeText.bind(this); + this.showConfirmDialog = this.showConfirmDialog.bind(this); + this.showInfoDialog = this.showInfoDialog.bind(this); + this.showEditDialog = this.showEditDialog.bind(this); + this.showErrorDialog = this.showErrorDialog.bind(this); + this.showDialog = this.showDialog.bind(this); + + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show info dialog. + * @param message info message. + */ + showInfoDialog(message) { + this.showDialog(this.MODE.INFO, { message }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show error dialog. + * @param message error message. + */ + showErrorDialog(message) { + this.showDialog(this.MODE.ERROR, { message }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show edit dialog. + * @param message dialog message. + * @param text current edit text. + * @param callback callback function to be invoked on OK. + */ + showEditDialog(message, text, callback) { + this.showDialog(this.MODE.EDIT, { message, text, callback }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show confirmation dialog. + * @param message dialog message. + * @param callback callback function to be invoked on OK. + */ + showConfirmDialog(message, callback) { + this.showDialog(this.MODE.CONFIRM, { message, callback }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle buttonclick. + */ + onClickOK() { + this.props.application.hideOverlay(); + this.setState({ visible: false }); + if (this.callback) { + + // So far the only thing we can return is edit text, but send it back + // as properties to allow for future return values. + + this.callback({ text: this.state.text }); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle buttonclick. + */ + onClickCancel() { + this.props.application.hideOverlay(); + this.setState({ visible: false }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle text changes. + * @param event update event. + */ + onChangeText(event) { + this.setState({ text: event.target.value }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show dialog in specified configuration. + * @param mode dialog mode. + * @param args dialog parameters, varying slightly by dialog type. + * @private + */ + showDialog(mode, args) { + this.props.application.showOverlay(); + this.callback = args.callback; + this.setState({ + mode, + visible: true, + message: args.message || '', + text: args.text || '', + }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render dialog into the page, initially hidden. + */ + render() { + + const dialogClass = (this.state.visible) ? '' : 'asdcs-hidden'; + const cancelClass = (this.callback) ? '' : 'asdcs-hidden'; + const textClass = (this.state.mode === this.MODE.EDIT) ? '' : 'asdcs-hidden'; + + return ( + <div className={`asdcs-dialog ${dialogClass}`}> + <div className="asdcs-dialog-header">{this.state.mode.heading}</div> + <div className="asdcs-dialog-close" onClick={this.onClickCancel} > + <Icon glyph={iconClose} className={this.MODE.CONFIRM.icon} /> + </div> + <div className={`asdcs-dialog-icon ${this.state.mode.icon}`}> + <Icon glyph={iconQuestion} className={this.MODE.CONFIRM.icon} /> + <Icon glyph={iconExclaim} className={this.MODE.ERROR.icon} /> + <Icon glyph={iconInfo} className={this.MODE.INFO.icon} /> + <Icon glyph={iconEdit} className={this.MODE.EDIT.icon} /> + </div> + <div className="asdcs-dialog-message"> + {this.state.message} + </div> + <div className={`asdcs-dialog-text ${textClass}`}> + <textarea + maxLength="255" + value={this.state.text} + onChange={this.onChangeText} + /> + </div> + <div className="asdcs-dialog-buttonbar"> + <button + className={`asdcs-dialog-button-cancel ${cancelClass}`} + onClick={this.onClickCancel} + > + Cancel + </button> + <button + className="asdcs-dialog-button-ok" + onClick={this.onClickOK} + > + OK + </button> + </div> + </div> + ); + } +} + +Dialog.propTypes = { + application: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx new file mode 100644 index 0000000000..09703b84bf --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx @@ -0,0 +1,171 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 Logger from '../../common/Logger'; +import Common from '../../common/Common'; +import Designer from './components/designer/Designer'; +import Toolbar from './components/toolbar/Toolbar'; +import Source from './components/source/Source'; + +/** + * Editor view, aggregating the designer, the code editor, the toolbar. + */ +export default class Editor extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct React view. + * @param props properties. + * @param context context. + */ + constructor(props, context) { + super(props, context); + + this.application = Common.assertNotNull(props.application); + this.demo = this.application.getOptions().demo; + + // Bindings. + + this.selectMessage = this.selectMessage.bind(this); + this.selectLifeline = this.selectLifeline.bind(this); + + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select message by ID. + * @param id message ID. + */ + selectMessage(id) { + if (this.designer) { + this.designer.selectMessage(id); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select lifeline by ID. + * @param id lifeline ID. + */ + selectLifeline(id) { + if (this.designer) { + this.designer.selectLifeline(id); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Record that we're dragging. + */ + onMouseDown() { + if (this.editor) { + this.resize = { + initialWidth: this.editor.offsetWidth, + initialPageX: undefined, + }; + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Record that we're not dragging. + */ + onMouseUp() { + this.resize = undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Record mouse movement. + */ + onMouseMove(event) { + if (this.resize) { + if (this.editor) { + if (this.resize.initialPageX) { + const deltaX = event.pageX - this.resize.initialPageX; + const newWidth = this.resize.initialWidth + deltaX; + const newWidthBounded = Math.min(800, Math.max(400, newWidth)); + this.editor.style.width = `${newWidthBounded}px`; + } else { + this.resize.initialPageX = event.pageX; + } + } + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render editor. + */ + render() { + + Logger.info('Editor.jsx - render()'); + + return ( + + <div + className="asdcs-editor" + ref={(r) => { this.editor = r; }} + > + + <Toolbar application={this.props.application} editor={this} /> + + <div className="asdcs-editor-content"> + <Source application={this.props.application} /> + <Designer + application={this.props.application} + ref={(r) => { + if (r) { + this.designer = r.getDecoratedComponentInstance(); + } else { + this.designer = null; + } + }} + /> + </div> + + <div className="asdcs-editor-statusbar"> + <div className="asdcs-editor-status"></div> + <div className="asdcs-editor-validation"></div> + </div> + + <div + className="asdcs-editor-resize-handle" + onMouseDown={this.onMouseDown} + onMouseUp={this.onMouseUp} + > + </div> + </div> + ); + } +} + +/** Element properties. */ +Editor.propTypes = { + application: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx new file mode 100644 index 0000000000..69cdd17ed5 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx @@ -0,0 +1,403 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 HTML5Backend from 'react-dnd-html5-backend'; +import { DragDropContext } from 'react-dnd'; + +import Common from '../../../../common/Common'; +import Logger from '../../../../common/Logger'; + +import Actions from './components/actions/Actions'; +import Lifelines from './components/lifeline/Lifelines'; +import Messages from './components/message/Messages'; +import Metadata from './components/metadata/Metadata'; + +import Icon from '../../../icons/Icon'; +import iconExpanded from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/expanded.svg'; +import iconCollapsed from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/collapsed.svg'; + +/** + * LHS design wid` view. + */ +class Designer extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + + Logger.noop(); + + this.application = Common.assertNotNull(props.application); + + this.state = { + lifelinesExpanded: false, + messagesExpanded: true, + activeLifelineId: undefined, + activeMessageId: undefined, + }; + + // Bind this. + + this.onToggle = this.onToggle.bind(this); + this.onMouseEnterLifeline = this.onMouseEnterLifeline.bind(this); + this.onMouseLeaveLifeline = this.onMouseLeaveLifeline.bind(this); + this.onMouseEnterMessage = this.onMouseEnterMessage.bind(this); + this.onMouseLeaveMessage = this.onMouseLeaveMessage.bind(this); + + this.addMessage = this.addMessage.bind(this); + this.updateMessage = this.updateMessage.bind(this); + this.deleteMessage = this.deleteMessage.bind(this); + this.addLifeline = this.addLifeline.bind(this); + this.updateLifeline = this.updateLifeline.bind(this); + this.deleteLifeline = this.deleteLifeline.bind(this); + + this.selectMessage = this.selectMessage.bind(this); + this.selectLifeline = this.selectLifeline.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select message by ID. + * @param id message ID. + */ + selectMessage(id) { + + // TODO: scroll into view. + + this.setState({ activeMessageId: id }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select lifeline by ID. + * @param id lifeline ID. + */ + selectLifeline(id) { + + // TODO: scroll into view. + + this.setState({ activeLifelineId: id }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show/hide lifelines section. + */ + onToggle() { + const lifelinesExpanded = !this.state.lifelinesExpanded; + const messagesExpanded = !lifelinesExpanded; + this.setState({ lifelinesExpanded, messagesExpanded }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouse event. + * @param id lifeline identifier. + */ + onMouseEnterLifeline(id) { + this.application.selectLifeline(id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouse event. + */ + onMouseLeaveLifeline() { + this.application.selectLifeline(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouse event. + * @param id message identifier. + */ + onMouseEnterMessage(id) { + this.application.selectMessage(id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouse event. + */ + onMouseLeaveMessage() { + // Only on next selection. + // this.application.selectMessage(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add new message. + */ + addMessage() { + + if (this.application.getModel().unwrap().diagram.lifelines.length < 2) { + self.application.showErrorDialog('You need at least two lifelines.'); + return; + } + + this.application.getModel().addMessage(); + this.forceUpdate(); + this.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Apply property changes to the message identified by props.id. + * @param props properties to be updated (excluding 'id'). + */ + updateMessage(props) { + Common.assertPlainObject(props); + const model = this.application.getModel(); + const message = model.getMessageById(props.id); + if (message) { + for (const k of Object.keys(props)) { + if (k !== 'id') { + message[k] = props[k]; + } + } + } + this.forceUpdate(); + this.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete message after confirmation. + * @param id ID of message to be deleted. + */ + deleteMessage(id) { + + const self = this; + const model = this.application.getModel(); + + const confirmComplete = function f() { + model.deleteMessageById(id); + self.render(); + self.application.renderDiagram(); + }; + + this.application.showConfirmDialog('Delete this message?', + confirmComplete); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add new lifeline. + */ + addLifeline() { + this.application.getModel().addLifeline(); + this.forceUpdate(); + this.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Apply property changes to the lifeline identified by props.id. + * @param props properties to be updated (excluding 'id'). + */ + updateLifeline(props) { + Common.assertPlainObject(props); + const model = this.application.getModel(); + const lifeline = model.getLifelineById(props.id); + if (lifeline) { + for (const k of Object.keys(props)) { + if (k !== 'id') { + lifeline[k] = props[k]; + } + } + } + this.forceUpdate(); + this.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete lifeline after confirmation. + * @param id candidate for deletion. + */ + deleteLifeline(id) { + + const self = this; + const model = this.application.getModel(); + + const confirmComplete = function f() { + model.deleteLifelineById(id); + self.forceUpdate(); + self.application.renderDiagram(); + }; + this.application.showConfirmDialog('Delete this lifeline and all its steps?', + confirmComplete); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render designer. + */ + render() { + + const application = this.props.application; + const model = application.getModel(); + const diagram = model.unwrap().diagram; + const metadata = diagram.metadata; + + const lifelinesIcon = this.state.lifelinesExpanded ? iconExpanded : iconCollapsed; + const lifelinesClass = this.state.lifelinesExpanded ? '' : 'asdcs-hidden'; + const messagesIcon = this.state.messagesExpanded ? iconExpanded : iconCollapsed; + const messagesClass = this.state.messagesExpanded ? '' : 'asdcs-hidden'; + + return ( + + <div className="asdcs-editor-designer"> + <div className="asdcs-designer-accordion"> + + <div className="asdcs-designer-metadata-container"> + <Metadata metadata={metadata} /> + </div> + + <h3 onClick={this.onToggle}>Lifelines + <div className="asdcs-designer-icon" onClick={this.onToggle}> + <Icon glyph={lifelinesIcon} /> + </div> + </h3> + + <div className={`asdcs-designer-lifelines-container ${lifelinesClass}`}> + <Lifelines + application={this.application} + designer={this} + activeLifelineId={this.state.activeLifelineId} + /> + </div> + + <h3 onClick={this.onToggle}>Steps + <div className="asdcs-designer-icon" onClick={this.onToggle}> + <Icon glyph={messagesIcon} /> + </div> + </h3> + + <div className={`asdcs-designer-steps-container ${messagesClass}`} > + <Messages + application={this.application} + designer={this} + activeMessageId={this.state.activeMessageId} + /> + </div> + + </div> + + <Actions + application={this.props.application} + model={model} + ref={(r) => { this.actions = r; }} + /> + + </div> + ); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scroll accordion pane to make + * @param $element focused element. + * @private + */ + static _scrollIntoView($element) { + const $pane = $element.closest('.ui-accordion-content'); + const paneScrollTop = $pane.scrollTop(); + const paneHeight = $pane.height(); + const paneBottom = paneScrollTop + paneHeight; + const elementTop = $element[0].offsetTop - $pane[0].offsetTop; + const elementHeight = $element.height(); + const elementBottom = elementTop + elementHeight; + if (elementBottom > paneBottom) { + $pane.scrollTop(elementTop); + } else if (elementTop < paneScrollTop) { + $pane.scrollTop(elementTop); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show actions menu. + * @param id selected message ID. + * @param position page coordinates. + */ + showActions(id, position) { + if (this.actions) { + this.actions.show(id, position); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show notes popup. + * @param id selected message identifier. + */ + showNotes(id) { + const model = this.application.getModel(); + const options = this.application.getOptions(); + const message = model.getMessageById(id); + const notes = (message.notes && (message.notes.length > 0)) ? message.notes[0] : ''; + const editComplete = function f(p) { + message.notes = []; + if (p && p.text) { + const sanitized = Common.sanitizeText(p.text, options, 'notes'); + message.notes.push(sanitized); + } + }; + this.application.showEditDialog('Notes:', notes, editComplete); + } +} + +/** Element properties. */ +Designer.propTypes = { + application: React.PropTypes.object.isRequired, +}; + +export default DragDropContext(HTML5Backend)(Designer); diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx new file mode 100644 index 0000000000..851da78870 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx @@ -0,0 +1,471 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 Select from 'react-select'; + +import Common from '../../../../../../common/Common'; +import Logger from '../../../../../../common/Logger'; + +import Icon from '../../../../../icons/Icon'; +import iconSettings from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/settings.svg'; +import iconExpanded from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/expanded.svg'; +import iconCollapsed from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/collapsed.svg'; +import iconOccurrenceDefault from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/occurrence-default.svg'; +import iconOccurrenceStart from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/occurrence-start.svg'; +import iconOccurrenceStop from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/occurrence-stop.svg'; +import iconFragmentDefault from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/fragment-default.svg'; +import iconFragmentStart from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/fragment-start.svg'; +import iconFragmentStop from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/fragment-stop.svg'; + +/** + * Action menu view. + */ +export default class Actions extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + + Logger.noop(); + + this.state = { + id: undefined, + visible: false, + }; + + // Bindings. + + this.show = this.show.bind(this); + + this.onClickOccurrenceToggle = this.onClickOccurrenceToggle.bind(this); + this.onClickOccurrenceFrom = this.onClickOccurrenceFrom.bind(this); + this.onClickOccurrenceTo = this.onClickOccurrenceTo.bind(this); + + this.onClickFragmentToggle = this.onClickFragmentToggle.bind(this); + this.onChangeFragmentGuard = this.onChangeFragmentGuard.bind(this); + this.onChangeFragmentOperator = this.onChangeFragmentOperator.bind(this); + + this.onMouseOut = this.onMouseOut.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show for message. + * @param id message ID. + * @param position xy coordinates. + */ + show(id, position) { + const message = this.props.model.getMessageById(id); + + let occurrencesToggle = false; + let fragmentToggle = false; + if (message) { + + message.occurrences = message.occurrences || { start: [], stop: [] }; + message.occurrences.start = message.occurrences.start || []; + message.occurrences.stop = message.occurrences.stop || []; + message.fragment = message.fragment || {}; + message.fragment.start = message.fragment.start || false; + message.fragment.stop = message.fragment.stop || false; + message.fragment.guard = message.fragment.guard || ''; + message.fragment.operator = message.fragment.operator || ''; + + const mo = message.occurrences; + occurrencesToggle = (mo.start.length > 0 || mo.stop.length > 0); + + const mf = message.fragment; + fragmentToggle = (mf.start || mf.stop); + } + + this.setState({ + id, + message, + occurrencesToggle, + fragmentToggle, + visible: true, + x: position.x, + y: position.y, + }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Toggle occurrence state. + */ + onClickOccurrenceToggle() { + const message = this.state.message; + if (message) { + const oFromState = Actions.getOccurrenceState(message.occurrences, message.from); + const oToState = Actions.getOccurrenceState(message.occurrences, message.to); + const oExpanded = oFromState > 0 || oToState > 0; + if (oExpanded) { + this.setState({ occurrencesExpanded: true }); + } else { + const occurrencesExpanded = !this.state.occurrencesExpanded; + this.setState({ occurrencesExpanded }); + } + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle menu click. + */ + onClickOccurrenceFrom() { + const message = this.state.message; + if (message) { + Actions._toggleOccurrence(message.occurrences, message.from); + } + this.setState({ message }); + this.props.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle menu click. + */ + onClickOccurrenceTo() { + const message = this.state.message; + if (message) { + Actions._toggleOccurrence(message.occurrences, message.to); + } + this.setState({ message }); + this.props.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Toggle fragment. + */ + onClickFragmentToggle() { + const message = this.state.message; + if (message) { + Actions._toggleFragment(message.fragment); + } + this.setState({ message }); + this.props.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle menu click. + * @param event update event. + */ + onChangeFragmentGuard(event) { + const message = this.state.message; + if (message) { + const options = this.props.application.getOptions(); + message.fragment.guard = Common.sanitizeText(event.target.value, options, 'guard'); + } + this.setState({ message }); + this.props.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle menu click. + * @param value updated value. + */ + onChangeFragmentOperator(value) { + const message = this.state.message; + if (message) { + message.fragment.operator = value.value; + } + this.setState({ message }); + this.props.application.renderDiagram(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouse movement. + */ + onMouseOut() { + this.setState({ id: -1, visible: false, x: 0, y: 0 }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render view. + * @returns {XML} + */ + render() { + + const actionsStyles = { }; + const message = this.state.message; + if (!message || !this.state.visible) { + + // Invisible. + + return (<div className="asdcs-actions" ></div>); + } + + // Position and display. + + actionsStyles.display = 'block'; + actionsStyles.left = this.state.x - 10; + actionsStyles.top = this.state.y - 10; + + const oFromState = Actions.getOccurrenceState(message.occurrences, message.from); + const oToState = Actions.getOccurrenceState(message.occurrences, message.to); + const fState = Actions.getFragmentState(message.fragment); + + const oExpanded = this.state.occurrencesExpanded || (oFromState > 0) || (oToState > 0); + const oAuxClassName = oExpanded ? '' : 'asdcs-hidden'; + + const fExpanded = fState !== 0; + const fAuxClassName = fExpanded ? '' : 'asdcs-hidden'; + + const fragmentOperatorOptions = [{ + value: 'alt', + label: 'Alternate', + }, { + value: 'opt', + label: 'Optional', + }, { + value: 'loop', + label: 'Loop', + }]; + + const operator = message.fragment.operator || 'alt'; + + return ( + <div + className="asdcs-actions" + style={actionsStyles} + onMouseLeave={this.onMouseOut} + > + <div className="asdcs-actions-header"> + <div className="asdcs-actions-icon"> + <Icon glyph={iconSettings} /> + </div> + </div> + + <div className="asdcs-actions-options"> + + <div className="asdcs-actions-optiongroup asdcs-actions-optiongroup-occurrence"> + <div + className="asdcs-actions-option asdcs-actions-option-occurrence-toggle" + onClick={this.onClickOccurrenceToggle} + > + <span className="asdcs-label">Occurrence</span> + <div className="asdcs-actions-state"> + <Icon glyph={iconCollapsed} className={oExpanded ? 'asdcs-hidden' : ''} /> + <Icon glyph={iconExpanded} className={oExpanded ? '' : 'asdcs-hidden'} /> + </div> + </div> + </div> + + <div + className={`asdcs-actions-option asdcs-actions-option-occurrence-from ${oAuxClassName}`} + onClick={this.onClickOccurrenceFrom} + > + <span className="asdcs-label">From</span> + <div className="asdcs-actions-state"> + <span className="asdcs-annotation"></span> + <Icon glyph={iconOccurrenceDefault} className={oFromState === 0 ? '' : 'asdcs-hidden'} /> + <Icon glyph={iconOccurrenceStart} className={oFromState === 1 ? '' : 'asdcs-hidden'} /> + <Icon glyph={iconOccurrenceStop} className={oFromState === 2 ? '' : 'asdcs-hidden'} /> + </div> + </div> + + <div + className={`asdcs-actions-option asdcs-actions-option-occurrence-to ${oAuxClassName}`} + onClick={this.onClickOccurrenceTo} + > + <span className="asdcs-label">To</span> + <div className="asdcs-actions-state"> + <span className="asdcs-annotation"></span> + <Icon glyph={iconOccurrenceDefault} className={oToState === 0 ? '' : 'asdcs-hidden'} /> + <Icon glyph={iconOccurrenceStart} className={oToState === 1 ? '' : 'asdcs-hidden'} /> + <Icon glyph={iconOccurrenceStop} className={oToState === 2 ? '' : 'asdcs-hidden'} /> + </div> + </div> + + <div className="asdcs-actions-optiongroup asdcs-actions-optiongroup-fragment"> + <div + className="asdcs-actions-option asdcs-actions-fragment-toggle" + onClick={this.onClickFragmentToggle} + > + <span className="asdcs-label">Fragment</span> + <div className="asdcs-actions-state"> + <span className="asdcs-annotation"></span> + <Icon glyph={iconFragmentDefault} className={fState === 0 ? '' : 'asdcs-hidden'} /> + <Icon glyph={iconFragmentStart} className={fState === 1 ? '' : 'asdcs-hidden'} /> + <Icon glyph={iconFragmentStop} className={fState === 2 ? '' : 'asdcs-hidden'} /> + </div> + </div> + </div> + + <div className={`asdcs-actions-option asdcs-actions-fragment-operator ${fAuxClassName}`}> + <div className="asdcs-label">Operator</div> + <div className="asdcs-value"> + <Select + className="asdcs-editable-select" + openOnFocus + clearable={false} + searchable={false} + value={operator} + onChange={this.onChangeFragmentOperator} + options={fragmentOperatorOptions} + /> + </div> + </div> + + <div className={`asdcs-actions-option asdcs-actions-fragment-guard ${fAuxClassName}`}> + <div className="asdcs-label">Guard</div> + <div className="asdcs-value"> + <input + className="asdcs-editable" + type="text" + size="20" + maxLength="80" + value={message.fragment.guard} + placeholder="Condition" + onChange={this.onChangeFragmentGuard} + /> + </div> + </div> + + </div> + + <div className="asdcs-actions-footer"></div> + + </div> + + ); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Toggle through three occurrence states on click. + * @param occurrence occurrences state, updated as side-effect. + * @param lifelineId message end that's being toggled. + * @private + */ + static _toggleOccurrence(occurrence, lifelineId) { + const o = occurrence; + + const rm = function rm(array, value) { + const index = array.indexOf(value); + if (index !== -1) { + array.splice(index, 1); + } + }; + + const add = function add(array, value) { + if (array.indexOf(value) === -1) { + array.push(value); + } + }; + + if (o.start && o.start.indexOf(lifelineId) !== -1) { + // Start -> stop. + rm(o.start, lifelineId); + add(o.stop, lifelineId); + } else if (o.stop && o.stop.indexOf(lifelineId) !== -1) { + // Stop -> default. + rm(o.start, lifelineId); + rm(o.stop, lifelineId); + } else { + // Default -> start. + add(o.start, lifelineId); + rm(o.stop, lifelineId); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Toggle fragment setting on click. + * @param fragment + * @private + **/ + static _toggleFragment(fragment) { + const f = fragment; + if (f.start === true) { + f.start = false; + f.stop = true; + } else if (f.stop === true) { + f.stop = false; + f.start = false; + } else { + f.start = true; + f.stop = false; + } + f.guard = ''; + f.operator = ''; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get ternary occurrences state. + * @param o occurrences. + * @param lifelineId from/to lifeline ID. + * @returns {number} + * @private + */ + static getOccurrenceState(o, lifelineId) { + if (o.start.indexOf(lifelineId) !== -1) { + return 1; + } + if (o.stop.indexOf(lifelineId) !== -1) { + return 2; + } + return 0; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get ternary fragment state. + * @param f fragment. + * @returns {number} + * @private + */ + static getFragmentState(f) { + if (f.start) { + return 1; + } + if (f.stop) { + return 2; + } + return 0; + } +} + +/** Element properties. */ +Actions.propTypes = { + application: React.PropTypes.object.isRequired, + model: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx new file mode 100644 index 0000000000..e8d8cffb7a --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx @@ -0,0 +1,264 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 { DragSource, DropTarget } from 'react-dnd'; + +import Common from '../../../../../../common/Common'; + +import Icon from '../../../../../icons/Icon'; +import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg'; +import iconDelete from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/delete.svg'; + +/** + * LHS lifeline row view. + */ +class Lifeline extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct editor view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + + this.state = { + active: false, + name: props.lifeline.name, + }; + + const metamodel = Common.assertNotNull(this.props.metamodel).unwrap(); + this.canReorder = metamodel.diagram.lifelines.constraints.reorder; + this.canDelete = metamodel.diagram.lifelines.constraints.delete; + + // Bindings. + + this.onChangeName = this.onChangeName.bind(this); + this.onBlurName = this.onBlurName.bind(this); + this.onClickDelete = this.onClickDelete.bind(this); + this.onMouseEnter = this.onMouseEnter.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle name change. + * @param event change event. + */ + onChangeName(event) { + this.setState({ name: event.target.value }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle name change. + * @param event change event. + */ + onBlurName(event) { + const options = this.props.application.getOptions(); + const sanitized = Common.sanitizeText(event.target.value, options, 'lifeline'); + const props = { + id: this.props.lifeline.id, + name: sanitized, + }; + this.props.designer.updateLifeline(props); + this.setState({ name: sanitized }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle lifeline delete. + */ + onClickDelete() { + this.props.designer.deleteLifeline(this.props.lifeline.id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouseover event. + */ + onMouseEnter() { + this.setState({ active: true }); + this.props.designer.onMouseEnterLifeline(this.props.lifeline.id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouseleave event. + */ + onMouseLeave() { + this.setState({ active: false }); + this.props.designer.onMouseLeaveLifeline(this.props.lifeline.id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get whether metadata permits reorder. + * @returns true if reorderable. + */ + isCanReorder() { + return this.canReorder; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get whether metadata permits delete. + * @returns true if lifeline can be deleted. + */ + isCanDelete() { + return this.canDelete; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * React render. + * @returns {*} + */ + render() { + + const id = this.props.lifeline.id; + const activeClass = (this.props.active === true) ? 'asdcs-active' : ''; + const { connectDragSource, connectDropTarget } = this.props; + return connectDragSource(connectDropTarget( + + <div + className={`asdcs-designer-lifeline ${activeClass}`} + data-id={id} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > + <table className="asdcs-designer-layout asdcs-designer-lifeline-row1"> + <tbody> + <tr> + <td> + <div className="asdcs-designer-sort asdcs-designer-icon"> + <Icon glyph={iconHandle} /> + </div> + </td> + <td> + <div className="asdcs-designer-lifeline-index">{this.props.lifeline.index}.</div> + </td> + <td> + <div className="asdcs-designer-lifeline-name"> + <input + type="text" + className="asdcs-editable" + placeholder="Unnamed" + value={this.state.name} + onChange={this.onChangeName} + onBlur={this.onBlurName} + /> + </div> + </td> + <td> + <div className="asdcs-designer-delete asdcs-designer-icon" onClick={this.onClickDelete}> + <Icon glyph={iconDelete} /> + </div> + </td> + </tr> + </tbody> + </table> + </div> + )); + } +} + +/** + * Declare properties. + */ +Lifeline.propTypes = { + application: React.PropTypes.object.isRequired, + designer: React.PropTypes.object.isRequired, + lifeline: React.PropTypes.object.isRequired, + active: React.PropTypes.bool.isRequired, + metamodel: React.PropTypes.object.isRequired, + id: React.PropTypes.any.isRequired, + index: React.PropTypes.number.isRequired, + lifelines: React.PropTypes.object.isRequired, + isDragging: React.PropTypes.bool.isRequired, + connectDragSource: React.PropTypes.func.isRequired, + connectDropTarget: React.PropTypes.func.isRequired, +}; + +/** DND. */ +const source = { + beginDrag(props) { + return { + id: props.id, + index: props.index, + }; + }, +}; + +/** DND. */ +const sourceCollect = function collection(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + }; +}; + +/** DND. */ +const target = { + drop(props, monitor, component) { + Common.assertNotNull(props); + Common.assertNotNull(monitor); + const decorated = component.getDecoratedComponentInstance(); + if (decorated) { + const lifelines = decorated.props.lifelines; + if (lifelines) { + const dragIndex = monitor.getItem().index; + const hoverIndex = lifelines.getHoverIndex(); + lifelines.onDrop(dragIndex, hoverIndex); + } + } + }, + hover(props, monitor, component) { + Common.assertNotNull(props); + Common.assertNotNull(monitor); + if (component) { + const decorated = component.getDecoratedComponentInstance(); + if (decorated) { + const lifelines = decorated.props.lifelines; + if (lifelines) { + lifelines.setHoverIndex(decorated.props.index); + } + } + } + }, +}; + +/** DND. */ +function targetCollect(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + }; +} + +const wrapper1 = DragSource('lifeline', source, sourceCollect)(Lifeline); +export default DropTarget(['lifeline', 'lifeline-new'], target, targetCollect)(wrapper1); diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx new file mode 100644 index 0000000000..a6e9a70703 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx @@ -0,0 +1,112 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 { DragSource } from 'react-dnd'; + +import Icon from '../../../../../icons/Icon'; +import iconPlus from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/plus.svg'; +import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg'; + +/** + * LHS lifeline row view. + */ +class LifelineNew extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + + // Bindings. + + this.onClickAdd = this.onClickAdd.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle click event. + */ + onClickAdd() { + this.props.designer.addLifeline(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render view. + * @returns {*} + */ + render() { + const { connectDragSource } = this.props; + return connectDragSource( + <div className="asdcs-designer-lifeline asdcs-designer-lifeline-new"> + <table className="asdcs-designer-layout asdcs-designer-lifeline-new"> + <tbody> + <tr> + <td> + <div className="asdcs-designer-sort asdcs-designer-icon"> + <Icon glyph={iconHandle} /> + </div> + </td> + <td> + <div className="asdcs-designer-label" onClick={this.onClickAdd}> + Add Lifeline + </div> + </td> + <td> + <div className="asdcs-designer-icon" onClick={this.onClickAdd}> + <Icon glyph={iconPlus} /> + </div> + </td> + <td> </td> + </tr> + </tbody> + </table> + </div> + ); + } +} + +/** Element properties. */ +LifelineNew.propTypes = { + designer: React.PropTypes.object.isRequired, + lifelines: React.PropTypes.object.isRequired, + connectDragSource: React.PropTypes.func.isRequired, +}; + +/** DND. */ +const source = { + beginDrag(props) { + return { id: props.id }; + }, +}; + +/** DND. */ +const collect = function collection(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + }; +}; + +export default DragSource('lifeline-new', source, collect)(LifelineNew); diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx new file mode 100644 index 0000000000..2e2f2ee7fd --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx @@ -0,0 +1,136 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 Common from '../../../../../../common/Common'; + +import Lifeline from './Lifeline'; +import LifelineNew from './LifelineNew'; + +/** + * Lifeline container, facilitating DND. + * @param props lifeline element properties. + * @returns {*} + * @constructor + */ +export default class Lifelines extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + this.setHoverIndex = this.setHoverIndex.bind(this); + this.getHoverIndex = this.getHoverIndex.bind(this); + this.onDrop = this.onDrop.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Record last hover index as non-state. + * @param index index. + */ + setHoverIndex(index) { + this.hoverIndex = index; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get last recorded hover index. + * @returns {*} + */ + getHoverIndex() { + return this.hoverIndex; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle drop. + * @param dragIndex dragged item index; undefined if new. + * @param hoverIndex drop index. + */ + onDrop(dragIndex, hoverIndex) { + if (hoverIndex >= 0) { + const application = this.props.application; + const model = application.getModel(); + if (Common.isNumber(dragIndex)) { + if (dragIndex !== hoverIndex) { + model.reorderLifelines(dragIndex, hoverIndex); + } + } else { + model.addLifeline(hoverIndex); + } + this.forceUpdate(); + application.renderDiagram(); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render view. + * @returns {XML} + */ + render() { + const model = this.props.application.getModel(); + const metamodel = model.getMetamodel(); + const diagram = model.unwrap().diagram; + + const lifelines = []; + for (const lifeline of diagram.lifelines) { + lifelines.push(<Lifeline + key={`l${lifeline.id}`} + application={this.props.application} + designer={this.props.designer} + lifeline={lifeline} + active={this.props.activeLifelineId === lifeline.id} + id={lifeline.id} + metamodel={metamodel} + lifelines={this} + index={lifelines.length} + />); + } + + lifelines.push(<LifelineNew + key="_l" + designer={this.props.designer} + lifelines={this} + />); + + return ( + <div className="asdcs-designer-lifelines"> + {lifelines} + </div> + ); + } +} + +/** + * Declare properties. + */ +Lifelines.propTypes = { + application: React.PropTypes.object.isRequired, + designer: React.PropTypes.object.isRequired, + activeLifelineId: React.PropTypes.string, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx new file mode 100644 index 0000000000..95bff702da --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx @@ -0,0 +1,587 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 Select from 'react-select'; +import { DragSource, DropTarget } from 'react-dnd'; + +import Common from '../../../../../../common/Common'; + +import Icon from '../../../../../icons/Icon'; +import iconDelete from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/delete.svg'; +import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg'; +import iconNotes from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/notes.svg'; +import iconSettings from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/settings.svg'; +import iconRequestSync from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/request-sync.svg'; +import iconRequestAsync from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/request-async.svg'; +import iconResponse from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/response.svg'; + +/** + * LHS message row view. + */ +class Message extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + + this.state = { + active: false, + name: props.message.name || '', + }; + + this.combinedOptions = [{ + value: 'REQUEST_SYNC', + }, { + value: 'REQUEST_ASYNC', + }, { + value: 'RESPONSE', + }]; + + // Bindings. + + this.onChangeName = this.onChangeName.bind(this); + this.onBlurName = this.onBlurName.bind(this); + this.onChangeType = this.onChangeType.bind(this); + this.onChangeFrom = this.onChangeFrom.bind(this); + this.onChangeTo = this.onChangeTo.bind(this); + this.onClickDelete = this.onClickDelete.bind(this); + this.onClickActions = this.onClickActions.bind(this); + this.onClickNotes = this.onClickNotes.bind(this); + this.onMouseEnter = this.onMouseEnter.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle name change. + * @param event change event. + */ + onChangeName(event) { + this.setState({ name: event.target.value }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle name change. + * @param event change event. + */ + onBlurName(event) { + const options = this.props.application.getOptions(); + const sanitized = Common.sanitizeText(event.target.value, options, 'message'); + const props = { + id: this.props.message.id, + name: sanitized, + }; + this.props.designer.updateMessage(props); + this.setState({ name: sanitized }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle delete. + */ + onClickDelete() { + this.props.designer.deleteMessage(this.props.message.id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle menu click. + */ + onClickActions(event) { + this.props.designer.showActions(this.props.message.id, { x: event.pageX, y: event.pageY }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle menu click. + */ + onClickNotes() { + this.props.designer.showNotes(this.props.message.id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle selection. + * @param value selection. + */ + onChangeFrom(value) { + if (value.target) { + this.updateMessage({ from: value.target.value }); + } else { + this.updateMessage({ from: value.value }); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle selection. + * @param value selection. + */ + onChangeTo(value) { + if (value.target) { + this.updateMessage({ to: value.target.value }); + } else { + this.updateMessage({ to: value.value }); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle selection. + * @param selected selection. + */ + onChangeType(selected) { + + const value = selected.target ? selected.target.value : selected.value; + const props = {}; + if (value.indexOf('RESPONSE') !== -1) { + props.type = 'response'; + props.asynchronous = false; + } else { + props.type = 'request'; + props.asynchronous = (value.indexOf('ASYNC') !== -1); + } + + this.updateMessage(props); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouse event. + */ + onMouseEnter() { + this.setState({ active: true }); + this.props.designer.onMouseEnterMessage(this.props.message.id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle mouse event. + */ + onMouseLeave() { + this.setState({ active: false }); + this.props.designer.onMouseLeaveMessage(this.props.message.id); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Update message properties. + * @param props properties updates. + */ + updateMessage(props) { + const update = { + id: this.props.message.id, + }; + for (const k of Object.keys(props)) { + update[k] = props[k]; + } + this.props.designer.updateMessage(update); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render icon. + * @param option selection. + * @returns {XML} + */ + renderOption(option) { + if (option.value === 'RESPONSE') { + return <Icon glyph={iconResponse} />; + } + if (option.value === 'REQUEST_ASYNC') { + return <Icon glyph={iconRequestAsync} />; + } + return <Icon glyph={iconRequestSync} />; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get request/response and asynchronous combined constant. + * @param message message whose properties define spec. + * @returns {*} + */ + getMessageSpec(message) { + if (message.type === 'response') { + return 'RESPONSE'; + } + if (message.asynchronous) { + return 'REQUEST_ASYNC'; + } + return 'REQUEST_SYNC'; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @returns {*} + * @private + */ + renderHTMLSelect() { + + const message = this.props.message; + const from = this.props.from; + const to = Common.assertNotNull(this.props.to); + const messageNotesActiveClass = message.notes && message.notes.length > 0 ? 'asdcs-active' : ''; + const combinedValue = this.getMessageSpec(message); + + const lifelineOptions = []; + for (const lifeline of this.props.model.unwrap().diagram.lifelines) { + lifelineOptions.push(<option + key={lifeline.id} + value={lifeline.id} + > + {lifeline.name} + </option>); + } + + const activeClass = (this.state.active || this.props.active) ? 'asdcs-active' : ''; + const { connectDragSource, connectDropTarget } = this.props; + return connectDragSource(connectDropTarget( + <div + className={`asdcs-designer-message ${activeClass}`} + data-id={message.id} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > + + <table className="asdcs-designer-layout asdcs-designer-message-row1"> + <tbody> + <tr> + <td> + <div className="asdcs-designer-sort asdcs-designer-icon"> + <Icon glyph={iconHandle} /> + </div> + </td> + <td> + <div className="asdcs-designer-message-index">{message.index}.</div> + </td> + <td> + <div className="asdcs-designer-message-name"> + <input + type="text" + className="asdcs-editable" + value={this.state.name} + placeholder="Unnamed" + onBlur={this.onBlurName} + onChange={this.onChangeName} + /> + </div> + </td> + <td> + <div className="asdcs-designer-actions"> + <div + className="asdcs-designer-settings asdcs-designer-icon" + onClick={this.onClickActions} + > + <Icon glyph={iconSettings} /> + </div> + <div + className={`asdcs-designer-notes asdcs-designer-icon ${messageNotesActiveClass}`} + onClick={this.onClickNotes} + > + <Icon glyph={iconNotes} /> + </div> + <div + className="asdcs-designer-delete asdcs-designer-icon" + onClick={this.onClickDelete} + > + <Icon glyph={iconDelete} /> + </div> + </div> + </td> + </tr> + </tbody> + </table> + + <table className="asdcs-designer-layout asdcs-designer-message-row2"> + <tbody> + <tr> + <td> + <select + onChange={this.onChangeFrom} + className="asdcs-designer-select-message-from" + value={from.id} + onChange={this.onChangeFrom} + > + options={lifelineOptions} + </select> + </td> + <td> + <select + onChange={this.onChangeFrom} + className="asdcs-designer-select-message-type" + value={combinedValue} + onChange={this.onChangeType} + > + <option value="REQUEST_SYNC">⇾</option> + <option value="REQUEST_ASYNC">→</option> + <option value="RESPONSE">⇠</option> + </select> + </td> + <td> + <select + onChange={this.onChangeFrom} + className="asdcs-designer-select-message-to" + value={to.id} + onChange={this.onChangeTo} + > + options={lifelineOptions} + </select> + </td> + </tr> + </tbody> + </table> + + </div> + )); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render view. + * @returns {*} + * @private + */ + renderReactSelect() { + + const message = this.props.message; + const from = this.props.from; + const to = Common.assertNotNull(this.props.to); + const messageNotesActiveClass = message.notes && message.notes.length > 0 ? 'asdcs-active' : ''; + const combinedValue = this.getMessageSpec(message); + + const lifelineOptions = []; + for (const lifeline of this.props.model.unwrap().diagram.lifelines) { + lifelineOptions.push({ + value: lifeline.id, + label: lifeline.name, + }); + } + + const activeClass = (this.state.active || this.props.active) ? 'asdcs-active' : ''; + const { connectDragSource, connectDropTarget } = this.props; + return connectDragSource(connectDropTarget( + + <div + className={`asdcs-designer-message ${activeClass}`} + data-id={message.id} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > + + <table className="asdcs-designer-layout asdcs-designer-message-row1"> + <tbody> + <tr> + <td> + <div className="asdcs-designer-sort asdcs-designer-icon"> + <Icon glyph={iconHandle} /> + </div> + </td> + <td> + <div className="asdcs-designer-message-index">{message.index}.</div> + </td> + <td> + <div className="asdcs-designer-message-name"> + <input + type="text" + className="asdcs-editable" + value={this.state.name} + placeholder="Unnamed" + onBlur={this.onBlurName} + onChange={this.onChangeName} + /> + </div> + </td> + <td> + <div className="asdcs-designer-actions"> + <div + className="asdcs-designer-settings asdcs-designer-icon" + onClick={this.onClickActions} + > + <Icon glyph={iconSettings} /> + </div> + <div + className={`asdcs-designer-notes asdcs-designer-icon ${messageNotesActiveClass}`} + onClick={this.onClickNotes} + > + <Icon glyph={iconNotes} /> + </div> + <div + className="asdcs-designer-delete asdcs-designer-icon" + onClick={this.onClickDelete} + > + <Icon glyph={iconDelete} /> + </div> + </div> + </td> + </tr> + </tbody> + </table> + + <table className="asdcs-designer-layout asdcs-designer-message-row2"> + <tbody> + <tr> + <td> + <Select + className="asdcs-editable-select asdcs-designer-editable-message-from" + openOnFocus + clearable={false} + searchable={false} + value={from.id} + onChange={this.onChangeFrom} + options={lifelineOptions} + /> + </td> + <td> + <Select + className="asdcs-editable-select asdcs-designer-editable-message-type" + openOnFocus + clearable={false} + searchable={false} + value={combinedValue} + onChange={this.onChangeType} + options={this.combinedOptions} + optionRenderer={this.renderOption} + valueRenderer={this.renderOption} + /> + </td> + <td> + <Select + className="asdcs-editable-select asdcs-designer-editable-message-to" + openOnFocus + clearable={false} + searchable={false} + value={to.id} + onChange={this.onChangeTo} + options={lifelineOptions} + /> + </td> + + </tr> + </tbody> + </table> + + </div> + )); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + render() { + const options = this.props.application.getOptions(); + if (options.useHtmlSelect) { + return this.renderHTMLSelect(); + } + return this.renderReactSelect(); + } +} + +/** + * Declare properties. + * @type {{designer: *, message: *, from: *, to: *, model: *, connectDragSource: *}} + */ +Message.propTypes = { + application: React.PropTypes.object.isRequired, + designer: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, + active: React.PropTypes.bool.isRequired, + from: React.PropTypes.object.isRequired, + to: React.PropTypes.object.isRequired, + model: React.PropTypes.object.isRequired, + index: React.PropTypes.number.isRequired, + messages: React.PropTypes.object.isRequired, + connectDragSource: React.PropTypes.func.isRequired, + connectDropTarget: React.PropTypes.func.isRequired, +}; + +/** DND. */ +const source = { + beginDrag(props) { + return { + id: props.id, + index: props.index, + }; + }, +}; + +/** DND. */ +const sourceCollect = function collection(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + }; +}; + + +/** DND. */ +const target = { + drop(props, monitor, component) { + Common.assertNotNull(props); + Common.assertNotNull(monitor); + const decorated = component.getDecoratedComponentInstance(); + if (decorated) { + const messages = decorated.props.messages; + if (messages) { + const dragIndex = monitor.getItem().index; + const hoverIndex = messages.getHoverIndex(); + messages.onDrop(dragIndex, hoverIndex); + } + } + }, + hover(props, monitor, component) { + Common.assertNotNull(props); + Common.assertNotNull(monitor); + if (component) { + const decorated = component.getDecoratedComponentInstance(); + if (decorated) { + decorated.props.messages.setHoverIndex(decorated.props.index); + } + } + }, +}; + +/** DND. */ +function targetCollect(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + }; +} + +const wrapper = DragSource('message', source, sourceCollect)(Message); +export default DropTarget(['message', 'message-new'], target, targetCollect)(wrapper); diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx new file mode 100644 index 0000000000..230cb9fa60 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx @@ -0,0 +1,106 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 { DragSource } from 'react-dnd'; + +import Icon from '../../../../../icons/Icon'; +import iconPlus from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/plus.svg'; +import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg'; + +/** + * LHS lifeline row view. + */ +class MessageNew extends React.Component { + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + this.onClickAdd = this.onClickAdd.bind(this); + } + + /** + * Handle add. + */ + onClickAdd() { + this.props.designer.addMessage(); + } + + /** + * Render view. + * @returns {*} + */ + render() { + const { connectDragSource } = this.props; + return connectDragSource( + <div className="asdcs-designer-message asdcs-designer-message-new"> + <table className="asdcs-designer-layout asdcs-designer-message-new"> + <tbody> + <tr> + <td> + <div className="asdcs-designer-sort asdcs-designer-icon"> + <Icon glyph={iconHandle} /> + </div> + </td> + <td> + <div className="asdcs-designer-label" onClick={this.onClickAdd}> + Add Message + </div> + </td> + <td> + <div className="asdcs-designer-icon" onClick={this.onClickAdd}> + <Icon glyph={iconPlus} /> + </div> + </td> + <td> + + </td> + </tr> + </tbody> + </table> + </div> + ); + } +} + +/** Element properties. */ +MessageNew.propTypes = { + designer: React.PropTypes.object.isRequired, + messages: React.PropTypes.object.isRequired, + connectDragSource: React.PropTypes.func.isRequired, +}; + +/** DND. */ +const source = { + beginDrag(props) { + return { id: props.id }; + }, +}; + +/** DND. */ +const collect = function collection(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + }; +}; + +export default DragSource('message-new', source, collect)(MessageNew); + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx new file mode 100644 index 0000000000..a305a6bd86 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx @@ -0,0 +1,143 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 Common from '../../../../../../common/Common'; + +import Message from './Message'; +import MessageNew from './MessageNew'; + +/** + * Messages container, facilitating DND. + * @param props lifeline element properties. + * @returns {*} + * @constructor + */ +export default class Messages extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + this.state = { + }; + this.setHoverIndex = this.setHoverIndex.bind(this); + this.getHoverIndex = this.getHoverIndex.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Record last hover index as non-state. + * @param index index. + */ + setHoverIndex(index) { + this.hoverIndex = index; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get last recorded hover index. + * @returns {*} + */ + getHoverIndex() { + return this.hoverIndex; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle drop. + * @param dragIndex dragged item index; undefined if new. + * @param hoverIndex drop index. + */ + onDrop(dragIndex, hoverIndex) { + if (hoverIndex >= 0) { + const application = this.props.application; + const model = application.getModel(); + if (Common.isNumber(dragIndex)) { + if (dragIndex !== hoverIndex) { + model.reorderMessages(dragIndex, hoverIndex); + } + } else { + model.addMessage(hoverIndex); + } + this.forceUpdate(); + application.renderDiagram(); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render view. + * @returns {*} + */ + render() { + + const model = this.props.application.getModel(); + const diagram = model.unwrap().diagram; + + // Render existing messages. + + const messages = []; + for (const step of diagram.steps) { + const message = step.message; + const from = model.getLifelineById(message.from); + const to = model.getLifelineById(message.to); + messages.push(<Message + key={`m${message.id}`} + application={this.props.application} + designer={this.props.designer} + message={message} + active={this.props.activeMessageId === message.id} + from={from} + to={to} + model={model} + index={messages.length} + messages={this} + />); + } + + // Render add. + + messages.push(<MessageNew + key="_m" + designer={this.props.designer} + messages={this} + />); + + return ( + <div className="asdcs-designer-steps"> + {messages} + </div> + ); + } +} + +/** Element properties. */ +Messages.propTypes = { + application: React.PropTypes.object.isRequired, + designer: React.PropTypes.object.isRequired, + activeMessageId: React.PropTypes.string, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx new file mode 100644 index 0000000000..a17a197ca0 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx @@ -0,0 +1,34 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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'; + +/** + * Metadata view. + */ +const Metadata = function Metadata(props) { + return ( + <div className="asdcs-designer-metadata"> + {props.metadata.name} + </div> + ); +}; + +Metadata.propTypes = { + metadata: React.PropTypes.object.isRequired, +}; + +export default Metadata; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx new file mode 100644 index 0000000000..3d13d830da --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx @@ -0,0 +1,86 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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'; + +/** + * Editor view, aggregating the designer, the code editor, the toolbar. + */ +export default class Source extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + */ + constructor(props, context) { + super(props, context); + this.demo = this.props.application.getOptions().demo; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set JSON mode. + * @param json JSON (stringified) code. + */ + setJSON(json = '') { + if (this.textarea) { + this.textarea.value = json; + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set YAML mode. + * @param yaml YAML code. + */ + setYAML(yaml = '') { + if (this.textarea) { + this.textarea.value = yaml; + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + componentDidMount() { + /* + this.cm = CodeMirror.fromTextArea(this.textarea, { + lineNumbers: true, + readOnly: true, + }); + */ + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render to DOM. + */ + render() { + return ( + <div className="asdcs-editor-code"> + <textarea ref={(r) => { this.textarea = r; }}></textarea> + </div> + ); + } +} + +Source.propTypes = { + application: React.PropTypes.object.isRequired, +}; + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx new file mode 100644 index 0000000000..dd75180b2a --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx @@ -0,0 +1,275 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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 Common from '../../../../common/Common'; + +import iconPlus from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/plus.svg'; +import iconOpen from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/open.svg'; + +/** + * Toolbar view. Buttons offered in the toolbar depend on the mode. Unless in demo mode, + * all you get are the buttons for toggling between JSON/YAML/Designer. + */ +export default class Toolbar extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + */ + constructor(props, context) { + super(props, context); + this.application = Common.assertType(this.props.application, 'Object'); + this.editor = Common.assertType(this.props.editor, 'Object'); + this.mode = 'design'; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set editor mode, one of {design, json, yaml}. + * @param mode + */ + setMode(mode = 'design') { + this.mode = mode; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render into the DOM. + */ + render() { + + const demo = this.application.getOptions().demo; + const demoCss = demo ? '' : 'asdc-hide'; + + return ( + <div className={`asdcs-editor-toolbar ${demoCss}`}> + <div className="asdcs-editor-toolbar-demo"> + <button className="asdcs-button-new" data-title="New sequence"> + <svg> + <use xlinkHref={iconPlus} className="asdcs-icon" /> + </svg> + </button> + <button className="asdcs-button-open" data-title="Open sequence"> + <svg> + <use xlinkHref={iconOpen} className="asdcs-icon" /> + </svg> + </button> + <button className="asdcs-button-save" data-title="Save checkpoint"> + <svg> + <use xlinkHref="#icon--save" className="asdcs-icon" /> + </svg> + </button> + <button className="asdcs-button-validate" data-title="Validate"> + <svg> + <use xlinkHref="#icon--validate" className="asdcs-icon" /> + </svg> + </button> + <button className="asdcs-button-download" data-title="Download"> + <svg> + <use xlinkHref="#icon--download" className="asdcs-icon" /> + </svg> + </button> + <button className="asdcs-button-upload" data-title="Upload"> + <svg> + <use xlinkHref="#icon--upload" className="asdcs-icon" /> + </svg> + </button> + </div> + <div className="asdcs-editor-toolbar-toggle"> + <button className="asdcs-button-design asdcs-button-mode asdcs-button-toggle-left"> + Design + </button> + <button className="asdcs-button-json asdcs-button-mode asdcs-button-toggle-center"> + JSON + </button> + <button className="asdcs-button-yaml asdcs-button-mode asdcs-button-toggle-right"> + YAML + </button> + </div> + </div> + ); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initialize eventhandlers. + * @private + * + _initEvents() { + + $('button.asdcs-button-open', this.$el).click(() => { + this._doDemoOpen(); + }); + + $('button.asdcs-button-new', this.$el).click(() => { + this._doDemoNew(); + }); + + $('button.asdcs-button-save', this.$el).click(() => { + this._doDemoSave(); + }); + + $('button.asdcs-button-upload', this.$el).click(() => { + this._doDemoUpload(); + }); + + $('button.asdcs-button-download', this.$el).click(() => { + this._doDemoDownload(); + }); + + $('button.asdcs-button-validate', this.$el).click(() => { + this._doDemoValidate(); + }); + + $('button.asdcs-button-json', this.$el).click((e) => { + if ($(e.target).hasClass('asdcs-active')) { + return; + } + this.editor.toggleToJSON(); + }); + + $('button.asdcs-button-yaml', this.$el).click((e) => { + if ($(e.target).hasClass('asdcs-active')) { + return; + } + this.editor.toggleToYAML(); + }); + + $('button.asdcs-button-design', this.$el).click((e) => { + if ($(e.target).hasClass('asdcs-active')) { + return; + } + this.editor.toggleToDesign(); + }); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoOpen() { + const complete = function complete() { + const sequencer = this.application.getSequencer(); + const scenarios = sequencer.getDemoScenarios(); + sequencer.setModel(scenarios.getECOMP()); + }; + this.application.showConfirmDialog('[DEMO MODE] Open a canned DEMO sequence ' + + 'via the public #setModel() API?', complete); + + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoNew() { + const complete = function complete() { + const sequencer = this.application.getSequencer(); + sequencer.newModel(); + }; + this.application.showConfirmDialog('[DEMO MODE] Create an empty sequence via the ' + + 'public #newModel() API?', complete); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoSave() { + const sequencer = this.application.getSequencer(); + Logger.info(`[DEMO MODE] model:\n${JSON.stringify(sequencer.getModel(), null, 4)}`); + this.application.showInfoDialog('[DEMO MODE] Retrieved model via the public #getModel ' + + 'API and logged its JSON to the console.'); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoUpload() { + const sequencer = this.application.getSequencer(); + const svg = sequencer.getSVG(); + // console.log(`[DEMO MODE] SVG:\n${svg}`); + const $control = this.$el.closest('.asdcs-control'); + Logger.info(`parent: ${$control.length}`); + const $form = $('form.asdcs-export', $control); + Logger.info(`form: ${$form.length}`); + $('input[name=svg]', $form).val(svg); + try { + $form.submit(); + } catch (e) { + Logger.error(e); + this.application.showErrorDialog('[DEMO MODE] Export service not available. Retrieved ' + + 'SVG via the public #getSVG API and dumped it to the console.'); + } + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoDownload() { + const json = JSON.stringify(this.application.getSequencer().getModel()); + const $control = this.$el.closest('.asdcs-control'); + const $a = $('<a download="model.json" style="display:none">').appendTo($control); + $a.attr('href', `data:application/json;charset=utf-8,${encodeURIComponent(json)}`); + $a[0].click(); + $a.remove(); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoValidate() { + this.application.showInfoDialog('[DEMO MODE] Dumping validation result to the console.'); + const errors = this.application.getModel().validate(); + Logger.info(`[DEMO MODE] Validation: ${JSON.stringify(errors, null, 4)}`); + } + */ +} + +Toolbar.propTypes = { + application: React.PropTypes.object.isRequired, + editor: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx new file mode 100644 index 0000000000..529ae92ded --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx @@ -0,0 +1,31 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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'; + +const Export = function Export() { + return ( + <form className="asdcs-export" action="/ossui-svg/services/ossui/svg/export" method="post"> + <input name="svg" type="hidden" value="" /> + <input name="css" type="hidden" value="sdc/sequencer/default" /> + <input name="type" type="hidden" value="PDF" /> + <input name="height" type="hidden" value="1920" /> + <input name="width" type="hidden" value="1080" /> + </form> + ); +}; + +export default Export; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx new file mode 100644 index 0000000000..6bc04f997f --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx @@ -0,0 +1,41 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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'; + +/** + * Simple icon view. + * @param glyph glyph definition, from import. + * @param className optional classname, for svg element. + * @returns {XML} + * @constructor + */ +const Icon = function Icon({ glyph, className }) { + return ( + <svg viewBox="0 0 1000 1000" className={className} > + <use xlinkHref={glyph} className="asdcs-icon" /> + </svg> + ); +}; + +/** Declare properties. */ +Icon.propTypes = { + className: React.PropTypes.string, + glyph: React.PropTypes.string.isRequired, +}; + +export default Icon; + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx new file mode 100644 index 0000000000..817f4f1697 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx @@ -0,0 +1,61 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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'; + +/** + * Overlay view. + */ +export default class Overlay extends React.Component { + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + this.state = { + visible: false, + }; + this.setVisible = this.setVisible.bind(this); + } + + /** + * Set visibility. + * @param visible true if visible. + */ + setVisible(visible) { + this.setState({ + visible, + }); + } + + /** + * Render view. + * @returns {XML} + */ + render() { + const display = this.state.visible ? 'block' : 'none'; + return ( + <div + className="asdcs-overlay" + style={{ display }} + > + </div> + ); + } +} |