فهرست منبع

Refactored to use vux

luis 7 سال پیش
والد
کامیت
6869adf236

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

@@ -49,6 +49,7 @@
     "vue-router": "^3.0.1",
     "vue-svg-loader": "^0.4.0",
     "vue-template-compiler": "^2.4.4",
+    "vuex": "^3.0.1",
     "webpack": "^3.6.0",
     "webpack-dev-server": "^2.9.1"
   }

+ 0 - 54
browser/vue-flow/src/components/defregistry.js

@@ -1,54 +0,0 @@
-export default{
-  'Input': {
-    categories: ['core'],
-    output: {type: 'interface {}'},
-    style: { color: '#686', shape: 'circle' },
-    props: {} // should be sent in the node
-  },
-  'Variable': {
-    categories: ['core'],
-    output: {type: 'interface {}'},
-    style: { color: '#88a', shape: 'circle' },
-    props: {init: ''}
-  },
-  'Const': {
-    categories: ['core'],
-    output: {type: 'interface {}'},
-    style: { color: '#555' }
-    // , props: {value: ''}
-  },
-  'Notify': {
-    categories: ['flow-web'],
-    inputs: [{type: 'interface {}'}, {type: 'string', name: 'msg'}],
-    output: {type: 'interface {}'},
-    style: {color: '#665'}
-  },
-  'Log': {
-    categories: ['flow-web'],
-    output: {type: 'io.Writer'},
-    style: {color: '#665'}
-  }
-}
-
-/* {
-        // Fixed default stuff
-        'Test': {
-          group: 'Generic',
-          output: 'any',
-          style: {
-            shape: 'thing'
-          }
-        },
-
-        'MatMul': { group: 'Machine learning', inputs: [ '[]float32', '[]float32' ], output: '[]float32', style: { color: '#a44', textColor: 'white' } },
-        'Activator': { group: 'Machine learning', inputs: [ '[]float32' ], output: '[]float32', style: { color: '#a44', textColor: 'white', shape: 'circle' } },
-
-        'test': { group: 'Text', inputs: [ '[]float32', 'string' ], output: 'string', style: {'color': '#a93'} },
-        'reverse': { group: 'Text', inputs: [ 'string' ], output: 'string', style: {'color': '#a93'} },
-
-        'fetch': { group: 'json', output: 'json', style: {'color': '#99a'} },
-        'jsonExtract': { group: 'json', inputs: ['json'], output: 'string', style: {'color': '#99a'} },
-
-        'string': { group: 'Visualization', inputs: ['string'], style: {'color': '#9a9'} },
-        'lineGraph': { group: 'Visualization', inputs: ['[]float32', '[]float32'], style: {'color': '#9a9'} }
-      }, */

+ 542 - 0
browser/vue-flow/src/components/flow/editor.js

