浏览代码

Vuex and new flowbuilder

luis 7 年之前
父节点
当前提交
c65b29d403

+ 2 - 1
TODO.md

@@ -2,8 +2,9 @@
 
 ## Ideas
 
-* [ ] triggers, vertical slots in nodes, handling events giving the possibility
+* [x] triggers, vertical slots in nodes, handling events giving the possibility
       to trigger execution of subsequent nodes based on links
+* [ ] handle Trigger activity to show highlighted trigger
 
 # Separate development
 

+ 1 - 0
browser/vue-flow/package.json

@@ -43,6 +43,7 @@
     "flow-bin": "^0.62.0",
     "html-webpack-plugin": "^2.30.1",
     "node-sass": "^4.7.2",
+    "pug": "^2.0.0-rc.4",
     "reset-css": "^2.2.1",
     "sass-loader": "^6.0.6",
     "vue-loader": "^13.0.5",

+ 42 - 22
browser/vue-flow/src/components/flow/editor.js

@@ -39,8 +39,16 @@ export default {
     }
   },
   computed: {
-    ...mapGetters(['registry', 'activity', 'nodeData', 'nodeMap']),
-
+    ...mapGetters(['registry', 'activity', 'nodeData', 'nodeById']),
+    viewClasses () {
+      return {
+        'flow-linking': this.linking || this.stickySockets,
+        'flow-triggers': this.triggerLinking || this.stickyTriggers,
+        'activity': this.dragging || this.pointerLink.active,
+        'flow-node--activity': this.nodeActivity,
+        'selecting': !!this.selector
+      }
+    },
     nodeProps () {
       return (node) => {
         let highlight = {}
@@ -78,8 +86,8 @@ export default {
       return (link) => {
         if (!this.$refs.nodes) return
         // For size .x .y
-        const nodeFrom = this.nodeMap(link.from)
-        const nodeTo = this.nodeMap(link.to)
+        const nodeFrom = this.nodeById(link.from)
+        const nodeTo = this.nodeById(link.to)
 
         const refFrom = this.$refs.nodes.find(n => n.id === link.from)
         const refTo = this.$refs.nodes.find(n => n.id === link.to)
@@ -103,8 +111,8 @@ export default {
       return (trigger) => {
         if (!this.$refs.nodes) return
         // For size .x .y
-        const nodeFrom = this.nodeMap(trigger.from)
-        const nodeTo = this.nodeMap(trigger.to)
+        const nodeFrom = this.nodeById(trigger.from)
+        const nodeTo = this.nodeById(trigger.to)
 
         const refFrom = this.$refs.nodes.find(n => n.id === trigger.from)
         const refTo = this.$refs.nodes.find(n => n.id === trigger.to)
@@ -202,7 +210,7 @@ export default {
       if (ev.button !== 0) return
 
       const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
-      const node = this.nodeMap(nodeId)
+      const node = this.nodeById(nodeId)
       const isInput = socket.in !== undefined
       const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
 
@@ -271,8 +279,8 @@ export default {
             console.error('LINK: input same direction (in->in/out->out)')
             return
           }
-          const nodeFrom = this.nodeMap(link.from)
-          const nodeTo = this.nodeMap(link.to)
+          const nodeFrom = this.nodeById(link.from)
+          const nodeTo = this.nodeById(link.to)
 
           const output = this.registry[nodeFrom.src].output.type
           const input = this.registry[nodeTo.src].inputs[link.in].type
@@ -283,9 +291,10 @@ export default {
           }
 
           // Input already exists, replace
-          const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
-          if (existingInputI !== -1) {
-            this.nodeData.links.splice(existingInputI, 1)
+          const existingInput = this.nodeData.links.find(l => l.to === link.to && l.in === link.in)
+          if (existingInput) {
+            // REMOVE LINK
+            this.LINK_REMOVE(existingInput)
           }
           this.LINK_ADD(link)
         }})
@@ -294,7 +303,7 @@ export default {
       if (ev.button !== 0) return
 
       const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
-      const node = this.nodeMap(nodeId)
+      const node = this.nodeById(nodeId)
 
       const isInput = (dir === 'in')
       const triggerPos = {
@@ -389,14 +398,7 @@ export default {
       // we can handle with nodeId and a search
 
       this.NODE_RAISE(this.nodeSelection)
-      // Raise node
-      // put the list of nodes in last
-      /* for (let nk in this.nodeSelection) {
-        // WRONG
-        let ni = this.nodeData.nodes.findIndex(n => n.id === this.nodeSelection[nk].id)
-        this.nodeData.nodes.splice(ni, 1)
-        this.nodeData.nodes.push(this.nodeSelection[nk]) // put in last
-      } */
+
       let curP = this.transformedPoint(ev.x, ev.y)
 
       this.dragging = this.nodeSelection
@@ -409,7 +411,7 @@ export default {
           const nodeUpdate = []
           for (let k in this.nodeSelection) {
             // const n = this.nodeData.nodes.find(n => n.id === k)
-            const n = this.nodeMap(k)
+            const n = this.nodeById(k)
             // create new nodes
             nodeUpdate.push({
               ...n,
@@ -422,7 +424,25 @@ export default {
           curP = dragP
         },
         drop: (ev) => {
+          // snap?
           this.dragging = null
+          // Snapping
+          const dragP = this.transformedPoint(ev.x, ev.y)
+          const nodeUpdate = []
+          for (let k in this.nodeSelection) {
+            // const n = this.nodeData.nodes.find(n => n.id === k)
+            const n = this.nodeById(k)
+            // create new nodes
+            nodeUpdate.push({
+              ...n,
+              x: Math.round((n.x + dragP.x - curP.x) / 10) * 10,
+              y: Math.round((n.y + dragP.y - curP.y) / 10) * 10
+            })
+          }
+
+          // Updating nodes
+          this.NODE_UPDATE(nodeUpdate)
+
           this.DOCUMENT_SYNC()
         }
       })

+ 1 - 8
browser/vue-flow/src/components/flow/editor.vue

@@ -5,14 +5,7 @@
       xmlns:xlink="http://www.w3.org/1999/xlink"
       ref="svg"
       class="flow-view view"
-      :class="{
-        'flow-linking':linking || stickySockets,
-        'flow-triggers':triggerLinking || stickyTriggers,
-
-        'activity':dragging || pointerLink.active ,
-        'flow-node--activity':nodeActivity,
-        'selecting': !!selector
-      }"
+      :class="viewClasses"
       @dragover.prevent
       @drop="managerDrop"
       @mousedown="viewPointerDown"

+ 4 - 20
browser/vue-flow/src/components/flow/link.vue

@@ -1,24 +1,8 @@
 <template>
-  <g
-    class="flow-link"
-    :class="{'flow-link--pointer':pointer}"
-    :status="status"
-    @click="$emit('click',$event)">
-    <path
-      class="flow-link__area"
-      :d="path"
-    />
-
-    <path
-      class="flow-link__visible"
-      :d="path"
-    />
-    <path
-      v-if="status"
-      class="flow-link__status"
-      :d="path"
-    />
-
+  <g class="flow-link" :class="{'flow-link--pointer':pointer}" :status="status" @click="$emit('click',$event)">
+    <path class="flow-link__area" :d="path" />
+    <path class="flow-link__visible" :d="path" />
+    <path v-if="status" class="flow-link__status" :d="path" />
   </g>
 </template>
 <script>

+ 0 - 777
browser/vue-flow/src/components/flow/manager-bak.vue

@@ -1,777 +0,0 @@
-<template>
-  <div class="flow-container">
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      xmlns:xlink="http://www.w3.org/1999/xlink"
-      ref="svg"
-      class="flow-view view"
-      :class="{
-        'flow-linking':linking || stickySockets,
-        'flow-triggers':triggerLinking || stickyTriggers,
-
-        'activity':dragging || pointerLink.active ,
-        'flow-node--activity':nodeActivity,
-        'selecting': !!selector
-      }"
-      @dragover.prevent
-      @drop="managerDrop"
-      @mousedown="viewPointerDown"
-      :width="width"
-      :height="height">
-      <svg-defs/>
-
-      <flow-pan-zoom
-        ref="panzoom"
-        v-model="panzoom">
-
-        <!-- links below nodes -->
-        <flow-link
-          v-for="(link,i) in nodeData.links"
-          :key="'link' + i"
-          v-bind="linkProps(link)"
-          @click="linkPointerClick($event,link)"
-        />
-        <!-- trigger links -->
-        <flow-trigger-link
-          v-for="(trigger,i) in nodeData.triggers"
-          :key="'trigger'+i"
-          label="WIP"
-          v-bind="triggerProps(trigger)"
-          @click="triggerRemove(trigger)"
-        />
-        <!-- nodes -->
-        <flow-node
-          ref="nodes"
-          v-for="(n,i) of nodeData.nodes"
-          v-bind="nodeProps(n)"
-          :key="'node' + n.id"
-          :id="n.id"
-          :selected="nodeSelection[n.id]?true:false"
-          @nodePointerDown.prevent="nodePointerDown($event,i)"
-          @socketPointerDown="socketPointerDown(n.id,...arguments)"
-          @triggerPointerDown="triggerPointerDown(n.id,...arguments)"
-          @nodeDoubleClick="$emit('nodeDblClick',n)"
-          @nodeRightClick="$refs.menu.open($event,n)"
-
-        />
-        <!-- mouse link-->
-        <flow-link
-          :pointer="true"
-          v-if="pointerLink.active"
-          v-bind="pointerLink.props"
-        />
-        <flow-trigger-link
-          :pointer="true"
-          v-if="pointerTriggerLink.active"
-          v-bind="pointerTriggerLink.props"
-        />
-        <rect
-          class="flow-selector"
-          :class="{'flow-selector--selecting':(selector)?true:false}"
-          v-bind="selector"/>
-      </flow-pan-zoom>
-    </svg>
-    <div class="flow-container__control">
-      <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} sockets </button>
-      <button @click="stickyTriggers=!stickyTriggers"> {{ stickyTriggers? 'Hide':'Show' }} triggers </button>
-      <button @click="nodeActivity=!nodeActivity"> {{ nodeActivity? 'Hide':'Show' }} activity </button>
-      <button @click="$emit('documentSave', nodeData)"> Save </button> <!-- should disable until confirmation -->
-      <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">
-        Reset view
-      </button>
-    </div>
-    <div class="flow-container__info">
-      x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }}
-      nodes: {{ nodeData.nodes.length }}
-      links: {{ nodeData.links.length }}
-      triggers: {{ nodeData.triggers.length }}
-    </div>
-    <hx-context-menu ref="menu">
-      <template slot-scope="d" >
-        <div class="flow-node__context-menu">
-          <div class="hover" @click="nodeProcess(d.userData)">Run</div>
-          <div class="hover" tabindex="0" @click="nodeRemove(d.userData)">Delete</div>
-          <hr>
-          <div class="hover" @click="nodeInspect(d.userData,true)">Inspect</div>
-        </div>
-      </template>
-    </hx-context-menu>
-  </div>
-</template>
-<script>
-import FlowNode from './node'
-import FlowLink from './link'
-import FlowTriggerLink from './link-trigger'
-
-import FlowPanZoom from './panzoom'
-import HxContextMenu from '@/components/shared/hx-contextmenu'
-import SvgDefs from './svgdefswrapper'
-import utils from '@/utils/utils'
-
-export default {
-  name: 'FlowManager',
-  components: {FlowNode, FlowLink, FlowTriggerLink, FlowPanZoom, HxContextMenu, SvgDefs},
-  props: {
-    'activity': {type: Object, default: () => {}},
-    'registry': {type: Object, default: () => {}},
-    'width': {type: String, default: '800px'},
-    'height': {type: String, default: '600px'}
-  },
-  data () {
-    // const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
-    return {
-      panzoom: { x: 0, y: 0, zoom: 1 },
-
-      // Shared state
-      nodeData: { nodes: [], links: [], triggers: [] },
-
-      dragging: null,
-      linking: false,
-      triggerLinking: false,
-
-      pointerLink: {active: false, props: {}, src: {}},
-      pointerTriggerLink: {active: false, props: {}, src: {}},
-
-      selector: null,
-      nodeSelection: {},
-
-      stickySockets: false,
-      stickyTriggers: false,
-      nodeActivity: true
-    }
-  },
-  computed: {
-    nodeProps () {
-      return (node) => {
-        let highlight = {}
-        if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
-          if (this.pointerLink.src.in !== undefined) {
-            highlight = {type: 'socket-out', dtype: this.pointerLink.src.type}
-          } else {
-            highlight = {type: 'socket-in', dtype: this.pointerLink.src.type}
-          }
-        }
-        if (this.pointerTriggerLink.active && this.pointerTriggerLink.src.nodeId !== node.id) {
-          highlight = {type: (this.pointerTriggerLink.src.dir === 'in') ? 'trigger-in' : 'trigger-out'}
-        }
-
-        const nodeClass = this.registry[node.src]
-        if (!nodeClass) {
-          this.nodeRemove(node)
-          return
-        }
-        return {
-          transform: `translate(${node.x} ${node.y})`,
-          id: node.id,
-          label: node.label,
-          inputs: nodeClass.inputs,
-          output: nodeClass.output,
-          // Combine this into one
-          match: highlight,
-          dragging: this.dragging && !!this.dragging[node.id],
-          activity: this.activity[node.id],
-          nodeStyle: nodeClass.style
-        }
-      }
-    },
-    linkProps () {
-      return (link) => {
-        if (!this.$refs.nodes) return
-        // For size .x .y
-        const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
-        const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
-
-        const refFrom = this.$refs.nodes.find(n => n.id === link.from)
-        const refTo = this.$refs.nodes.find(n => n.id === link.to)
-        if (!refFrom || !refTo) { // delete link
-          return {}
-        }
-
-        const fromOutput = refFrom.outputPos(0) // only 1 output
-        const toInput = refTo.inputPos(link.in)
-
-        return {
-          x1: nodeFrom.x + fromOutput.x,
-          y1: nodeFrom.y + fromOutput.y,
-          x2: nodeTo.x + toInput.x,
-          y2: nodeTo.y + toInput.y,
-          status: this.activity[nodeFrom.id] && this.activity[nodeFrom.id].status
-        }
-      }
-    },
-    triggerProps () {
-      return (trigger) => {
-        if (!this.$refs.nodes) return
-        // For size .x .y
-        const nodeFrom = this.nodeData.nodes.find(n => n.id === trigger.from)
-        const nodeTo = this.nodeData.nodes.find(n => n.id === trigger.to)
-
-        const refFrom = this.$refs.nodes.find(n => n.id === trigger.from)
-        const refTo = this.$refs.nodes.find(n => n.id === trigger.to)
-        if (!refFrom || !refTo) { // delete trigger
-          return {}
-        }
-
-        const fromOutput = {
-          x: 0,
-          y: refFrom.bodyProps.height / 2
-        }
-        const toInput = {
-          x: 0,
-          y: -refTo.bodyProps.height / 2
-        }
-
-        return {
-          x1: nodeFrom.x + fromOutput.x,
-          y1: nodeFrom.y + fromOutput.y,
-          x2: nodeTo.x + toInput.x,
-          y2: nodeTo.y + toInput.y,
-          status: this.activity[nodeFrom.id] && this.activity[nodeFrom.id].status
-        }
-      }
-    }
-
-  },
-  mounted () {
-    this.$flowService.on('document', (v) => {
-      if (!v || !v.data || !v.data.nodes) { return }
-      const nodes = []
-
-      for (let node of v.data.nodes) {
-        if (this.dragging && !!this.dragging[node.id]) {
-          continue
-        }
-        let localNode = this.nodeData.nodes.find(n => n.id === node.id)
-        if (localNode) {
-          Object.assign(localNode, node)
-          nodes.push(localNode)
-          continue
-        }
-        nodes.push(node)
-      }
-      if (this.dragging) { // Add our nodes
-        for (let nid in this.dragging) {
-          nodes.push(this.dragging[nid])
-        }
-      }
-      this.nodeData = {
-        nodes: nodes,
-        links: v.data.links,
-        triggers: v.data.triggers
-      }
-      this.$nextTick(() => {
-        this.$forceUpdate()
-      })
-    })
-    this.$flowService.on('nodeUpdate', (v) => {
-      console.log('Incoming node update')
-      const nodes = v.data
-      const nd = this.nodeData
-      for (let nid in nodes) {
-        const node = nodes[nid]
-        const idx = nd.nodes.findIndex(n => n.id === node.id)
-        if (idx === -1) { // new node
-          nd.nodes.push(nodes[nid])
-          continue
-        }
-        let lnode = nd.nodes[idx]
-
-        Object.assign(lnode, node)
-
-        // if (!this.dragging || !this.dragging[node.id]) {
-        // this.$set(nd.nodes, idx, node) // new Node
-        // }
-      }
-    })
-    this.$nextTick(() => {
-      this.$forceUpdate()
-    })
-    document.addEventListener('keydown', this.keyDown)
-    document.addEventListener('keyup', this.keyUp)
-  },
-  beoreDestroy () {
-    document.removeEventListener('keydown', this.keyDown)
-    document.removeEventListener('keyup', this.keyUp)
-  },
-  methods: {
-    keyDown (ev) {
-      if (document.activeElement && document.activeElement.matches('input,textarea')) { return }
-      if (ev.shiftKey) {
-        this.linking = true
-        this.triggerLinking = true
-      }
-
-      let single = null
-      const selectionIds = Object.keys(this.nodeSelection)
-      if (selectionIds.length === 1) { single = this.nodeSelection[selectionIds[0]] }
-      switch (ev.key) {
-        case 'Enter':
-          if (!single) { return }
-          this.nodeInspect(single)
-          break
-        case 'Delete':
-          if (!this.nodeSelection) { return }
-          for (let k in this.nodeSelection) {
-            this.nodeRemove(this.nodeSelection[k])
-          }
-          break
-        case 'a':
-          if (ev.ctrlKey) {
-            ev.preventDefault()
-            ev.stopPropagation()
-
-            this.nodeSelection = {}
-            for (let n of this.nodeData.nodes) {
-              this.nodeSelection[n.id] = n
-            }
-          }
-
-          break
-      }
-    },
-    keyUp (ev) {
-      if (!ev.shiftKey) {
-        this.linking = false
-        this.triggerLinking = false
-      }
-    },
-    panzoomReset () {
-      this.panzoom = {x: 0, y: 0, zoom: 1}
-    },
-    // XXX: Shrink this function
-    // and create some LinkAdd method
-    socketPointerDown (nodeId, ev, socket) {
-      if (ev.button !== 0) return
-
-      const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
-      const node = this.nodeData.nodes.find(n => n.id === nodeId)
-      const isInput = socket.in !== undefined
-      const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)
-
-      // const p = this.transformedPoint(ev.clientX, ev.clientY)
-      this.pointerLink.props = {
-        x1: node.x + socketPos.x,
-        y1: node.y + socketPos.y,
-        x2: node.x + socketPos.x,
-        y2: node.y + socketPos.y
-      }
-
-      this.pointerLink.active = true
-      if (isInput) {
-        this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in].type, in: socket.in}
-      } else {
-        this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output.type, out: 0}
-      }
-      utils.createDrag({
-        drag: (ev) => {
-          const p = this.transformedPoint(ev.clientX, ev.clientY)
-          if (isInput) {
-            this.pointerLink.props.x1 = p.x
-            this.pointerLink.props.y1 = p.y
-          } else {
-            this.pointerLink.props.x2 = p.x
-            this.pointerLink.props.y2 = p.y
-          }
-        },
-        drop: (ev) => {
-          this.pointerLink.active = false
-
-          // find Parent
-
-          var curTarget = ev.target
-          for (; curTarget.hasAttribute !== undefined && curTarget !== document.body; curTarget = curTarget.parentNode) {
-            if (curTarget.hasAttribute('data-nodeid')) {
-              break
-            }
-          }
-          if (!curTarget.hasAttribute || curTarget === document.body) {
-            console.error('LINK: target is not a socket')
-            return
-          }
-
-          const targetNodeId = curTarget.getAttribute('data-nodeid')
-          const targetIn = curTarget.getAttribute('data-in')
-          const targetOut = curTarget.getAttribute('data-out')
-
-          let link
-          // target is input
-          if (targetIn && !isInput) {
-            link = {
-              from: nodeId,
-              to: targetNodeId,
-              in: parseInt(targetIn)
-            }
-          } else if (targetOut && isInput) {
-            link = {
-              from: targetNodeId,
-              to: nodeId,
-              in: socket.in
-            }
-          }
-          // No link
-          if (!link) {
-            console.error('LINK: input same direction (in->in/out->out)')
-            return
-          }
-          const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
-          const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)
-
-          const output = this.registry[nodeFrom.src].output.type
-          const input = this.registry[nodeTo.src].inputs[link.in].type
-          // Type checking
-          if (!(output === 'interface {}' || output === input || input === 'interface {}')) {
-            console.error('LINK: Invalid type')
-            return
-          }
-
-          // Input already exists, replace
-          const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
-          if (existingInputI !== -1) {
-            this.nodeData.links.splice(existingInputI, 1)
-          }
-          this.linkAdd(link)
-        }})
-    },
-    triggerPointerDown (nodeId, ev, dir) {
-      if (ev.button !== 0) return
-
-      const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
-      const node = this.nodeData.nodes.find(n => n.id === nodeId)
-
-      const isInput = (dir === 'in')
-      const triggerPos = {
-        x: 0,
-        y: nodeRef.bodyProps.height / 2 * (isInput ? -1 : 1)
-      }
-      this.pointerTriggerLink.src = {nodeId: node.id, dir: dir}
-
-      this.pointerTriggerLink.props = {
-        x1: node.x + triggerPos.x,
-        y1: node.y + triggerPos.y,
-        x2: node.x + triggerPos.x,
-        y2: node.y + triggerPos.y
-      }
-
-      this.pointerTriggerLink.active = true
-      utils.createDrag({
-        drag: (ev) => {
-          const p = this.transformedPoint(ev.clientX, ev.clientY)
-          if (isInput) {
-            this.pointerTriggerLink.props.x1 = p.x
-            this.pointerTriggerLink.props.y1 = p.y
-          } else {
-            this.pointerTriggerLink.props.x2 = p.x
-            this.pointerTriggerLink.props.y2 = p.y
-          }
-        },
-        drop: (ev) => {
-          this.pointerTriggerLink.active = false
-
-          // find Parent
-          var curTarget = ev.target
-          for (; curTarget.hasAttribute !== undefined && curTarget !== document.body; curTarget = curTarget.parentNode) {
-            if (curTarget.hasAttribute('data-nodeid')) {
-              break
-            }
-          }
-          if (!curTarget.hasAttribute || curTarget === document.body) {
-            console.error('TRIGGER: target is not a socket')
-            return
-          }
-          const targetNodeId = curTarget.getAttribute('data-nodeid')
-          const targetDir = curTarget.getAttribute('data-dir')
-          if (targetNodeId === node.id) {
-            console.error('TRIGGER: cannot link to self')
-            return
-          }
-
-          let trigger
-          // target is input
-          if (targetDir === 'in') {
-            trigger = {
-              from: nodeId,
-              to: targetNodeId
-            }
-          } else {
-            trigger = {
-              from: targetNodeId,
-              to: nodeId
-            }
-          }
-          this.triggerAdd(trigger)
-        }
-      })
-    },
-
-    nodeInspect (tnode, force) {
-      this.$emit('nodeInspect', tnode, force)
-    },
-
-    nodePointerDown (ev, i) {
-      document.activeElement && document.activeElement.blur()
-      const tnode = this.nodeData.nodes[i]
-      if (ev.button === 1) {
-        this.nodeRemove(tnode)
-        // remove related links
-        return
-      }
-      if (ev.button !== 0) return // first button
-      if (ev.shiftKey) {
-        this.socketPointerDown(tnode.id, ev, {out: 0})
-        return
-      }
-      this.nodeInspect(tnode)
-
-      // Switch selection
-      if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) this.nodeSelection = {}
-      this.nodeSelection[tnode.id] = tnode
-      // we can handle with nodeId and a search
-
-      // put the list of nodes in last
-      for (let nk in this.nodeSelection) {
-        let ni = this.nodeData.nodes.findIndex(n => n.id === this.nodeSelection[nk].id)
-        this.nodeData.nodes.splice(ni, 1)
-        this.nodeData.nodes.push(this.nodeSelection[nk]) // put in last
-      }
-      let curP = this.transformedPoint(ev.x, ev.y)
-
-      this.dragging = this.nodeSelection
-      utils.createDrag({
-        drag: (ev) => {
-          if (this.nodeSelection === undefined) {
-            console.error('Well something went wrong')
-          }
-          const dragP = this.transformedPoint(ev.x, ev.y)
-          for (let n in this.nodeSelection) {
-            this.nodeSelection[n].x += dragP.x - curP.x
-            this.nodeSelection[n].y += dragP.y - curP.y
-          }
-          this.sendFlowEvent('nodeUpdate', this.nodeSelection)
-          curP = dragP
-        },
-        drop: (ev) => {
-          this.dragging = null
-          this.sendDocumentUpdate()
-        }
-      })
-    },
-
-    // STORE
-    nodeRemove (node) {
-      const i = this.nodeData.nodes.indexOf(node)
-      if (i === -1) return
-      this.nodeData.links = this.nodeData.links.filter(l => l.from !== node.id && l.to !== node.id)
-      this.nodeData.triggers = this.nodeData.triggers.filter(l => l.from !== node.id && l.to !== node.id)
-
-      this.nodeData.nodes.splice(i, 1)
-      this.sendFlowEvent('nodeRemove', node)
-      this.sendDocumentUpdate()
-    },
-    /// ////////////
-    // NODE CREATOR FUNC
-    //
-    // STORE
-    nodeAdd (src, x = 100, y = 100) {
-      const newNode = {
-        id: utils.guid(),
-        x: x,
-        y: y,
-        defaultInputs: {},
-        label: src,
-        src: src
-      }
-      // Setup Props
-      if (this.registry[src].props) {
-        for (let k in this.registry[src].props) {
-          newNode.prop || (newNode.prop = {})
-          newNode.prop[k] = ''
-        }
-      }
-
-      this.nodeData.nodes.push(newNode)
-      let nu = {}
-      this.sendFlowEvent('nodeUpdate', (nu[newNode.id] = newNode, nu))
-      this.sendDocumentUpdate()
-    },
-    nodeProcess (node) {
-      this.sendDocumentUpdate()
-      console.log('Node process demand')
-      this.$emit('nodeProcess', node)
-    },
-    linkPointerClick (ev, link) {
-      ev.preventDefault()
-      this.linkRemove(link)
-    },
-    linkAdd (link) {
-      this.nodeData.links.push(link)
-      this.sendFlowEvent('linkUpdate', link)
-      this.sendDocumentUpdate()
-    },
-    linkRemove (link) {
-      const i = this.nodeData.links.findIndex(l => l === link)
-      if (i === -1) return
-      this.nodeData.links.splice(i, 1)
-      this.sendFlowEvent('linkRemove', link)
-      this.sendDocumentUpdate()
-    },
-
-    triggerAdd (trigger) {
-      this.nodeData.triggers.push(trigger)
-      this.sendDocumentUpdate()
-    },
-    triggerRemove (trigger) {
-      const i = this.nodeData.triggers.findIndex(l => l === trigger)
-      if (i === -1) return
-      this.nodeData.triggers.splice(i, 1)
-      this.sendFlowEvent('triggerRemove', trigger)
-      this.sendDocumentUpdate()
-    },
-
-    managerDrop (ev) {
-      ev.preventDefault()
-      const reg = ev.dataTransfer.getData('text')
-      if (this.registry[reg] === undefined) {
-        console.error('Registry: Drop src not found in registry')
-        return
-      }
-      const pt = this.transformedPoint(ev.x, ev.y)
-      this.nodeAdd(reg, pt.x, pt.y)
-    },
-    viewPointerDown (ev) {
-      if (ev.button !== 0) return
-      ev.preventDefault()
-      this.nodeSelection = {}
-      const p = this.transformedPoint(ev.x, ev.y)
-      this.selector = {x: p.x, y: p.y, width: 0, height: 0}
-      utils.createDrag({
-        drag: (evd) => {
-          // transform again in case we changed zoom/pan
-          const p = this.transformedPoint(ev.x, ev.y)
-          const p2 = this.transformedPoint(evd.x, evd.y)
-          const nwidth = p2.x - p.x
-          const nheight = p2.y - p.y
-          this.selector = {
-            x: nwidth < 0 ? p2.x : p.x,
-            y: nheight < 0 ? p2.y : p.y,
-            width: nwidth < 0 ? -nwidth : nwidth,
-            height: nheight < 0 ? -nheight : nheight
-          }
-        },
-        drop: (ev) => {
-          for (let n of this.nodeData.nodes) {
-            if (n.x > this.selector.x && n.x < (this.selector.x + this.selector.width) &&
-              n.y > this.selector.y && n.y < (this.selector.y + this.selector.height)
-            ) {
-              this.nodeSelection[n.id] = n
-            }
-          }
-          this.selector = null
-        }})
-    },
-
-    // service events
-    sendFlowEvent (type, param) {
-      this.$flowService[type](param)
-    },
-    sendDocumentUpdate () {
-      this.$flowService.documentUpdate(this.nodeData, this.$route.params.sessId)
-    },
-    /* sendDocumentRun () {
-      console.log('Document running')
-      this.$flowService.documentRun(this.nodeData, this.$route.params.sessId)
-    }, */
-    // HELPERS depending on svg ref
-    createSVGPoint (x, y) {
-      const p = this.$refs.svg.createSVGPoint()
-      p.x = x; p.y = y
-      return p
-    },
-    transformedPoint (x, y, abs) {
-      const svgRect = this.$refs.svg.getBoundingClientRect()
-      if (!abs) {
-        x -= svgRect.left
-        y -= svgRect.top
-      }
-      return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
-    }
-  }
-}
-
-</script>
-<style>
-.flow-container {
-  display:flex;
-  flex-flow:row;
-  position:relative;
-}
-
-.flow-container__control {
-  position:absolute;
-  top: 20px;
-  left: 20px;
-  display:flex;
-  justify-content: center;
-  align-items: center;
-  color: #fff;
-}
-
-.flow-container__control button {
-  display:flex;
-  justify-content: center;
-  margin:0;
-  padding:14px;
-  color:#333;
-}
-
-.flow-container__info {
-  position:absolute;
-  bottom:10px;
-  left:10px;
-  padding:2px;
-  font-size:9px;
-}
-
-.flow-view {
-  border:none;
-  position:relative;
-  fill:transparent;
-}
-
-.flow-selector {
-  pointer-events:none;
-  opacity:0;
-}
-
-.flow-selector.flow-selector--selecting {
-  opacity:1;
-}
-
-.flow-node__context-menu {
-  box-shadow: 0 1px 4px  rgba(0,0,0,0.2);
-  display:flex;
-  flex-flow: column;
-  justify-content: start;
-  align-items: stretch;
-  width:180px;
-}
-
-.flow-node__context-menu > div {
-  padding:10px 20px;
-  cursor: pointer;
-}
-
-.flow-node__context-menu > * {
-  width:100%;
-}
-
-.flow-node__context-menu hr {
-  width:90%;
-  border:none;
-  border-bottom: solid 1px rgba(150,150,150,0.2);
-}
-
-.flow-node__context-menu button {
-  text-align:left;
-}
-
-</style>

