diff --git a/sys/workflow/engine.go b/sys/workflow/engine.go index 1abce6e05..d26d29d2f 100644 --- a/sys/workflow/engine.go +++ b/sys/workflow/engine.go @@ -304,8 +304,9 @@ func (me *Engine) setupNodes(ctx *context) error { if len(me.nodeImpls) == 0 { return ErrNodeImplementationNotProvided } + for _, item := range ctx.flow.Nodes { - item.internalNode = item.isCondition() || item.isWorkflow() + item.internalNode = item.isCondition() || item.isWorkflow() || item.isPlaceholder() if !item.internalNode { if s, ok := me.nodeImpls[item.Type]; ok && s.InitImplFunc != nil { item.new = s.InitImplFunc @@ -319,6 +320,62 @@ func (me *Engine) setupNodes(ctx *context) error { return nil } +func (me *Engine) removeUselessNodes(ctx *context) error { + uselessConnections := make(map[string]*Connection, 0) + + for name, item := range ctx.flow.Nodes { + if !item.isPlaceholder() { + continue + } + + for _, conn := range item.Connections { + correctConnection := conn + currentNode := ctx.flow.Nodes[correctConnection.NodeID] + + for currentNode.isPlaceholder() { + if len(currentNode.Connections) > 0 { + correctConnection = currentNode.Connections[0] + currentNode = ctx.flow.Nodes[correctConnection.NodeID] + } else { + break + } + } + + uselessConnections[name] = correctConnection + } + } + + for _, item := range ctx.flow.Nodes { + if item.isPlaceholder() { + continue + } + + for i := 0; i < len(item.Connections); i++ { + conn := item.Connections[i] + + if val, ok := uselessConnections[conn.NodeID]; ok { + item.Connections[i] = val + } + } + } + + if val, ok := uselessConnections[ctx.flow.Start.NodeID]; ok { + ctx.flow.Start.NodeID = val.NodeID + } + + filteredNodes := make(map[string]*Node, 0) + + for name, item := range ctx.flow.Nodes { + if !item.isPlaceholder() { + filteredNodes[name] = item + } + } + + ctx.flow.Nodes = filteredNodes + + return nil +} + func (me *Engine) execute(nn *Node, considerSteps bool) (proceed bool, err error) { nn.focus() if nn.internalNode { diff --git a/sys/workflow/node.go b/sys/workflow/node.go index 8ce64a368..53e77e0e8 100644 --- a/sys/workflow/node.go +++ b/sys/workflow/node.go @@ -71,6 +71,10 @@ func (n *Node) isWorkflow() bool { return n.Type == "workflow" } +func (n *Node) isPlaceholder() bool { + return n.Type == "placeholder" +} + func (me *Node) getImpl() (NodeIF, error) { //instance is still valid for reuse if me.impl != nil { diff --git a/sys/workflow/workflow.go b/sys/workflow/workflow.go index ed0f0e019..c22511c97 100644 --- a/sys/workflow/workflow.go +++ b/sys/workflow/workflow.go @@ -73,9 +73,11 @@ func newWorkflow(wf *Workflow, engine *Engine, parent *context) (*context, error if wf == nil { return nil, ErrWorkflowMissing } + if wf.Flow == nil || wf.Flow.Start == nil || wf.Flow.Start.NodeID == "" { return nil, ErrStartNodeMissing } + me := context{ id: "root", flow: wf.Flow, @@ -84,11 +86,18 @@ func newWorkflow(wf *Workflow, engine *Engine, parent *context) (*context, error engine: engine, } - err := engine.setupNodes(&me) + err := engine.removeUselessNodes(&me) + if err != nil { + return nil, err + } + + err = engine.setupNodes(&me) if err != nil { return nil, err } + me.start() + return &me, nil } @@ -122,7 +131,9 @@ func (me *context) resolve(n *Node) error { return err } return me.resolve(nn) - } else if n.isWorkflow() { + } else if n.isPlaceholder() { + return nil + }else if n.isWorkflow() { return me.stepIntoWorkflow(n) } } else { diff --git a/ui/core/src/assets/styles/vis-styles.scss b/ui/core/src/assets/styles/vis-styles.scss index 3b05f1212..6c75aa171 100644 --- a/ui/core/src/assets/styles/vis-styles.scss +++ b/ui/core/src/assets/styles/vis-styles.scss @@ -23,7 +23,9 @@ z-index: 10001; } -.flow-chart-main nav, aside, footer { +.flow-chart-main nav, +aside, +footer { background: #dddddd; } @@ -54,29 +56,30 @@ margin: 0; } -.flow-chart-main aside .table > tbody > tr > td, -.flow-chart-main aside .table > thead > tr > th { +.flow-chart-main aside .table>tbody>tr>td, +.flow-chart-main aside .table>thead>tr>th { border-color: #cccccc; } -.flow-chart-main aside .table > thead > tr > th { +.flow-chart-main aside .table>thead>tr>th { text-align: right; } -.flow-chart-main aside .table > tbody > tr > td { +.flow-chart-main aside .table>tbody>tr>td { padding: 0; } -.flow-chart-main aside .table > tbody > tr > td.action { +.flow-chart-main aside .table>tbody>tr>td.action { width: 30px; } -.flow-chart-main aside .table > tbody > tr > td input { +.flow-chart-main aside .table>tbody>tr>td input { width: 100%; height: 30px; } -.flow-chart-main aside .table > tbody > tr > td a, aside .table > tbody > tr > td span { +.flow-chart-main aside .table>tbody>tr>td a, +aside .table>tbody>tr>td span { display: block; height: 30px; width: 188px; @@ -87,18 +90,18 @@ padding: 0 5px; } -.flow-chart-main aside .table > tbody > tr > td span { +.flow-chart-main aside .table>tbody>tr>td span { background: #428bca; color: #ffffff; } -.flow-chart-main aside .table > tbody > tr > td.action a { +.flow-chart-main aside .table>tbody>tr>td.action a { text-align: center; padding: 0; width: auto; } -.flow-chart-main aside .table > tbody > tr > td.delete a { +.flow-chart-main aside .table>tbody>tr>td.delete a { width: auto; font-size: 21px; font-weight: bold; @@ -109,7 +112,7 @@ padding: 0; } -.flow-chart-main aside .table > tbody > tr > td.delete a:hover { +.flow-chart-main aside .table>tbody>tr>td.delete a:hover { color: #000000; text-decoration: none; cursor: pointer; @@ -133,7 +136,8 @@ position: absolute; height: 100%; width: 100%; - -webkit-transition: height 2s; /* Safari */ + -webkit-transition: height 2s; + /* Safari */ transition: width 2s; } @@ -233,11 +237,13 @@ width: 25px; } -.flow-chart-main .step-inner:hover .delete, .result-inner:hover .delete { +.flow-chart-main .step-inner:hover .delete, +.result-inner:hover .delete { opacity: 0.85; } -.flow-chart-main .step-inner .delete, .result-inner .delete { +.flow-chart-main .step-inner .delete, +.result-inner .delete { border-radius: 100px; position: absolute; top: -8px; @@ -252,11 +258,15 @@ transition: opacity 200ms ease-in-out; } -.flow-chart-main .step-inner .delete.btn-default, .result-inner .delete.btn-default { +.flow-chart-main .step-inner .delete.btn-default, +.result-inner .delete.btn-default { border-color: #5d85a1; } -.flow-chart-main .step-inner .delete:hover, .step-inner .delete:active, .result-inner .delete:hover, .result-inner .delete:active { +.flow-chart-main .step-inner .delete:hover, +.step-inner .delete:active, +.result-inner .delete:hover, +.result-inner .delete:active { opacity: 1.0; outline: none; } @@ -270,7 +280,11 @@ position: relative; } -.flow-chart-main .editable select, .editable input, .editable textarea, .editable label, .editable .placeholder { +.flow-chart-main .editable select, +.editable input, +.editable textarea, +.editable label, +.editable .placeholder { position: absolute; z-index: 999; } @@ -318,16 +332,21 @@ top: -100px; } -.flow-chart-main .contact > span, .name > span, .desc > span { +.flow-chart-main .contact>span, +.name>span, +.desc>span { display: block; color: #333333; } -.flow-chart-main .contact > span:hover, .name > span:hover, .desc > span:hover { +.flow-chart-main .contact>span:hover, +.name>span:hover, +.desc>span:hover { text-decoration: none; } -.flow-chart-main .contact, .name { +.flow-chart-main .contact, +.name { height: 37px; float: left; } @@ -336,7 +355,7 @@ border-right: solid 1px rgba(0, 0, 0, 0.13); } -.flow-chart-main .contact > span { +.flow-chart-main .contact>span { font-size: 24px; text-align: center; line-height: 42px; @@ -351,7 +370,7 @@ max-width: 70px; } -.flow-chart-main .name > span { +.flow-chart-main .name>span { font-size: 11px; line-height: 37px; text-indent: 5px; @@ -361,7 +380,7 @@ text-overflow: ellipsis; } -.flow-chart-main .name > span:hover { +.flow-chart-main .name>span:hover { cursor: text; } @@ -389,7 +408,7 @@ margin: 4px 0; } -.flow-chart-main .desc > a { +.flow-chart-main .desc>a { position: absolute; top: 0px; left: 0px; @@ -453,7 +472,8 @@ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); } -.flow-chart-main .poppanel .poppanel-title, .poppanel .poppanel-footer { +.flow-chart-main .poppanel .poppanel-title, +.poppanel .poppanel-footer { padding: 8px 14px; margin: 0; font-size: 14px; @@ -512,12 +532,12 @@ overflow: auto; } -.flow-chart-main .poppanel .poppanel-content .table > tbody > tr > td { +.flow-chart-main .poppanel .poppanel-content .table>tbody>tr>td { padding: 0; line-height: normal; } -.flow-chart-main .poppanel .poppanel-content .table > tbody > tr > td label { +.flow-chart-main .poppanel .poppanel-content .table>tbody>tr>td label { width: 150px; margin: 0; position: relative; @@ -529,16 +549,16 @@ text-overflow: ellipsis; } -.flow-chart-main .poppanel .poppanel-content .table > tbody > tr > td label:hover { +.flow-chart-main .poppanel .poppanel-content .table>tbody>tr>td label:hover { cursor: pointer; } -.flow-chart-main .poppanel .poppanel-content .table > tbody > tr > td label input { +.flow-chart-main .poppanel .poppanel-content .table>tbody>tr>td label input { position: absolute; left: -100px; } -.flow-chart-main .poppanel .poppanel-content .table > tbody > tr > td label .glyphicon { +.flow-chart-main .poppanel .poppanel-content .table>tbody>tr>td label .glyphicon { min-width: 16px; } @@ -611,7 +631,8 @@ font-size: 11px; } -.flow-chart-main .key label svg, .key label img { +.flow-chart-main .key label svg, +.key label img { position: absolute; top: 50%; left: 0px; @@ -754,13 +775,13 @@ input.flow-chart-finder-input { min-height: 54px; } -.fci-node > table { +.fci-node>table { width: 100%; max-width: 100%; table-layout: fixed; } -.flow-chart-node-inner > .fci-node { +.flow-chart-node-inner>.fci-node { width: 100%; } @@ -816,7 +837,8 @@ input.flow-chart-finder-input { margin-top: -4px; } -.wfchart-snavi .wfc-snn .flow-chart-node.flow-chart-node-condition { +.wfchart-snavi .wfc-snn .flow-chart-node.flow-chart-node-condition, +.wfchart-snavi .wfc-snn .flow-chart-node.flow-chart-node-placeholder { .flow-chart-node-inner { padding: $spacer * 0.5; } @@ -876,6 +898,10 @@ input.flow-chart-finder-input { color: #f0a30a; } +.fcn-placeholder { + color: #f5e203; +} + .fcn-form { color: #0eaa64; } @@ -946,7 +972,9 @@ input.flow-chart-finder-input { border: 1px solid lightgray; } -.vis-manipulation, .vis-edit-mode, .vis-close { +.vis-manipulation, +.vis-edit-mode, +.vis-close { display: none !important; } @@ -964,7 +992,9 @@ input.flow-chart-finder-input { color: red; } -.wfvis-ui-delete > table, .wfvis-ui-delete > table td, .wfvis-ui-delete > table tr { +.wfvis-ui-delete>table, +.wfvis-ui-delete>table td, +.wfvis-ui-delete>table tr { border: none; } diff --git a/ui/core/src/views/Workflow.vue b/ui/core/src/views/Workflow.vue index 1e81c5c47..8f14783df 100644 --- a/ui/core/src/views/Workflow.vue +++ b/ui/core/src/views/Workflow.vue @@ -115,6 +115,15 @@ +
+
+
+ +
{{$t('placeholder')}}
+
+
+
@@ -193,6 +202,44 @@ + = symbolsPerLine) { + split = true + } else { + split = false + } + + if (split && symbol === ' ') { + formatted = `${formatted}${this.lineSeparator}` + pointer = 0 + } else { + formatted = `${formatted}${symbol}` + } + + pointer++ + } + + this.currentNode.label = formatted + this.currentNode.name = formatted + + if ($.isFunction(this.callback)) { + this.callback(this.currentNode) + } + this.$dialog.modal('hide') + } catch (e) { + console.log(e) + } + } + } + }, readOnly: false, main: function () { this.readOnly = !writeRights @@ -777,6 +888,7 @@ function condition(){ _.attachDragAndDrop($(this)) }) this.initConditionDialogHandler() + this.initPlaceholderDialogHandler() }, searchNow: function (v) { var _ = this @@ -960,6 +1072,12 @@ function condition(){ n.data = n._data _.onNodeInsert(n, { x: e.offsetX, y: e.offsetY }) }, true) + } else if (_.$newTarget.attr('data-id') === 'placeholder') { + _.placeholderDialogHandler.open($.extend({}, _.leftMenuNodes[_.$newTarget.attr('data-id')]), + function (n) { + n.data = n._data + _.onNodeInsert(n, { x: e.offsetX, y: e.offsetY }) + }, true) } else { _.onNodeInsert(_.leftMenuNodes[_.$newTarget.attr('data-id')], { x: e.offsetX, @@ -1170,6 +1288,7 @@ function condition(){ priceretriever: 'fcn-externalnode node-icon mdi mdi-send', externalNode: 'fcn-externalnode node-icon mdi mdi-link-variant', condition: 'fcn-condition node-icon mdi mdi-circle-outline', + placeholder: 'fcn-placeholder node-icon mdi mdi-hexagon', user: 'fcn-usr node-icon mdi mdi-account', form: 'fcn-form node-icon mdi mdi-view-quilt', workflow: 'fcn-wflow node-icon mdi mdi-source-branch', @@ -1227,6 +1346,12 @@ function condition(){ _.updateNode(n) }) }, + placeholderDblClick: function (node) { + var _ = this + this.wfm.placeholderDialogHandler.open(node, function (n) { + _.updateNode(n) + }) + }, destroy: function () { if (this.network) { this.network.destroy() @@ -1592,6 +1717,40 @@ function condition(){ dblclick: _.conditionDblClick } }, + placeholder: { + connections: { + from: [ + { + node: { + color: { + background: '#f5e203', + highlight: { background: '#f5e203' }, + hover: { background: '#f5e203' } + }, + borderWidthSelected: 3 + }, + edge: { + font: { align: 'middle' }, + arrows: 'to', + color: { color: '#f5e203', highlight: '#f5e203', hover: '#f5e203' } + } + }], + to: Infinity, + space: 0.6 + }, + font: { + color: '#343434', + size: 15 + }, + icon: { + face: 'Material Design Icons', + code: '\u2B22', + color: '#f5e203' + }, + events: { + dblclick: _.placeholderDblClick + } + }, workflow: { connections: { from: [