@@ -0,0 +1,542 @@
+import {mapGetters, mapActions} from 'vuex'
+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: {
+    '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: {}, // map of true false
+
+      stickySockets: false,
+      stickyTriggers: false,
+      nodeActivity: true
+    }
+  },
+  computed: {
+    ...mapGetters(['registry', 'activity', 'nodeData', 'nodeMap']),
+
+    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.NODE_REMOVE([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.nodeMap(link.from)
+        const nodeTo = this.nodeMap(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.nodeMap(trigger.from)
+        const nodeTo = this.nodeMap(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.$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: {
+    ...mapActions([
+      'DOCUMENT_SYNC',
+      'NODE_RAISE', 'NODE_UPDATE', 'NODE_ADD', 'NODE_REMOVE', 'NODE_INSPECT',
+      'LINK_ADD', 'LINK_REMOVE',
+      'TRIGGER_ADD', 'TRIGGER_REMOVE' ]),
+
+    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 }
+          console.log('Removing nodes:', this.nodeSelection)
+          this.NODE_REMOVE(this.nodeSelection)
+          // 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.nodeMap(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.nodeMap(link.from)
+          const nodeTo = this.nodeMap(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.LINK_ADD(link)
+        }})
+    },
+    triggerPointerDown (nodeId, ev, dir) {
+      if (ev.button !== 0) return
+
+      const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
+      const node = this.nodeMap(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.TRIGGER_ADD(trigger)
+        }
+      })
+    },
+
+    nodeInspect (tnode, force) {
+      this.$emit('nodeInspect', tnode, force)
+    },
+
+    // HERE
+    nodePointerDown (ev, i) {
+      document.activeElement && document.activeElement.blur()
+      const tnode = this.nodeData.nodes[i]
+      if (ev.button === 1) {
+        this.NODE_REMOVE([tnode])
+        // 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.NODE_INSPECT(tnode.id)
+      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
+
+      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
+      utils.createDrag({
+        drag: (ev) => {
+          if (this.nodeSelection === undefined) {
+            console.error('Well something went wrong')
+          }
+          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.nodeMap(k)
+            // create new nodes
+            nodeUpdate.push({
+              ...n,
+              x: n.x + dragP.x - curP.x,
+              y: n.y + dragP.y - curP.y
+            })
+          }
+          this.NODE_UPDATE(nodeUpdate)
+          // this.sendFlowEvent('nodeUpdate', this.nodeSelection)
+          curP = dragP
+        },
+        drop: (ev) => {
+          this.dragging = null
+          this.DOCUMENT_SYNC()
+        }
+      })
+    },
+    /// ////////////
+    // 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.NODE_ADD(newNode)
+    },
+    nodeProcess (node) {
+      // this.DOCUMENT_DYNC()
+      // console.log('Node process demand')
+      this.$emit('nodeProcess', node)
+    },
+    linkPointerClick (ev, link) {
+      ev.preventDefault()
+      this.LINK_REMOVE(link)
+    },
+
+    /* 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.DOCUMENT_DYNC()
+    }, */
+
+    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 in this.nodeData.nodes) {
+            const node = this.nodeData.nodes[n]
+            if (node.x > this.selector.x && node.x < (this.selector.x + this.selector.width) &&
+              node.y > this.selector.y && node.y < (this.selector.y + this.selector.height)
+            ) {
+              this.nodeSelection[node.id] = node
+            }
+          }
+          this.selector = null
+        }})
+    },
+
+    // service events
+    /* sendFlowEvent (type, param) {
+      this.$flowService[type](param)
+    }, */
+
+    //
+    // DOCUMENT_DYNC () {
+    // this.DOCUMENT_SYNC()
+    // 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))
+    }
+  }
+}

+ 179 - 0
browser/vue-flow/src/components/flow/editor.vue

@@ -0,0 +1,179 @@
+<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="TRIGGER_REMOVE(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 src="./editor.js">
+</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>

browser/vue-flow/src/components/flow/manager.vue → browser/vue-flow/src/components/flow/manager-bak.vue


+ 0 - 1
browser/vue-flow/src/components/flow/node-activity.vue

@@ -60,7 +60,6 @@ export default {
       let intervalms = this.finishTime - s
       if (intervalms < 0) {
         intervalms = 0
-        console.log('got time smallr')
       }
       const min = Math.floor(intervalms / 60000)
       const sec = (intervalms / 1000) % 60

+ 1 - 0
browser/vue-flow/src/components/flow/node.vue

@@ -285,6 +285,7 @@ export default {
         y: 0
       }
     },
+
     socketPointerDown (ev, socket) {
       this.$emit('socketPointerDown', ev, socket)
     },

+ 12 - 46
browser/vue-flow/src/components/main.vue

@@ -56,7 +56,6 @@
             <transition name="fade">
               <flow-funcs
                 v-show="panel=='palette'"
-                :registry="registry"
                 @toggleResizeable="funcsResizeable=!funcsResizeable"
                 @toggleStickySockets="managerStickySockets=!managerStickySockets"
               />
@@ -65,18 +64,12 @@
               <flow-inspector
                 ref="inspector"
                 v-show="panel=='inspector'"
-                :registry="registry"
-                :activity="activity"
-                :node-inspect="nodeActive"
-                @update="nodeInspectorChange"
                 @nodeProcess="nodeProcess($event)"
               />
             </transition>
           </div>
-          <flow-manager
+          <flow-editor
             ref="flowManager"
-            :activity="activity"
-            :registry="registry"
             @nodeInspect="nodeInspectStart(...arguments)"
             @nodeProcess="nodeProcess(...arguments)"
             @nodeDblClick="nodeInspectStart(...arguments,true)"