+ 2 - 18
browser/vue-flow/src/components/flow/node-activity.vue

@@ -56,7 +56,7 @@ export default {
     ellapsed () {
       if (!this.finishTime) return null
       const s = new Date(Date.parse(this.activity.startTime))
-      if (!dateIsValid(s)) { return null }
+      if (!utils.dateIsValid(s)) { return null }
       let intervalms = this.finishTime - s
       if (intervalms < 0) {
         intervalms = 0
@@ -71,9 +71,6 @@ export default {
     activity (val, oldVal) {
       this.finishTime = null
       this.updateTime()
-      /* const finish = new Date(Date.parse(val.endTime))
-      if (dateIsValid(finish)) {
-      } */
     }
   },
   mounted () {
@@ -85,7 +82,7 @@ export default {
   methods: {
     updateTime () {
       const finish = new Date(Date.parse(this.activity.endTime))
-      if (dateIsValid(finish)) {
+      if (utils.dateIsValid(finish)) {
         this.finishTime = finish
         return
       }
@@ -95,19 +92,6 @@ export default {
 
   }
 }
-// Golang 0 date
-const invalidDate = -62135596800000
-function dateIsValid (d) {
-  if (Object.prototype.toString.call(d) === '[object Date]') {
-    if (!isNaN(d.getTime())) { // d.valueOf() could also work
-      if (d.getTime() === invalidDate) {
-        return false
-      }
-      return true
-    }
-  }
-  return false
-}
 </script>
 
 <style>

+ 67 - 66
browser/vue-flow/src/services/flowservice.js

@@ -15,78 +15,79 @@ let log = () => {}
 if (debug) {
   log = console.log.bind(console.log, '%cSVC:', 'color:#0a0', (Math.random() * 1000).toFixed())
 }
-
+function FlowService () {
 // singleton per module
-var ws = null
-var connected = false
-var reconnect = false
-var eventBus = new Vue()
-var loc = '' // The location
-// WS Connector
-function connect () {
-  reconnect = true
-  ws = new window.WebSocket(loc)
-  ws.onopen = () => {
-    log('connected', ws)
-    connected = true
-    eventBus.$emit('open')
-  }
-  ws.onmessage = (e) => { // receiving message
-    log('received:', e.data)
-    const msg = JSON.parse(e.data)
-    eventBus.$emit(msg.op, msg) // Pass message through
-  }
-  ws.onerror = (e) => { connected = false }
-  ws.onclose = (e) => {
-    log('Lost connection', e)
-    if (connected === true) {
-      eventBus.$emit('close')
-    } // emit close
-    connected = false
-    if (reconnect !== true) {
-      return
+  var ws = null
+  var connected = false
+  var reconnect = false
+  var eventBus = new Vue()
+  var loc = '' // The location
+
+  // WS Connector
+  function connect () {
+    reconnect = true
+    ws = new window.WebSocket(loc)
+    ws.onopen = () => {
+      log('connected', ws)
+      connected = true
+      eventBus.$emit('open')
+    }
+    ws.onmessage = (e) => { // receiving message
+      log('received:', e.data)
+      const msg = JSON.parse(e.data)
+      eventBus.$emit(msg.op, msg) // Pass message through
     }
+    ws.onerror = (e) => { connected = false }
+    ws.onclose = (e) => {
+      log('Lost connection', e)
+      if (connected === true) {
+        eventBus.$emit('close')
+      } // emit close
+      connected = false
+      if (reconnect !== true) {
+        return
+      }
 
-    setTimeout(() => connect(loc), 3000) // Reconnect
+      setTimeout(() => connect(loc), 3000) // Reconnect
+    }
   }
-}
-
-const service = {
-  send (msg) {
-    ws.send(JSON.stringify(msg))
-  },
-  connected (cb) {
-    if (connected === false) {
-      eventBus.$on('open', cb)
-      return
+  const service = {
+    send (msg) {
+      ws.send(JSON.stringify(msg))
+    },
+    connected (cb) {
+      if (connected === false) {
+        eventBus.$on('open', cb)
+        return
+      }
+      cb()
+    },
+    on: eventBus.$on.bind(eventBus),
+    once: eventBus.$once.bind(eventBus),
+    off: eventBus.$off.bind(eventBus),
+    connect (ploc) {
+      loc = ploc
+      if (ws) { ws.close() }
+      if (!reconnect) connect()
+      log('Connecting to', loc)
     }
-    cb()
-  },
-  on: eventBus.$on.bind(eventBus),
-  once: eventBus.$once.bind(eventBus),
-  off: eventBus.$off.bind(eventBus),
-  connect (ploc) {
-    loc = ploc
-    if (ws) { ws.close() }
-    if (!reconnect) connect()
-    log('Connecting to', loc)
   }
-}
 
-methods.forEach(ftyp => {
-  service[ftyp] = (param, id) => {
-    log('sending:', ftyp, ' -- ', param)
-    if (connected) {
-      service.send({op: ftyp, id: id, data: param})
-      return
+  methods.forEach(ftyp => {
+    service[ftyp] = (param, id) => {
+      log('sending:', ftyp, ' -- ', param)
+      if (connected) {
+        service.send({op: ftyp, id: id, data: param})
+        return
+      }
+      if (ftyp !== 'documentUpdate') return // Do not persist other than documentUpdate on reconnection
+      // Schedule when is connected
+      service.once('open', () => {
+        service.send({op: ftyp, id: id, data: param})
+      })
     }
-    if (ftyp !== 'documentUpdate') return // Do not persist other than documentUpdate on reconnection
-    // Schedule when is connected
-    service.once('open', () => {
-      service.send({op: ftyp, id: id, data: param})
-    })
-  }
-})
-this.id = Math.random()
+  })
+  Object.assign(this, service)
+}
 
-export default service
+export default new FlowService()

+ 14 - 0
browser/vue-flow/src/utils/utils.js

@@ -25,6 +25,20 @@ module.exports = {
 
     return text
   },
+  dateIsValid (d) {
+    // Golang 0 date
+    const invalidDate = -62135596800000
+    if (Object.prototype.toString.call(d) === '[object Date]') {
+      if (!isNaN(d.getTime())) { // d.valueOf() could also work
+        if (d.getTime() === invalidDate) {
+          return false
+        }
+        return true
+      }
+    }
+    return false
+  },
+
   textWrap (text, maxLen, opt) {
     const ret = []
     let parts = text.split(' ', -1)

+ 194 - 11
browser/vue-flow/yarn.lock

@@ -124,17 +124,23 @@ acorn-dynamic-import@^2.0.0:
   dependencies:
     acorn "^4.0.3"
 
+acorn-globals@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
+  dependencies:
+    acorn "^4.0.4"
+
 acorn-jsx@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
   dependencies:
     acorn "^3.0.4"
 
-acorn@^3.0.4:
+acorn@^3.0.4, acorn@^3.1.0, acorn@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
 
-acorn@^4.0.3:
+acorn@^4.0.3, acorn@^4.0.4, acorn@~4.0.2:
   version "4.0.13"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
 
@@ -275,6 +281,10 @@ arrify@^1.0.0, arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
 
+asap@~2.0.3:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+
 asn1.js@^4.0.0:
   version "4.9.2"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a"
@@ -1188,6 +1198,12 @@ chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0:
     escape-string-regexp "^1.0.5"
     supports-color "^4.0.0"
 
+character-parser@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
+  dependencies:
+    is-regex "^1.0.3"
+
 chardet@^0.4.0:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
@@ -1234,6 +1250,13 @@ clean-css@4.1.x:
   dependencies:
     source-map "0.5.x"
 
+clean-css@^3.3.0:
+  version "3.4.28"
+  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
+  dependencies:
+    commander "2.8.x"
+    source-map "0.4.x"
+
 cli-cursor@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
@@ -1333,6 +1356,12 @@ commander@2.12.x, commander@^2.9.0, commander@~2.12.1:
   version "2.12.2"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
 
+commander@2.8.x:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
+  dependencies:
+    graceful-readlink ">= 1.0.0"
+
 commondir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -1387,6 +1416,13 @@ consolidate@^0.14.0:
   dependencies:
     bluebird "^3.1.1"
 
+constantinople@^3.0.1:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.0.tgz#7569caa8aa3f8d5935d62e1fa96f9f702cd81c79"
+  dependencies:
+    acorn "^3.1.0"
+    is-expression "^2.0.1"
+
 constants-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -1810,6 +1846,10 @@ doctrine@^2.1.0:
   dependencies:
     esutils "^2.0.2"
 
+doctypes@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
+
 dom-converter@~0.1:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
@@ -2709,6 +2749,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
+"graceful-readlink@>= 1.0.0":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+
 handle-thing@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
@@ -3111,6 +3155,20 @@ is-equal-shallow@^0.1.3:
   dependencies:
     is-primitive "^2.0.0"
 
+is-expression@^2.0.1:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-2.1.0.tgz#91be9d47debcfef077977e9722be6dcfb4465ef0"
+  dependencies:
+    acorn "~3.3.0"
+    object-assign "^4.0.1"
+
+is-expression@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-3.0.0.tgz#39acaa6be7fd1f3471dc42c7416e61c24317ac9f"
+  dependencies:
+    acorn "~4.0.2"
+    object-assign "^4.0.1"
+
 is-extendable@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@@ -3212,7 +3270,7 @@ is-primitive@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
 
-is-promise@^2.1.0:
+is-promise@^2.0.0, is-promise@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
 
@@ -3220,7 +3278,7 @@ is-property@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
 
-is-regex@^1.0.4:
+is-regex@^1.0.3, is-regex@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
   dependencies:
@@ -3290,6 +3348,10 @@ js-base64@^2.1.8, js-base64@^2.1.9:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.0.tgz#9e566fee624751a1d720c966cd6226d29d4025aa"
 
+js-stringify@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
+
 js-tokens@^3.0.0, js-tokens@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
@@ -3371,6 +3433,13 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
+jstransformer@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
+  dependencies:
+    is-promise "^2.0.0"
+    promise "^7.0.1"
+
 killable@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
@@ -4511,6 +4580,12 @@ promise-inflight@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
 
+promise@^7.0.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  dependencies:
+    asap "~2.0.3"
+
 proxy-addr@~2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
@@ -4536,6 +4611,99 @@ public-encrypt@^4.0.0:
     parse-asn1 "^5.0.0"
     randombytes "^2.0.1"
 
+pug-attrs@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-2.0.2.tgz#8be2b2225568ffa75d1b866982bff9f4111affcb"
+  dependencies:
+    constantinople "^3.0.1"
+    js-stringify "^1.0.1"
+    pug-runtime "^2.0.3"
+
+pug-code-gen@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.0.tgz#96aea39a9e62f1ec5d2b6a5b42a29d528c70b43d"
+  dependencies:
+    constantinople "^3.0.1"
+    doctypes "^1.1.0"
+    js-stringify "^1.0.1"
+    pug-attrs "^2.0.2"
+    pug-error "^1.3.2"
+    pug-runtime "^2.0.3"
+    void-elements "^2.0.1"
+    with "^5.0.0"
+
+pug-error@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-1.3.2.tgz#53ae7d9d29bb03cf564493a026109f54c47f5f26"
+
+pug-filters@^2.1.5:
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-2.1.5.tgz#66bf6e80d97fbef829bab0aa35eddff33fc964f3"
+  dependencies:
+    clean-css "^3.3.0"
+    constantinople "^3.0.1"
+    jstransformer "1.0.0"
+    pug-error "^1.3.2"
+    pug-walk "^1.1.5"
+    resolve "^1.1.6"
+    uglify-js "^2.6.1"
+
+pug-lexer@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-3.1.0.tgz#fd087376d4a675b4f59f8fef422883434e9581a2"
+  dependencies:
+    character-parser "^2.1.1"
+    is-expression "^3.0.0"
+    pug-error "^1.3.2"
+
+pug-linker@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-3.0.3.tgz#25f59eb750237f0368e59c3379764229c0189c41"
+  dependencies:
+    pug-error "^1.3.2"
+    pug-walk "^1.1.5"
+
+pug-load@^2.0.9:
+  version "2.0.9"
+  resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-2.0.9.tgz#ee217c914cc1d9324d44b86c32d1df241d36de7a"
+  dependencies:
+    object-assign "^4.1.0"
+    pug-walk "^1.1.5"
+
+pug-parser@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-4.0.0.tgz#c9f52322e4eabe4bf5beeba64ed18373bb627801"
+  dependencies:
+    pug-error "^1.3.2"
+    token-stream "0.0.1"
+
+pug-runtime@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-2.0.3.tgz#98162607b0fce9e254d427f33987a5aee7168bda"
+
+pug-strip-comments@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-1.0.2.tgz#d313afa01bcc374980e1399e23ebf2eb9bdc8513"
+  dependencies:
+    pug-error "^1.3.2"
+
+pug-walk@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-1.1.5.tgz#90e943acbcf7021e6454cf1b32245891cba6f851"
+
+pug@^2.0.0-rc.4:
+  version "2.0.0-rc.4"
+  resolved "https://registry.yarnpkg.com/pug/-/pug-2.0.0-rc.4.tgz#b7b08f6599bd5302568042b7436984fb28c80a13"
+  dependencies:
+    pug-code-gen "^2.0.0"
+    pug-filters "^2.1.5"
+    pug-lexer "^3.1.0"
+    pug-linker "^3.0.3"
+    pug-load "^2.0.9"
+    pug-parser "^4.0.0"
+    pug-runtime "^2.0.3"
+    pug-strip-comments "^1.0.2"
+
 pump@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
@@ -4928,7 +5096,7 @@ resolve-from@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
 
-resolve@^1.2.0, resolve@^1.3.3, resolve@^1.4.0:
+resolve@^1.1.6, resolve@^1.2.0, resolve@^1.3.3, resolve@^1.4.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
   dependencies:
@@ -5189,16 +5357,16 @@ source-map-support@^0.4.15:
   dependencies:
     source-map "^0.5.6"
 
-source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
-  version "0.5.7"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
-
-source-map@^0.4.2:
+source-map@0.4.x, source-map@^0.4.2:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
   dependencies:
     amdefine ">=0.0.4"
 
+source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
 source-map@^0.6.1, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
@@ -5486,6 +5654,10 @@ to-fast-properties@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
 
+token-stream@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a"
+
 toposort@^1.0.0:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec"
@@ -5552,7 +5724,7 @@ uglify-js@3.3.x:
     commander "~2.12.1"
     source-map "~0.6.1"
 
-uglify-js@^2.8.29:
+uglify-js@^2.6.1, uglify-js@^2.8.29:
   version "2.8.29"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
   dependencies:
@@ -5691,6 +5863,10 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
+void-elements@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+
 vue-eslint-parser@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.2.tgz#8d603545e9d7c134699075bd1772af1ffd86b744"
@@ -5890,6 +6066,13 @@ window-size@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
 
+with@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/with/-/with-5.1.1.tgz#fa4daa92daf32c4ea94ed453c81f04686b575dfe"
+  dependencies:
+    acorn "^3.1.0"
+    acorn-globals "^3.0.0"
+
 wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"

+ 6 - 0
go/src/flow/flow.go

@@ -155,6 +155,12 @@ func (f *Flow) Op(name string, params ...interface{}) (Operation, error) {
 	// Initialize opfunc maybe
 }
 
+// HasOp verifies if an operation exists
+func (f *Flow) HasOp(name string) bool {
+	_, ret := f.operations.Load(name)
+	return ret
+}
+
 // Must it will panic on error
 func (f *Flow) Must(op Operation, err error) Operation {
 	if err != nil {

+ 27 - 1
go/src/flowserver/cmd/demo1/main.go

@@ -7,7 +7,10 @@ import (
 	"flowserver/cmd/demo1/devops"
 	"flowserver/cmd/demo1/testops"
 	"log"
+	"mime"
 	"net/http"
+	"path/filepath"
+	"strings"
 
 	"github.com/gohxs/prettylog"
 	"github.com/gohxs/webu"
@@ -27,7 +30,7 @@ func main() {
 	c := chain.New(webu.ChainLogger(prettylog.New("req")))
 
 	mux := http.NewServeMux()
-	mux.HandleFunc("/", assets.AssetHandleFunc)
+	mux.HandleFunc("/", assetFunc)
 
 	mux.Handle("/default/", c.Build(flowserver.New(defaultops.New(), "default").ServeHTTP))
 	mux.Handle("/devops/", c.Build(flowserver.New(devops.New(), "devops").ServeHTTP))
@@ -36,3 +39,26 @@ func main() {
 	// Context registry
 	http.ListenAndServe(addr, mux)
 }
+
+func assetFunc(w http.ResponseWriter, r *http.Request) {
+	urlPath := ""
+
+	// func that handles mux
+	server := r.Context().Value(http.ServerContextKey).(*http.Server)
+	mux, ok := server.Handler.(*http.ServeMux)
+	if ok {
+		_, handlerPath := mux.Handler(r)
+		urlPath = strings.TrimPrefix(r.URL.String(), handlerPath)
+	}
+	if urlPath == "" { // Auto index
+		urlPath = "index.html"
+	}
+	data, ok := assets.Data[urlPath]
+	if !ok {
+		http.Redirect(w, r, "/default", 302)
+		//w.WriteHeader(404)
+	}
+
+	w.Header().Set("Content-type", mime.TypeByExtension(filepath.Ext(urlPath)))
+	w.Write(data)
+}

+ 115 - 62
go/src/flowserver/flowbuilder.go

@@ -2,10 +2,13 @@ package flowserver
 
 import (
 	"encoding/json"
+	"errors"
 	"flow"
 	"flow/registry"
+	"fmt"
 	"log"
 	"reflect"
+	"strconv"
 	"strings"
 	"time"
 )
@@ -40,59 +43,93 @@ type FlowDocument struct {
 	Triggers []Trigger `json:"triggers"`
 }
 
-// FlowBuild build a flowGraph from incoming web ui json
-func FlowBuild(rawData []byte, r *registry.R) (*flow.Flow, error) {
+func (fd *FlowDocument) fetchTriggerFrom(ID string) []Trigger {
+	ret := []Trigger{}
+	for _, t := range fd.Triggers {
+		if t.From != ID {
+			continue
+		}
+		log.Println("Trigger is:", ID, "adding")
+		ret = append(ret, t)
+	}
+	return ret
+}
+
+func (fd *FlowDocument) fetchLinksTo(ID string) []Link {
+	ret := []Link{}
+	for _, l := range fd.Links {
+		if l.To != ID {
+			continue
+		}
+		ret = append(ret, l)
+	}
+	return ret
+}
+
+var ErrLoop = errors.New("Looping through loops is disabled for now")
+
+// FlowBuild build the graph based on an starting node
+func FlowBuild(rawData []byte, r *registry.R, startingID string) (*flow.Flow, error) {
+
+	log.Println("Unmarshal document")
 	doc := FlowDocument{[]Node{}, []Link{}, []Trigger{}}
 	err := json.Unmarshal(rawData, &doc)
 	if err != nil {
 		return nil, err
 	}
 
+	log.Println("Create new flow and add a registry")
 	f := flow.New()
 	f.SetRegistry(r)
+
+	log.Println("Temporarly map the nodes with ID")
 	nodeMap := map[string]Node{}
 	for _, n := range doc.Nodes {
+		log.Println("Mapping:", n.ID)
 		nodeMap[n.ID] = n
 	}
-	inputMap := map[string]flow.Operation{}
+	//inputMap := map[string]flow.Operation{}
 
-	ninput := 0
-	for _, n := range doc.Nodes {
-		if n.Src == "Input" || n.Src == "Variable" || n.Src == "Const" {
-			continue
+	nodeTrack := map[string]bool{}
+
+	log.Println("Recursive node matching")
+	var fetchInputs func(ID string) error
+	fetchInputs = func(ID string) error {
+		log.Println("Check for loops")
+		if _, ok := nodeTrack[ID]; ok {
+			return ErrLoop //fmt.Errorf("[%v] Looping through nodes is disabled:", ID)
 		}
+		nodeTrack[ID] = true
 
-		// Find link refered as To
-		entry, err := r.Entry(n.Src)
-		if err != nil {
-			return nil, err
+		// If flow already has ID just return
+		if f.HasOp(ID) {
+			log.Println("Entry found, continuing")
+			return nil
 		}
 
-		param := make([]flow.Data, len(entry.Inputs))
+		node, ok := nodeMap[ID]
+		if !ok {
+			return fmt.Errorf("node not found [%v]", startingID)
+		}
 
-		// Default inputs
-		for i := range param {
-			if v, ok := n.DefaultInputs[i]; ok {
-				param[i] = v
-			}
+		entry, err := r.Entry(node.Src)
+		if err != nil {
+			return err
 		}
 
-		// Find links
-		for _, l := range doc.Links {
-			if l.To != n.ID {
-				continue
-			}
-			// Define operators here
+		inputs := doc.fetchLinksTo(node.ID)
+		param := make([]flow.Data, len(entry.Inputs))
+		// Build ops
+		for _, l := range inputs {
 			from := nodeMap[l.From]
+
 			switch from.Src {
 			case "Input":
-				inOp, ok := inputMap[n.ID]
-				if !ok {
-					inOp = f.In(ninput)
-					inputMap[n.ID] = inOp
-					ninput++
+				inputID, err := strconv.Atoi(from.Prop["input"])
+				if err != nil {
+					return errors.New("Invalid inputID value, must be a number")
 				}
-				param[l.In] = inOp // By id perhaps
+				param[l.In] = f.In(inputID) // By id perhaps
 			case "Variable":
 				param[l.In] = f.Var(from.ID, from.Prop["init"])
 			case "Const":
@@ -105,57 +142,73 @@ func FlowBuild(rawData []byte, r *registry.R) (*flow.Flow, error) {
 					var val string
 					json.Unmarshal([]byte(raw), &val)
 					param[l.In] = val
+					log.Println("next Input")
 					continue
 				}
-				log.Println("Will unmarshal to input:", raw)
 				newVal := reflect.New(entry.Inputs[l.In])
 				err := json.Unmarshal([]byte(raw), newVal.Interface())
 				if err != nil {
 					// ignore error?
 					log.Println("unmarshalling Error", err)
+					return err
 					//param[l.In] = nil
 					//continue
 				}
 				param[l.In], _ = f.Const(newVal.Elem().Interface())
 			default:
 				param[l.In] = f.Res(from.ID)
+				// Sub process the child
+				if err := fetchInputs(from.ID); err != nil {
+					return err
+				}
 			}
 		}
-		f.DefOp(n.ID, n.Src, param...)
-	}
-	//Trigger testing
-	for _, t := range doc.Triggers {
-		log.Println("Trigger:", t)
-		log.Println("Attaching an hook per trigger test")
-
-		f.Hook(flow.Hook{
-			Any: func(name string, ID string, triggerTime time.Time, extra ...interface{}) {
-				if name == "waiting" {
-					return
-				}
-				if ID != t.From {
-					return
-				}
-				exec := false
-				for _, o := range t.On {
-					if name == o {
-						exec = true
-						break
+		f.DefOp(node.ID, node.Src, param...)
+
+		// Process triggers for this node
+		triggers := doc.fetchTriggerFrom(node.ID)
+		for _, t := range triggers {
+			log.Println("Trigger:", t)
+			log.Println("Registering operator", t.To)
+			err := fetchInputs(t.To)
+			if err != nil {
+				log.Println("Error on trigger input", err)
+				return err
+			}
+			// Register the thing here
+			f.Hook(flow.Hook{
+				Any: func(name string, ID string, triggerTime time.Time, extra ...interface{}) {
+					if name != "Error" && name != "Finish" {
+						return
+					}
+					if ID != t.From {
+						log.Printf("ID[%v] triggered [%v], I'm t.From: %v", ID, name, t.From)
+						return
+					}
+					exec := false
+					for _, o := range t.On {
+						if name == o {
+							exec = true
+							break
+						}
+					}
+					if !exec {
+						log.Println("Mismatching trigger, but its a test")
 					}
-				}
-				log.Println("Should trigger on:", name, t.On)
-				if !exec {
-					log.Println("Mismatching trigger, but its a test")
-				}
 
-				log.Println("Grabbing an executor:", t.To)
-				op := f.Res(t.To)
-				log.Println("Processing this operator")
-				go op.Process(name) // Background
-				log.Println("Returning from operator")
-			},
-		})
+					log.Println("Matching trigger", t.To)
+					op := f.Res(t.To)
+					go op.Process(name) // Background
+				},
+			})
 
+			delete(nodeTrack, node.ID)
+		}
+		return nil
+	}
+	err = fetchInputs(startingID)
+	if err != nil {
+		return nil, err
 	}
 
 	return f, nil

+ 2 - 1
go/src/flowserver/session.go

@@ -200,10 +200,11 @@ func (s *FlowSession) NodeRun(c *websocket.Conn, data []byte) error {
 			return s
 		})
 
-		s.flow, err = FlowBuild(s.RawDoc, localr)
+		s.flow, err = FlowBuild(s.RawDoc, localr, ID)
 
 		log.Println("Flow:", s.flow)
 		if err != nil {
+			s.Notify(fmt.Sprint("ERR:", err))
 			log.Println("Flow error:", err)
 			return
 		}