@@ -94,18 +87,15 @@
   </div>
 </template>
 <script>
-import Vue from 'vue'
+import {mapGetters, mapActions} from 'vuex'
 import AppChat from '@/components/chat'
-import FlowManager from '@/components/flow/manager'
-import FlowNode from '@/components/flow/node'
+import FlowEditor from '@/components/flow/editor'
 import FlowPanzoom from '@/components/flow/panzoom'
 import FlowFuncs from './panel-funcs'
 import FlowInspector from './panel-inspector'
 import HxSplit from '@/components/shared/hx-split'
 import HxModal from '@/components/shared/hx-modal'
 import HxNotify from '@/components/shared/hx-notify'
-import FlowService from '@/services/flowservice'
-import defRegistry from './defregistry'
 import 'reset-css/reset.css'
 
 import '@/assets/dark-theme.css'
@@ -114,8 +104,7 @@ import '@/assets/style.css'
 
 export default {
   components: {
-    FlowManager,
-    FlowNode,
+    FlowEditor,
     FlowPanzoom,
     FlowInspector,
     FlowFuncs,
@@ -126,14 +115,8 @@ export default {
   },
   data () {
     return {
-      registry: JSON.parse(JSON.stringify(defRegistry)),
-      activity: {},
-
       panel: 'palette',
 
-      nodeActive: null,
-      // nodeInspect: null,
-
       funcsSize: '250px',
       funcsResizeable: false,
 
@@ -147,12 +130,12 @@ export default {
       ctx,
       'conn'
     ]
-
     let targetws = 'ws://' + urlPath.join('/')
     if (window.location.protocol === 'https:') {
       targetws = 'wss://' + urlPath.join('/')
     }
-    Vue.use(FlowService, {location: targetws})
+    this.$flowService.connect(targetws)
+    // Vue.use(FlowService, {location: targetws})
   },
 
   mounted () {
@@ -162,25 +145,6 @@ export default {
         this.$router.push('/' + this.$route.params.context + '/' + v.id) // Swap to ID
       }
     })
-    this.$flowService.on('registry', (v) => {
-      let res = {}
-      for (let k of Object.keys(v.data)) {
-        const e = v.data[k]
-        res[k] = {
-          categories: e.categories,
-          inputs: e.inputs,
-          inputDesc: e.inputDesc,
-
-          output: e.output,
-          outputDesC: e.outputDesc,
-          style: e.extra && e.extra.style
-        }
-      }
-      this.registry = Object.assign({}, defRegistry, res)
-    })
-    this.$flowService.on('nodeActivity', (v) => {
-      this.activity = v.data || {}
-    })
     this.$flowService.on('sessionNotify', (v) => {
       this.$notify(v.data)
     })
@@ -193,18 +157,20 @@ export default {
       this.$notify('Connected')
       // Make this in a service
       if (this.$route.params.sessId === undefined) {
+        console.log('Creating new session')
         this.$flowService.sessionNew()
         return
       }
       this.$flowService.sessionLoad(undefined, this.$route.params.sessId)
     })
   },
+  computed: {
+    ...mapGetters(['registry', 'activity'])
+  },
   methods: {
-    nodeInspectorChange (node) {
-      this.$refs.flowManager.sendFlowEvent('nodeUpdate', [node])
-    },
+    ...mapActions(['NODE_INSPECT']),
     nodeInspectStart (node, changePane) { // node
-      this.nodeActive = node
+      this.NODE_INSPECT(node.id)
       if (changePane) {
         this.panel = 'inspector'
       }

+ 2 - 3
browser/vue-flow/src/components/panel-funcs.vue

@@ -48,21 +48,20 @@
   </div>
 </template>
 <script>
+import {mapGetters} from 'vuex'
 import HxCollapsible from '@/components/shared/hx-collapsible'
 import utils from '@/utils/utils'
 
 export default {
   name: 'FlowPanel',
   components: {HxCollapsible},
-  props: {
-    registry: {type: Object, default: () => {}}
-  },
   data () {
     return {
       search: null
     }
   },
   computed: {
+    ...mapGetters(['registry']),
     funcsGroups () {
       // Set
       let group = new Set()

+ 13 - 10
browser/vue-flow/src/components/panel-inspector.vue

@@ -107,6 +107,7 @@
   </div>
 </template>
 <script>
+import {mapGetters, mapActions} from 'vuex'
 import FlowPanzoom from '@/components/flow/panzoom'
 import FlowNode from '@/components/flow/node'
 import HxCollapsible from '@/components/shared/hx-collapsible'
@@ -114,24 +115,26 @@ import HxCollapsible from '@/components/shared/hx-collapsible'
 export default {
   name: 'FlowInspector',
   components: {FlowPanzoom, FlowNode, HxCollapsible},
-  props: {
-    nodeInspect: {type: Object, default: null},
-    activity: {type: Object, default: null},
-    registry: {type: Object, default: null}
-  },
   data () {
     return {
-      // nodeInspect: this.value
-      // JSON.parse(JSON.stringify(this.value))
+      nodeInspect: null
+    }
+  },
+  computed: {
+    ...mapGetters(['registry', 'activity'])
+  },
+  watch: {
+    '$store.state.nodeInspect' (node) {
+      this.nodeInspect = JSON.parse(JSON.stringify(node))
     }
   },
   methods: {
+    ...mapActions(['NODE_UPDATE', 'DOCUMENT_SYNC']),
     localChange () {
+      this.NODE_UPDATE([JSON.parse(JSON.stringify(this.nodeInspect))])
+      this.DOCUMENT_SYNC()
       // Seems that there might be browsers triggering the input before the v-model
       // so we defer the execution until we have nodeInspect updated
-      this.$nextTick(() => {
-        this.$emit('update', this.nodeInspect)
-      })
     }
   }
 }

+ 8 - 0
browser/vue-flow/src/main.js

@@ -1,10 +1,18 @@
 import Vue from 'vue'
 import App from './App.vue'
 
+import { sync } from 'vuex-router-sync'
 import router from './router'
+import store from '@/store'
+
+import FlowService from '@/services/flowservice-vue'
+
+Vue.use(FlowService)
+sync(store, router)
 
 window.app = new Vue({
   el: '#app',
   router,
+  store,
   render: h => h(App)
 })

+ 10 - 0
browser/vue-flow/src/services/flowservice-vue.js

@@ -0,0 +1,10 @@
+// FlowWSService
+import flowService from './flowservice'
+
+export default {
+
+  install (Vue, options) {
+    Vue.prototype.$flowService = flowService
+  }
+
+}

+ 80 - 82
browser/vue-flow/src/services/flowservice.js

@@ -1,94 +1,92 @@
-// FlowWSService
+import Vue from 'vue'
+const methods = [
+  'sessionNew', 'sessionLoad', // SESSION
+  'documentUpdate', 'documentRun', 'documentSave', // DOCUMENT
+  'chatEvent', 'chatJoin', 'chatRename', // CHAT
+  'linkAdd', 'linkUpdate', 'linkRemove', // LINK
+  'triggerAdd', 'triggerUpdate', 'triggerRemove',
+  'nodeUpdate', 'nodeAdd', 'nodeRemove', 'nodeProcess' // NODE
+]
 
 const debug = 0
+
+// FlowWSService
 let log = () => {}
 if (debug) {
-  log = console.log.bind(console.log, '%cSVC:', 'color:#0a0')
+  log = console.log.bind(console.log, '%cSVC:', 'color:#0a0', (Math.random() * 1000).toFixed())
 }
 
-export default {
-
-  install (Vue, options) {
-    let ws
-    let connected = false
-    let failed
-
-    const eventBus = new Vue()
-
-    let service = {
-      send (msg) { // Raw message
-        ws.send(JSON.stringify(msg))
-      },
-      // Calls callback if already connected or waits for connection
-      connected (cb) {
-        if (connected === false) {
-          eventBus.$on('open', cb)
-          return
-        }
-        cb()
-      },
-      // event related
-      on: eventBus.$on.bind(eventBus),
-      once: eventBus.$once.bind(eventBus),
-      off: eventBus.$off.bind(eventBus)
-    };
+// 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
+    }
 
-    /// ///////////////
-    // Service entry points map
-    //
-    [
-      'sessionNew', 'sessionLoad', // SESSION
-      'documentUpdate', 'documentRun', 'documentSave', // DOCUMENT
-      'chatEvent', 'chatJoin', 'chatRename', // CHAT
-      'linkAdd', 'linkUpdate', 'linkRemove', // LINK
-      'triggerAdd', 'triggerUpdate', 'triggerRemove',
-      'nodeUpdate', 'nodeAdd', 'nodeRemove', 'nodeProcess' // NODE
-    ].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})
-        })
-      }
-    })
+    setTimeout(() => connect(loc), 3000) // Reconnect
+  }
+}
 
-    // WS Connector
-    const connect = (loc) => {
-      ws = new window.WebSocket(loc)
-      ws.onopen = () => {
-        log('connected')
-        connected = true
-        eventBus.$emit('open')
-        if (failed) {
-          log('Persisting failed document (retry)')
-          service.documentUpdate(failed.DocumentUpdate, failed.sessId)
-        }
-      }
-      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
-        setTimeout(() => connect(loc), 3000) // Reconnect
-      }
+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)
+  }
+}
 
-    connect(options.location)
-
-    Vue.prototype.$flowService = service
+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})
+    })
   }
+})
+this.id = Math.random()
 
-}
+export default service

+ 4 - 0
browser/vue-flow/yarn.lock

@@ -5757,6 +5757,10 @@ vue@^2.5.11:
   version "2.5.13"
   resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.13.tgz#95bd31e20efcf7a7f39239c9aa6787ce8cf578e1"
 
+vuex@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
+
 watchpack@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"

+ 16 - 0
node_modules/.yarn-integrity

@@ -0,0 +1,16 @@
+{
+  "nodeVersion": "v9.4.0",
+  "modulesFolders": [
+    "node_modules"
+  ],
+  "flags": [],
+  "linkedModules": [],
+  "topLevelPatterns": [
+    "vuex-router-sync@^5.0.0"
+  ],
+  "lockfileEntries": {
+    "vuex-router-sync@^5.0.0": "https://registry.yarnpkg.com/vuex-router-sync/-/vuex-router-sync-5.0.0.tgz#1a225c17a1dd9e2f74af0a1b2c62072e9492b305"
+  },
+  "files": [],
+  "artifacts": {}
+}

+ 21 - 0
node_modules/vuex-router-sync/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Evan You
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 51 - 0
node_modules/vuex-router-sync/README.md

@@ -0,0 +1,51 @@
+# vuex-router-sync [![CircleCI](https://circleci.com/gh/vuejs/vuex-router-sync.svg?style=svg)](https://circleci.com/gh/vuejs/vuex-router-sync)
+
+> Sync vue-router's current $route as part of vuex store's state.
+
+### Usage
+
+``` bash
+# the latest version works only with vue-router >= 2.0
+npm install vuex-router-sync
+
+# for usage with vue-router < 2.0:
+npm install vuex-router-sync@2
+```
+
+``` js
+import { sync } from 'vuex-router-sync'
+import store from './vuex/store' // vuex store instance
+import router from './router' // vue-router instance
+
+const unsync = sync(store, router) // done. Returns an unsync callback fn
+
+// bootstrap your app...
+
+// During app/Vue teardown (e.g., you only use Vue.js in a portion of your app and you 
+// navigate away from that portion and want to release/destroy Vue components/resources)
+unsync() // Unsyncs store from router
+```
+
+You can optionally set a custom vuex module name:
+
+```js
+sync(store, router, { moduleName: 'RouteModule' } )
+```
+
+### How does it work?
+
+- It adds a `route` module into the store, which contains the state representing the current route:
+
+  ``` js
+  store.state.route.path   // current path (string)
+  store.state.route.params // current params (object)
+  store.state.route.query  // current query (object)
+  ```
+
+- When the router navigates to a new route, the store's state is updated.
+
+- **`store.state.route` is immutable, because it is derived state from the URL, which is the source of truth**. You should not attempt to trigger navigations by mutating the route object. Instead, just call `$router.push()` or `$router.go()`. Note that you can do `$router.push({ query: {...}})` to update the query string on the current path.
+
+### License
+
+[MIT](http://opensource.org/licenses/MIT)

+ 75 - 0
node_modules/vuex-router-sync/index.js

@@ -0,0 +1,75 @@
+exports.sync = function (store, router, options) {
+  var moduleName = (options || {}).moduleName || 'route'
+
+  store.registerModule(moduleName, {
+    namespaced: true,
+    state: cloneRoute(router.currentRoute),
+    mutations: {
+      'ROUTE_CHANGED': function ROUTE_CHANGED (state, transition) {
+        store.state[moduleName] = cloneRoute(transition.to, transition.from)
+      }
+    }
+  })
+
+  var isTimeTraveling = false
+  var currentPath
+
+  // sync router on store change
+  var storeUnwatch = store.watch(
+    function (state) { return state[moduleName]; },
+    function (route) {
+      var fullPath = route.fullPath;
+      if (fullPath === currentPath) {
+        return
+      }
+      if (currentPath != null) {
+        isTimeTraveling = true
+        router.push(route)
+      }
+      currentPath = fullPath
+    },
+    { sync: true }
+  )
+
+  // sync store on router navigation
+  var afterEachUnHook = router.afterEach(function (to, from) {
+    if (isTimeTraveling) {
+      isTimeTraveling = false
+      return
+    }
+    currentPath = to.fullPath
+    store.commit(moduleName + '/ROUTE_CHANGED', { to: to, from: from })
+  })
+
+  return function unsync () {
+    // On unsync, remove router hook
+    if (afterEachUnHook != null) {
+      afterEachUnHook()
+    }
+
+    // On unsync, remove store watch
+    if (storeUnwatch != null) {
+      storeUnwatch()
+    }
+
+    // On unsync, unregister Module with store
+    store.unregisterModule(moduleName)
+  }
+}
+
+function cloneRoute (to, from) {
+  var clone = {
+    name: to.name,
+    path: to.path,
+    hash: to.hash,
+    query: to.query,
+    params: to.params,
+    fullPath: to.fullPath,
+    meta: to.meta
+  }
+  if (from) {
+    clone.from = cloneRoute(from)
+  }
+  return Object.freeze(clone)
+}
+

+ 45 - 0
node_modules/vuex-router-sync/package.json

@@ -0,0 +1,45 @@
+{
+  "name": "vuex-router-sync",
+  "version": "5.0.0",
+  "description": "Effortlessly keep vue-router and vuex store in sync.",
+  "main": "index.js",
+  "files": [
+    "index.js",
+    "types/*.d.ts"
+  ],
+  "typings": "types/index.d.ts",
+  "scripts": {
+    "test": "jest && npm run test:types",
+    "test:types": "tsc -p types/test",
+    "build": "buble src/index.js > index.js",
+    "pretest": "npm run build",
+    "prepublishOnly": "npm run build"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vuejs/vuex-router-sync.git"
+  },
+  "keywords": [
+    "vuex",
+    "vue-router",
+    "vue"
+  ],
+  "author": "Evan You",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/vuejs/vuex-router-sync/issues"
+  },
+  "homepage": "https://github.com/vuejs/vuex-router-sync#readme",
+  "devDependencies": {
+    "buble": "^0.16.0",
+    "jest": "^21.2.1",
+    "typescript": "^2.5.3",
+    "vue": "github:vuejs/vue#dev",
+    "vue-router": "^3.0.0",
+    "vuex": "^3.0.0"
+  },
+  "peerDependencies": {
+    "vue-router": "^3.0.0",
+    "vuex": "^3.0.0"
+  }
+}

+ 12 - 0
node_modules/vuex-router-sync/types/index.d.ts

@@ -0,0 +1,12 @@
+import { Store } from 'vuex';
+import VueRouter from 'vue-router';
+
+interface SyncOptions {
+  moduleName: string;
+}
+
+export declare function sync(
+  store: Store<any>,
+  router: VueRouter,
+  options?: SyncOptions
+): Function;

+ 5 - 0
package.json

@@ -0,0 +1,5 @@
+{
+  "devDependencies": {
+    "vuex-router-sync": "^5.0.0"
+  }
+}

+ 7 - 0
yarn.lock

@@ -0,0 +1,7 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+vuex-router-sync@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/vuex-router-sync/-/vuex-router-sync-5.0.0.tgz#1a225c17a1dd9e2f74af0a1b2c62072e9492b305"