Kaynağa Gözat

new event view

lianghuang 4 yıl önce
işleme
5c1e774ecb

+ 37 - 0
coms/EventJenga.jsx

@@ -0,0 +1,37 @@
+import React from 'react'
+import JengaDrag from './jenga/panel/JengaDrag'
+import JengaNav from './jenga/nav/JengaNav'
+import JengaPanel from './jenga/panel/JengaPanel'
+// import JengaAction from './jenga/panel/JengaAction'
+// import JengaServiceDebug from '../../common/serviceDebug/JengaServiceDebug'
+// import ServiceLogView from '../../common/serviceLogView/ServiceLogView'
+import { shallowEqualImmutable } from 'utils/utils'
+
+export default class EventJenga extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  render() {
+    return (
+      <div className="event-jenga-panel">
+        <JengaDrag />
+        <JengaNav curEventNodeList={this.props.curEventNodeList} />
+        <JengaPanel
+          curEventNodeList={this.props.curEventNodeList}
+          filterBlockIdsMap={this.props.filterBlockIdsMap}
+        />
+        {/* <JengaServiceDebug />
+        <ServiceLogView />
+        <JengaAction /> */}
+      </div>
+    )
+  }
+}

+ 149 - 0
coms/jenga/block/BlockEvent.jsx

@@ -0,0 +1,149 @@
+import React from 'react'
+import LazyLoad from 'react-lazyload'
+import cls from 'classnames'
+import Block from './base/Block'
+import BlockChildren from './base/BlockChildren'
+import BlockOrder from './base/BlockOrder'
+import BlockDel from './base/BlockDel'
+import BlockCite from './base/BlockCite'
+import BlockMemo from './base/BlockMemo'
+import BlockEventTrigger from './base/event/BlockEventTrigger'
+import BlockEventCons from './base/event/BlockEventCons'
+
+export default class BlockEvent extends Block {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    let jengaContent = document.getElementsByClassName('jenga-content')[0]
+
+    let getRelChildComs = () => {
+      return this.state.hasChildren ? (
+        <React.Fragment>
+          {this.state.isExpand ? (
+            <span
+              className={cls(
+                'index-line',
+                'index-main-line',
+                'index-line-' + this.getNextLineColor()
+              )}
+            />
+          ) : null}
+          <button
+            className={cls(
+              'btn-clear btn-toggle-sub btn-expand',
+              'btn-toggle-sub-' +
+                (this.state.isExpand ? 'expand-' : 'collapse-') +
+                this.getLineColor()
+            )}
+            onClick={this.onToggleBlockExpand}
+          />
+        </React.Fragment>
+      ) : (
+        <div
+          className={cls(
+            'btn-clear btn-toggle-sub btn-toggle-sub-empty',
+            'btn-toggle-sub-empty-' + this.getLineColor()
+          )}
+        />
+      )
+    }
+
+    return (
+      <LazyLoad height={26} overflow={true} scrollContainer={jengaContent}>
+        <div className="block-wrap block-wrap-event f--h">
+          <span
+            className={cls(
+              'index-line index-line-event',
+              'index-line-' + this.getLineColor(),
+              {
+                'index-last-line': this.props.isLast
+              }
+            )}
+            style={this.indexLeftStyle()}
+          />
+          <div
+            className={cls('flex-1', {
+              'flex-wrap-disabled': !this.state.isEnable
+            })}
+          >
+            <div
+              className={cls('block-item block-event f--h', {
+                'block-active': this.state.isActive
+              })}
+              onDragOver={this.onDragOver}
+              onDrop={this.onDrop}
+              onClick={this.onSelectBlock}
+              onContextMenu={this.onContextMenu}
+            >
+              <BlockDel
+                isActive={this.state.isActive}
+                layer={this.props.layer}
+                bid={this.props.bid}
+              />
+              <BlockOrder
+                layer={this.props.layer}
+                bid={this.props.bid}
+                eventNodeId={this.props.eventNodeId}
+                extraGapGroupLayer={this.extraGapGroupLayer()}
+              />
+              <div
+                className="block-gap"
+                style={{
+                  left: -this.getGapLeft() + 'px',
+                  padding: '0 ' + this.getGapLeft() / 2 + 'px'
+                }}
+              />
+              {getRelChildComs()}
+              <div
+                id={'block-main-' + this.props.bid}
+                className="block-main flex-1 f--h"
+                style={this.blockMainStyle()}
+                draggable={true}
+                onDragStart={this.onDragStart}
+                onDragEnd={this.onDragEnd}
+              >
+                <div
+                  id={'over-cover-' + this.props.bid}
+                  className="over-cover"
+                />
+                <div className="event-trigger-wrap">
+                  <BlockEventTrigger
+                    bid={this.props.bid}
+                    isEnable={this.state.isEnable}
+                    eventNodeId={this.props.eventNodeId}
+                  />
+                </div>
+                <BlockEventCons bid={this.props.bid} />
+              </div>
+              <BlockCite
+                bid={this.props.bid}
+                eventNodeId={this.props.eventNodeId}
+              />
+              <BlockMemo
+                className="block-event-memo"
+                active={this.state.isActive}
+                bid={this.props.bid}
+                eventNodeId={this.props.eventNodeId}
+              />
+            </div>
+            {this.state.isExpand ? (
+              <BlockChildren
+                loops={this.getLoops()}
+                layer={this.props.layer + 1}
+                groupLayer={this.props.groupLayer}
+                nestGroupLayer={0}
+                offspringInNestGroupLayer={this.props.nestGroupLayer}
+                extraGapGroupLayer={this.extraGapGroupLayer()}
+                parentBid={this.props.bid}
+                eventNodeId={this.props.eventNodeId}
+                visibleBlockIds={this.props.visibleBlockIds}
+              />
+            ) : null}
+          </div>
+        </div>
+      </LazyLoad>
+    )
+  }
+}

+ 586 - 0
coms/jenga/block/base/Block.jsx

@@ -0,0 +1,586 @@
+import React from 'react'
+import { shallowEqualImmutable, getY, isMac } from 'utils/utils'
+import uiActions from 'actions/ui'
+import newEventActions from 'actions/newEvent'
+import newEventStores from 'stores/newEvent'
+import {
+  getEventBlockByBid,
+  filterBlocks,
+  getBidList
+} from 'stores/funcs/newEvent'
+import { dragPosition, dragUtils } from 'stores/funcs/event/dragUtils'
+import { CONTEXT_MENU_TYPE, EVENT_BLOCK_TYPE } from 'const/const'
+
+export default class Block extends React.Component {
+  constructor(props) {
+    super(props)
+    this.curBlockId = newEventStores.curBlockId
+    this.multiBlockIds = newEventStores.multiBlockIds
+    this.state = {
+      isActive: this.isActive(props.bid),
+      isEnable: this.isEnable(props.bid),
+      isExpand: this.isExpand(props.bid),
+      hasChildren: this.hasChildren(props.bid)
+    }
+    this.onEventChange = this.onEventChange.bind(this)
+
+    this.isActive = this.isActive.bind(this)
+    this.isEnable = this.isEnable.bind(this)
+    this.isExpand = this.isExpand.bind(this)
+
+    this.onSelectBlock = this.onSelectBlock.bind(this)
+    this.onToggleBlockExpand = this.onToggleBlockExpand.bind(this)
+
+    this.getLineColor = this.getLineColor.bind(this)
+    this.getGapLeft = this.getGapLeft.bind(this)
+
+    // 拖拽
+    this.allowDrag = this.allowDrag.bind(this)
+    this.allowDrop = this.allowDrop.bind(this)
+    this.getPosition = this.getPosition.bind(this)
+    this.onDragStart = this.onDragStart.bind(this)
+    this.onDragEnd = this.onDragEnd.bind(this)
+    this.onDragOver = this.onDragOver.bind(this)
+    this.onDrop = this.onDrop.bind(this)
+
+    // 右键
+    this.onContextMenu = this.onContextMenu.bind(this)
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  componentDidUpdate(preProps) {
+    let obj = {}
+    // bid改变时需要更新
+    if (preProps.bid !== this.props.bid) {
+      obj.isActive = this.isActive()
+      obj.isEnable = this.isEnable()
+      obj.isExpand = this.isExpand()
+      obj.hasChildren = this.hasChildren()
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status.types.includes('curBlockId')) {
+      if (this.curBlockId !== status.data.curBlockId) {
+        this.curBlockId = status.data.curBlockId
+        let isActive = this.isActive()
+        if (this.state.isActive !== isActive) {
+          obj.isActive = isActive
+        }
+      }
+    }
+    if (status.types.includes('multiBlockIds')) {
+      this.multiBlockIds = status.data.multiBlockIds
+      let isActive = this.isActive()
+      if (this.state.isActive !== isActive) {
+        obj.isActive = isActive
+      }
+    }
+    if (status.types.includes('toggleBlockProp')) {
+      if (this.props.bid === status.data.affectBlockId) {
+        if (status.data.prop === 'enable') {
+          let isEnable = this.isEnable()
+          if (this.state.isEnable !== isEnable) {
+            obj.isEnable = isEnable
+          }
+        } else if (status.data.prop === 'expand') {
+          let isExpand = this.isExpand()
+          if (this.state.isExpand !== isExpand) {
+            obj.isExpand = isExpand
+          }
+        }
+      }
+    }
+    if (
+      (status.types.includes('changeBlockChildren') &&
+        status.data &&
+        status.data.parentBid === this.props.bid) ||
+      (status.types.includes('reloadBlock') &&
+        this.props.bid === status.data.affectBlockId)
+    ) {
+      let hasChildren = this.hasChildren()
+      if (hasChildren !== this.state.hasChildren) {
+        obj.hasChildren = hasChildren
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  isActive(bid) {
+    let _bid = bid || this.props.bid
+    return this.curBlockId === _bid || this.multiBlockIds.indexOf(_bid) >= 0
+  }
+
+  isEnable(bid) {
+    let _bid = bid || this.props.bid
+    let result = true
+    let block = getEventBlockByBid(_bid)
+    if (block) {
+      result = block.enable !== false
+    }
+    return result
+  }
+
+  isExpand(bid) {
+    let _bid = bid || this.props.bid
+    let result = true
+    let block = getEventBlockByBid(_bid)
+    if (block) {
+      result = block.expand !== false
+    }
+    return result
+  }
+
+  hasChildren(bid) {
+    let _bid = bid || this.props.bid
+    let result = false
+    let block = getEventBlockByBid(_bid)
+    if (block) {
+      result = block.children && block.children.length > 0
+    }
+    return result
+  }
+
+  onSelectBlock(e, allowMulti = false) {
+    if (
+      this.props.eventNodeId &&
+      this.props.eventNodeId !== newEventStores.curEventNodeId
+    ) {
+      newEventActions.selectEvent({ eventNodeId: this.props.eventNodeId })
+    } else {
+      // 多选
+      let isCtrl = e && (isMac ? e.metaKey : e.ctrlKey)
+      if (isCtrl) {
+        newEventActions.selectMultiBlocks({ blockId: this.props.bid })
+        return
+      }
+      let isShift = e && e.shiftKey
+      if (isShift) {
+        newEventActions.shiftSelectBlocks({ blockId: this.props.bid })
+        return
+      }
+    }
+    if (
+      !this.state.isActive ||
+      (
+        this.multiBlockIds.length > 0 &&
+        (
+          !allowMulti ||
+          (
+            allowMulti &&
+            !this.multiBlockIds?.includes(this.props.bid)
+          )
+        )
+      )
+    ) {
+      newEventActions.selectBlock({ blockId: this.props.bid })
+    }
+  }
+
+  onToggleBlockExpand() {
+    newEventActions.toggleBlockProp({ blockId: this.props.bid, prop: 'expand' })
+  }
+
+  getLineColor() {
+    let group = ['red', 'green', 'purple', 'ching', 'orange', 'blue']
+    return group[this.props.layer % 6]
+  }
+
+  getNextLineColor() {
+    let group = ['red', 'green', 'purple', 'ching', 'orange', 'blue']
+    return group[(this.props.layer + 1) % 6]
+  }
+
+  getGapLeft() {
+    return 8 + this.props.layer * 20 + (this.props.layer + 1)
+  }
+
+  getGroupColor() {
+    const group = ['color1', 'color2', 'color3', 'color4', 'color5']
+    // const group = ['#7F438F', '#E5C88E', '#EEB2B2', '#B5D8F1', '#CBFF97']
+    return group[(this.props.groupLayer) % 5]
+  }
+
+  blockMainStyle() {
+    if (this.props.groupLayer >= 1) {
+      return {
+        paddingLeft: `${(this.props.nestGroupLayer - 1) * 2}px`,
+        marginRight: `${(this.props.groupLayer) * 2}px`
+      }
+    }
+    return null
+  }
+
+  getLoops(bid) {
+    let _bid = bid || this.props.bid
+    let block = getEventBlockByBid(_bid)
+    if (block && block.type === EVENT_BLOCK_TYPE.LOOP) {
+      if (this.props.loops) {
+        return JSON.stringify(
+          [].concat(JSON.parse(this.props.loops), [block.bid])
+        )
+      } else {
+        return JSON.stringify([block.bid])
+      }
+    } else {
+      return this.props.loops
+    }
+  }
+
+  indexLeftStyle() {
+    // let _bid = this.props.bid
+    // let block = getEventBlockByBid(_bid)
+    // console.log(block?.type, this.props.offspringInNestGroupLayer, this.props.nestGroupLayer)
+    if (this.props.offspringInNestGroupLayer >= 2 && this.props.nestGroupLayer === 0) {
+      return { marginLeft: 20 + (this.props.offspringInNestGroupLayer - 1) * 2 + 'px' }
+    }
+    return undefined
+  }
+
+  extraGapGroupLayer() {
+    if (this.props.offspringInNestGroupLayer >= 2 && this.props.nestGroupLayer === 0) {
+      return this.props.offspringInNestGroupLayer
+    } else {
+      return this.props.extraGapGroupLayer || 0
+    }
+  }
+
+  //拖拽
+  allowDrag() {
+    let allow = true
+    if (
+      document.activeElement.tagName === 'INPUT' ||
+      document.activeElement.tagName === 'TEXTAREA'
+    ) {
+      allow = false // block dragging
+    }
+    if (!this.isActive(this.props.bid)) {
+      this.onSelectBlock()
+    }
+    return allow
+  }
+
+  allowDrop(bid, position, multiInfo) {
+    let result = false
+    let dragBlock = getEventBlockByBid(bid)
+    let targetBlock = getEventBlockByBid(this.props.bid)
+    let dropBlock = undefined
+    switch (position) {
+      case dragPosition.mid:
+        // 加到targetBlock里面
+        dropBlock = targetBlock
+        break
+      case dragPosition.bot:
+      case dragPosition.top:
+        // 实际是放在targetBlock的父级
+        if (targetBlock.parentBid) {
+          let targetParentBlock = getEventBlockByBid(targetBlock.parentBid)
+          if (targetParentBlock) {
+            dropBlock = targetParentBlock
+          } else {
+            dropBlock = undefined
+          }
+        }
+        break
+    }
+    if (multiInfo && multiInfo.length > 0) {
+      // 多个移动
+      let blockTypes = []
+      multiInfo = filterBlocks(multiInfo)
+      multiInfo.forEach(bid => {
+        let block = getEventBlockByBid(bid)
+        if (block && block.type) {
+          let type = block.type
+          if (blockTypes.indexOf(type) === -1) {
+            blockTypes.push(type)
+          }
+        }
+      })
+      if (
+        targetBlock &&
+        targetBlock.type === EVENT_BLOCK_TYPE.ROOT &&
+        blockTypes.length === 1 &&
+        blockTypes[0] === EVENT_BLOCK_TYPE.ROOT &&
+        position !== dragPosition.mid
+      ) {
+        // 拖的和drop的都是Root只可放上和下
+        result = true
+      } else if (dropBlock) {
+        // 提炼出可drop的情况
+        if (dropBlock.type === EVENT_BLOCK_TYPE.ACTION) {
+          if (
+            blockTypes.length === 1 &&
+            blockTypes[0] === EVENT_BLOCK_TYPE.STATUS &&
+            dropBlock.action &&
+            dropBlock.action.callback === true
+          ) {
+            result = true
+          }
+        } else if (dropBlock.type === EVENT_BLOCK_TYPE.COMMENT) {
+          result = false
+        } else {
+          if (
+            blockTypes.indexOf(EVENT_BLOCK_TYPE.ROOT) === -1 &&
+            blockTypes.indexOf(EVENT_BLOCK_TYPE.STATUS) === -1
+          ) {
+            result = true
+          }
+        }
+      }
+    } else {
+      // 单个移动
+      if (
+        dragBlock &&
+        targetBlock &&
+        dragBlock.type === EVENT_BLOCK_TYPE.ROOT &&
+        targetBlock.type === EVENT_BLOCK_TYPE.ROOT &&
+        position !== dragPosition.mid
+      ) {
+        // 拖的和drop的都是Root只可放上和下
+        result = true
+      } else if (dragBlock && dropBlock) {
+        // 提炼出可drop的情况
+        if (dropBlock.type === EVENT_BLOCK_TYPE.ACTION) {
+          if (
+            dragBlock.type === EVENT_BLOCK_TYPE.STATUS &&
+            dropBlock.action &&
+            dropBlock.action.callback === true
+          ) {
+            result = true
+          }
+        } else if (dropBlock.type === EVENT_BLOCK_TYPE.COMMENT) {
+          result = false
+        } else {
+          if (
+            dragBlock.type !== EVENT_BLOCK_TYPE.ROOT &&
+            dragBlock.type !== EVENT_BLOCK_TYPE.STATUS
+          ) {
+            result = true
+          }
+        }
+      }
+    }
+    return result
+  }
+
+  getPosition(dragBlockId, deltaTop, maxHeight, multiInfo) {
+    let position = dragPosition.mid
+    let mid1 = maxHeight / 3
+    let mid2 = (maxHeight * 2) / 3
+    if (deltaTop >= 0 && deltaTop <= mid1) {
+      position = dragPosition.top
+    } else if (deltaTop >= mid2 && deltaTop <= maxHeight) {
+      position = dragPosition.bot
+    }
+    let dropBlock = getEventBlockByBid(this.props.bid)
+    if (multiInfo && multiInfo.length > 0) {
+      // 多个移动
+      let blockTypes = []
+      multiInfo = filterBlocks(multiInfo)
+      multiInfo.forEach(bid => {
+        let block = getEventBlockByBid(bid)
+        if (block && block.type) {
+          blockTypes.push(block.type)
+        }
+      })
+      if (
+        dropBlock &&
+        dropBlock.type === EVENT_BLOCK_TYPE.ROOT &&
+        blockTypes.indexOf(EVENT_BLOCK_TYPE.ROOT) === -1
+      ) {
+        // drop为root且drap均非root的情况
+        position = dragPosition.mid
+      }
+    } else {
+      // 单个移动
+      let dragBlock = getEventBlockByBid(dragBlockId)
+      if (
+        dropBlock &&
+        dropBlock.type === EVENT_BLOCK_TYPE.ROOT &&
+        !(dragBlock && dragBlock.type === EVENT_BLOCK_TYPE.ROOT)
+      ) {
+        // 不同为root的情况且drop的root的情况
+        position = dragPosition.mid
+      }
+    }
+    return position
+  }
+
+  onDragStart(e) {
+    dragUtils.isDragging = true
+    e.stopPropagation()
+    if (!this.allowDrag()) {
+      e.preventDefault()
+      return
+    }
+    e.dataTransfer.effectAllowed = 'move'
+    dragUtils.setDraggingItemInfo(this.props.bid, this.state.isExpand)
+    dragUtils.setDraggingMultiItemsInfo(this.multiBlockIds)
+    // 对block的处理
+    if (this.multiBlockIds.length === 0) {
+      if (this.state.isExpand && this.state.hasChildren) {
+        // 关闭起来
+        newEventActions.toggleBlockProp({
+          blockId: this.props.bid,
+          prop: 'expand',
+          value: false,
+          addRecord: false
+        })
+      }
+    }
+  }
+
+  onDragEnd(e) {
+    // drop先执行
+    e.stopPropagation()
+    if (dragUtils.isDragging) {
+      dragUtils.isDragging = false
+      let dragInfo = dragUtils.getDraggingItemInfo()
+      // 对block的处理
+      if (dragInfo) {
+        newEventActions.toggleBlockProp({
+          blockId: dragInfo.bid,
+          prop: 'expand',
+          value: dragInfo.expand,
+          addRecord: false
+        })
+      }
+      dragUtils.resetDraggingItem()
+    }
+  }
+
+  onDragOver(e) {
+    e.stopPropagation()
+    if (!dragUtils.isDragging) {
+      return
+    }
+    e.preventDefault()
+    let allowDrop = true
+    let deltaTop =
+      e.clientY -
+      getY(e.currentTarget) +
+      document.querySelector('.jenga-panel').scrollTop
+    let maxHeight = e.currentTarget.clientHeight
+    let position = dragPosition.mid
+    let info = dragUtils.getDraggingItemInfo()
+    let multiInfo = dragUtils.getDraggingMultiItemsInfo()
+    if (multiInfo && multiInfo.length > 0) {
+      // 多个移动
+      // console.log('多个移动。。。')
+      multiInfo = filterBlocks(multiInfo)
+      let bidList = []
+      multiInfo.forEach(bid => {
+        getBidList({ blockId: bid, list: bidList })
+      })
+      if (bidList.indexOf(this.props.bid) > -1) {
+        // dragOver为自己或childern时不允许
+        allowDrop = false
+        // console.log('包含自己。。。')
+      } else {
+        let blockTypes = []
+        multiInfo.forEach(bid => {
+          let block = getEventBlockByBid(bid)
+          if (block && block.type) {
+            let type = block.type
+            if (blockTypes.indexOf(type) === -1) {
+              blockTypes.push(type)
+            }
+          }
+        })
+        if (
+          multiInfo.length > 1 &&
+          blockTypes.indexOf(EVENT_BLOCK_TYPE.ROOT) > -1 &&
+          blockTypes.length > 1
+        ) {
+          // 多个待移动block中同时有root和非root时不允许
+          allowDrop = false
+          // console.log('同时有root和非root。。。')
+        } else {
+          // 对drag的处理
+          // console.log('ok。。。')
+          position = this.getPosition(info.bid, deltaTop, maxHeight, multiInfo)
+          allowDrop = this.allowDrop(info.bid, position, multiInfo)
+        }
+      }
+    } else {
+      // 单个移动
+      // console.log('单个移动。。。')
+      // 相同bid不允许
+      if (this.props.bid !== info.bid) {
+        // 对drag的处理
+        position = this.getPosition(info.bid, deltaTop, maxHeight, multiInfo)
+        allowDrop = this.allowDrop(info.bid, position, multiInfo)
+      } else {
+        allowDrop = false
+      }
+    }
+    dragUtils.setOverItemInfo(position, this.props.bid, allowDrop)
+  }
+
+  onDrop(e) {
+    e.stopPropagation()
+    e.preventDefault()
+    dragUtils.isDragging = false
+    let dragInfo = dragUtils.getDraggingItemInfo()
+    let multiInfo = dragUtils.getDraggingMultiItemsInfo()
+    multiInfo = filterBlocks(multiInfo)
+    let overInfo = dragUtils.getOverItemInfo()
+    // 对block的处理
+    if (multiInfo.length === 0) {
+      if (dragInfo) {
+        newEventActions.toggleBlockProp({
+          blockId: dragInfo.bid,
+          prop: 'expand',
+          value: dragInfo.expand,
+          addRecord: false
+        })
+      }
+    }
+    // 移动block
+    if (overInfo && overInfo.isAllow) {
+      newEventActions.moveBlock({
+        srcBlockId: dragInfo.bid,
+        targetBlockId: overInfo.bid,
+        position: overInfo.overPosition,
+        multiSrcBlockIds: multiInfo
+      })
+    }
+    dragUtils.resetDraggingItem()
+  }
+
+  onContextMenu(e) {
+    e.preventDefault()
+    e.stopPropagation()
+    this.onSelectBlock(e, true)
+    let info = {
+      x: e.pageX,
+      y: e.pageY,
+      type: CONTEXT_MENU_TYPE.EVENT
+    }
+    uiActions.contextMenu(info)
+  }
+
+  render() {
+    return null
+  }
+}

+ 29 - 0
coms/jenga/nav/JengaNav.jsx

@@ -0,0 +1,29 @@
+import React from 'react'
+import JengaNavInfo from './JengaNavInfo'
+import JengaTypeBlockOperator from './JengaTypeBlockOperator'
+import JengaNavMemo from './JengaNavMemo'
+
+export default class JengaNav extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.showOperator = this.showOperator.bind(this)
+  }
+
+  showOperator() {
+    if (this.props.curEventNodeList) {
+      return false
+    }
+    return true
+  }
+
+  render() {
+    return (
+      <div className="jenga-nav f--hlc">
+        <JengaNavInfo curEventNodeList={this.props.curEventNodeList} />
+        {this.showOperator() ? <JengaTypeBlockOperator /> : null}
+        <JengaNavMemo />
+      </div>
+    )
+  }
+}

+ 143 - 0
coms/jenga/nav/JengaNavInfo.jsx

@@ -0,0 +1,143 @@
+import React from 'react'
+import cls from 'classnames'
+import i18n from 'i18next'
+import newEventStores from 'stores/newEvent'
+import treeStores from 'stores/tree'
+import {
+  nodeIsDyTransaction,
+  nodeIsFunctionGroup,
+  nodeIsIotMessage,
+  nodeIsNormalService,
+  nodeIsTimerService,
+  nodeIsTransaction
+} from 'stores/funcs/nodeType'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import JengaDebugBtn from './debug/JengaDebugBtn'
+import LogBtn from 'src/components/common/serviceLogView/LogBtn'
+import JengaNavNavigateNodes from './JengaNavNavigateNodes'
+
+export default class JengaNavInfo extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      curEventNodeId: newEventStores.curEventNodeId,
+      type: this.getType(newEventStores.curEventNodeId),
+      curClassId: treeStores.curClassId
+    }
+
+    this.onEventChange = this.onEventChange.bind(this)
+    this.onTreeChange = this.onTreeChange.bind(this)
+    this.getType = this.getType.bind(this)
+    this.getInfo = this.getInfo.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+    this.unsubscribeTree = treeStores.listen(this.onTreeChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+    this.unsubscribeTree()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('curEventNodeId')) {
+        if (this.state.curEventNodeId !== status.data.curEventNodeId) {
+          obj.curEventNodeId = status.data.curEventNodeId
+          obj.type = this.getType(obj.curEventNodeId)
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  onTreeChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('curClassId')) {
+        if (this.state.curClassId !== status.data.curClassId) {
+          obj.curClassId = status.data.curClassId
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  getType(nodeId) {
+    let type = null
+    let node = getEventNodeById(nodeId)
+    if (node) {
+      if (nodeIsTimerService(node.type)) {
+        type = 'timer'
+      } else if (nodeIsTransaction(node.type)) {
+        type = 'transaction'
+      } else if (nodeIsDyTransaction(node.type)) {
+        type = 'dyTransaction'
+      } else if (nodeIsIotMessage(node.type)) {
+        type = 'iotMessage'
+      } else if (nodeIsNormalService(node.type)) {
+        type = 'service'
+      } else if (nodeIsFunctionGroup(node.type)) {
+        type = 'funcGroup'
+      }
+    }
+    return type
+  }
+
+  getInfo() {
+    let { curClassId } = this.state
+    if (this.props.curEventNodeList) {
+      return null
+    }
+    switch (this.state.type) {
+      case 'service':
+      case 'timer':
+      case 'transaction':
+      case 'dyTransaction':
+        return (
+          <div
+            className={cls('title', {
+              'f--hlc': ['service', 'timer'].indexOf(this.state.type) >= 0
+            })}
+          >
+            {i18n.t('EventView.jenga_nav.' + this.state.type + 'Id')}
+            <span>{this.state.curEventNodeId}</span>
+            {['service', 'timer'].indexOf(this.state.type) >= 0 ? (
+              <>
+                {!curClassId && (
+                  <>
+                    <JengaDebugBtn nodeId={this.state.curEventNodeId} />
+                    <LogBtn />
+                  </>
+                )}
+              </>
+            ) : null}
+          </div>
+        )
+      case 'iotMessage':
+        return (
+          <div className={cls('title f--hlc')}>
+            {<LogBtn className={'btn-iot-log'} />}
+          </div>
+        )
+      default:
+        return null
+    }
+  }
+
+  render() {
+    return (
+      <div className="jenga-nav-info f--hlc flex-1">
+        {this.getInfo()}
+        <JengaNavNavigateNodes />
+      </div>
+    )
+  }
+}

+ 56 - 0
coms/jenga/nav/JengaNavMemo.jsx

@@ -0,0 +1,56 @@
+import React from 'react'
+import cls from 'classnames'
+import i18n from 'i18next'
+import newEventActions from 'actions/newEvent'
+import newEventStores from 'stores/newEvent'
+
+export default class JengaNavMemo extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      expanded: newEventStores.showMemo
+    }
+    this.onEventChange = this.onEventChange.bind(this)
+    this.onToggleMemo = this.onToggleMemo.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('showMemo')) {
+        if (this.state.expanded !== status.data.showMemo) {
+          obj.expanded = status.data.showMemo
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  onToggleMemo() {
+    newEventActions.toggleMemo({})
+  }
+
+  render() {
+    return (
+      <div className={cls('jenga-nav-memo', { expanded: this.state.expanded })}>
+        <button
+          className="btn-clear btn-toggle-memo f--hcc"
+          onClick={this.onToggleMemo}
+        >
+          <div className="icon" />
+          <div className="title">{i18n.t('EventView.jenga_nav.memo')}</div>
+        </button>
+      </div>
+    )
+  }
+}

+ 112 - 0
coms/jenga/nav/JengaNavNavigateNodes.jsx

@@ -0,0 +1,112 @@
+import React from 'react'
+import treeActions from 'actions/tree'
+import newEventActions from 'actions/newEvent'
+import newEventStores from 'stores/newEvent'
+
+export default class JengaNavNavigateNodes extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      curNavIds: newEventStores.curNavIds,
+      curNavId: newEventStores.curNavId
+    }
+
+    this.onEventChange = this.onEventChange.bind(this)
+    this.curNavIdIndex = this.curNavIdIndex.bind(this)
+    this.onClick = this.onClick.bind(this)
+    this.isAllow = this.isAllow.bind(this)
+    this.allowBackward = this.allowBackward.bind(this)
+    this.allowForward = this.allowForward.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('curNavIds')) {
+        obj.curNavIds = status.data.curNavIds
+      }
+      if (status.types.includes('curNavId')) {
+        if (this.state.curNavId !== status.data.curNavId) {
+          obj.curNavId = status.data.curNavId
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+      this.forceUpdate()
+    }
+  }
+
+  curNavIdIndex() {
+    let { curNavIds, curNavId } = this.state
+    return curNavIds.findIndex(v => {
+      return v.id === curNavId
+    })
+  }
+
+  onClick(type) {
+    let index = this.curNavIdIndex()
+    let { curNavIds } = this.state
+    let navInfo = undefined
+    switch (type) {
+      case 'backward':
+        navInfo = curNavIds[index - 1]
+        break
+      case 'forward':
+        navInfo = curNavIds[index + 1]
+        break
+    }
+    if (navInfo) {
+      treeActions.selectNode(navInfo.nodeId)
+      newEventActions.toggleEventView({
+        show: true,
+        nodeId: navInfo.nodeId,
+        mode: navInfo.mode,
+        navId: navInfo.id
+      })
+    }
+  }
+
+  isAllow() {
+    let { curNavIds, curNavId } = this.state
+    return !!(curNavIds && curNavIds.length > 0 && curNavId)
+  }
+
+  allowBackward() {
+    return this.isAllow() && !([-1, 0].indexOf(this.curNavIdIndex()) >= 0)
+  }
+
+  allowForward() {
+    let { curNavIds } = this.state
+    return (
+      this.isAllow() &&
+      !([-1, curNavIds.length - 1].indexOf(this.curNavIdIndex()) >= 0)
+    )
+  }
+
+  render() {
+    return (
+      <div className="jenga-nav-navigate-nodes-wrap f--hlc">
+        <button
+          className="btn-clear btn-navigate-backward"
+          disabled={!this.allowBackward()}
+          onClick={this.onClick.bind(this, 'backward')}
+        />
+        <button
+          className="btn-clear btn-navigate-forward"
+          disabled={!this.allowForward()}
+          onClick={this.onClick.bind(this, 'forward')}
+        />
+      </div>
+    )
+  }
+}

+ 348 - 0
coms/jenga/nav/JengaTypeBlockOperator.jsx

@@ -0,0 +1,348 @@
+import React from 'react'
+import { Switch } from 'antd'
+import i18n from 'i18next'
+import cls from 'classnames'
+import newEventActions from 'actions/newEvent'
+import newEventStores from 'stores/newEvent'
+// import {
+//   getActionBlockObjNode,
+//   getEventBlockByBid,
+//   getEventNodeById
+// } from 'stores/funcs/newEvent'
+// import {
+//   nodeIsDbPayment,
+//   nodeIsFunctionGroup,
+//   nodeIsService,
+//   nodeIsIotMessage,
+//   nodeIsFuncGroupCb,
+//   nodeIsMq
+// } from 'stores/funcs/nodeType'
+import { EVENT_BLOCK_TYPE } from 'const/const'
+
+export default class JengaTypeBlockOperator extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      showComment: newEventStores.showComment, // 显示备注条
+      type: 'child' // 子层,同层
+      // enables: this.genEnableMap('child') // 分别对应循环,动作,条件
+    }
+
+    this.curEventNodeId = newEventStores.curEventNodeId
+    this.curBlockId = newEventStores.curBlockId
+
+    this.buttons = [
+      {
+        type: EVENT_BLOCK_TYPE.LOOP,
+        class: 'btn-loop',
+        name: i18n.t('EventView.jenga_nav.loop')
+      },
+      {
+        type: EVENT_BLOCK_TYPE.ACTION,
+        class: 'btn-action',
+        name: i18n.t('EventView.jenga_nav.action')
+      },
+      {
+        type: EVENT_BLOCK_TYPE.CON,
+        class: 'btn-con',
+        name: i18n.t('EventView.jenga_nav.con')
+      },
+      {
+        type: EVENT_BLOCK_TYPE.COMMENT,
+        class: 'btn-comment',
+        name: i18n.t('EventView.jenga_nav.memo')
+      }
+    ]
+
+    this.onEventChange = this.onEventChange.bind(this)
+
+    this.onChangeType = this.onChangeType.bind(this)
+    this.isSelectedType = this.isSelectedType.bind(this)
+    this.onChangeShowComment = this.onChangeShowComment.bind(this)
+    // this.genEnableMap = this.genEnableMap.bind(this)
+    // this.dealEnableMap = this.dealEnableMap.bind(this)
+    // this.dealStatusEnable = this.dealStatusEnable.bind(this)
+
+    this.onAddBlock = this.onAddBlock.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status.types.includes('curEventNodeId')) {
+      if (this.curEventNodeId !== status.data.curEventNodeId) {
+        this.curEventNodeId = status.data.curEventNodeId
+        obj.enables = this.genEnableMap(this.state.type)
+      }
+    }
+    if (status.types.includes('curBlockId')) {
+      if (this.curBlockId !== status.data.curBlockId) {
+        this.curBlockId = status.data.curBlockId
+        obj.enables = this.genEnableMap(this.state.type)
+      }
+    }
+    if (
+      status.types.includes('updateBlockProp') &&
+      ['object', 'action'].indexOf(status.data.prop) >= 0 &&
+      this.curBlockId === status.data.affectBlockId
+    ) {
+      obj.enables = this.genEnableMap(this.state.type)
+    }
+    if (status.types.includes('changeShowComment')) {
+      let { showComment } = this.state
+      if (showComment !== status.data.showComment) {
+        obj.showComment = status.data.showComment
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  onChangeType(type) {
+    let obj = {}
+    switch (type) {
+      case true:
+        obj.type = 'bro'
+        break
+      case false:
+        obj.type = 'child'
+        break
+    }
+    this.setState(obj, () => {
+      // this.setState({ enables: this.genEnableMap(this.state.type) })
+    })
+  }
+
+  isSelectedType(type) {
+    return this.state.type === type
+  }
+
+  onChangeShowComment(val) {
+    this.setState(
+      {
+        showComment: val
+      },
+      () => {
+        newEventActions.changeShowComment({
+          showComment: this.state.showComment
+        })
+      }
+    )
+  }
+
+  // genEnableMap(type) {
+  //   let result = {}
+  //   let curBlock = getEventBlockByBid(this.curBlockId)
+  //   if (!curBlock) {
+  //     return this.dealEnableMap()
+  //   } else {
+  //     switch (type) {
+  //       case 'bro':
+  //         let parentBid = curBlock.parentBid
+  //         let parentBlock = getEventBlockByBid(parentBid)
+  //         return this.dealEnableMap(parentBlock)
+  //       case 'child':
+  //         return this.dealEnableMap(curBlock)
+  //     }
+  //   }
+  //   return result
+  // }
+
+  // dealEnableMap(block) {
+  //   let curEventNode = getEventNodeById(this.curEventNodeId)
+  //   let result = {}
+  //   result[EVENT_BLOCK_TYPE.ACTION] = false
+  //   result[EVENT_BLOCK_TYPE.LOOP] = false
+  //   result[EVENT_BLOCK_TYPE.CON] = false
+  //   result[EVENT_BLOCK_TYPE.STATUS] = false
+  //   result[EVENT_BLOCK_TYPE.ROOT] =
+  //     curEventNode &&
+  //     (!nodeIsService(curEventNode.type) ||
+  //       (nodeIsService(curEventNode.type) &&
+  //         nodeIsIotMessage(curEventNode.type))) &&
+  //     !nodeIsFunctionGroup(curEventNode.type) &&
+  //     !nodeIsDbPayment(curEventNode.type) &&
+  //     !nodeIsMq(curEventNode.type)
+  //   result[EVENT_BLOCK_TYPE.COMMENT] = false
+  //   result[EVENT_BLOCK_TYPE.GROUP] = false
+  //   if (block) {
+  //     switch (block.type) {
+  //       case EVENT_BLOCK_TYPE.ACTION:
+  //         result[EVENT_BLOCK_TYPE.ACTION] = false
+  //         result[EVENT_BLOCK_TYPE.LOOP] = false
+  //         result[EVENT_BLOCK_TYPE.CON] = false
+  //         result[EVENT_BLOCK_TYPE.STATUS] = this.dealStatusEnable(block)
+  //         result[EVENT_BLOCK_TYPE.COMMENT] = false
+  //         result[EVENT_BLOCK_TYPE.GROUP] = false
+  //         break
+  //       case EVENT_BLOCK_TYPE.ROOT:
+  //       case EVENT_BLOCK_TYPE.LOOP:
+  //       case EVENT_BLOCK_TYPE.CON:
+  //       case EVENT_BLOCK_TYPE.STATUS:
+  //       case EVENT_BLOCK_TYPE.GROUP:
+  //         result[EVENT_BLOCK_TYPE.ACTION] = true
+  //         result[EVENT_BLOCK_TYPE.LOOP] = true
+  //         result[EVENT_BLOCK_TYPE.CON] = true
+  //         result[EVENT_BLOCK_TYPE.STATUS] = false
+  //         result[EVENT_BLOCK_TYPE.COMMENT] = true
+  //         result[EVENT_BLOCK_TYPE.GROUP] = true
+  //         break
+  //     }
+  //   }
+  //   return result
+  // }
+
+  // dealStatusEnable(block) {
+  //   let result = false
+  //   if (block.action && block.action.callback) {
+  //     // 当前的动作
+  //     let obj = getActionBlockObjNode({
+  //       obj: block.object,
+  //       blockId: block.bid
+  //     })
+  //     if (obj && nodeIsFunctionGroup(obj.type)) {
+  //       // 是否action是调用动作且此对象的事件有回调函数
+  //       if (
+  //         block.action.name === 'fireFuncGroup' &&
+  //         obj &&
+  //         obj.events &&
+  //         obj.events.list &&
+  //         obj.events.list.length > 0 &&
+  //         obj.events.order &&
+  //         obj.events.order.length > 0
+  //       ) {
+  //         let found = false
+  //         for (let i = 0; i < obj.events.order.length && !found; i++) {
+  //           let _block = getEventBlockByBid(obj.events.order[i])
+  //           if (
+  //             _block &&
+  //             _block.type === EVENT_BLOCK_TYPE.ACTION &&
+  //             _block.enable !== false &&
+  //             _block.action &&
+  //             _block.action.name === 'funcResult'
+  //           ) {
+  //             found = true
+  //           }
+  //         }
+  //         if (found) {
+  //           result = true
+  //         }
+  //       }
+  //       // 有自定义回调也算
+  //       if (!result && obj.children) {
+  //         obj.children.forEach(v => {
+  //           let childNode = getEventNodeById(v)
+  //           if (childNode && nodeIsFuncGroupCb(childNode.type)) {
+  //             result = true
+  //           }
+  //         })
+  //       }
+  //     } else {
+  //       result = true
+  //     }
+  //   }
+  //   return result
+  // }
+
+  onAddBlock(type) {
+    // if (type !== EVENT_BLOCK_TYPE.ROOT) {
+    //   switch (this.state.type) {
+    //     case 'bro':
+    //       let curBlock = getEventBlockByBid(this.curBlockId)
+    //       if (curBlock) {
+    //         let parentBid = curBlock.parentBid
+    //         let parentBlock = getEventBlockByBid(parentBid)
+    //         if (parentBlock) {
+    //           let index = parentBlock.children.indexOf(this.curBlockId)
+    //           if (index !== -1) {
+    //             newEventActions.addBlock({ type, parentBid, index: index + 1 })
+    //           }
+    //         }
+    //       }
+    //       break
+    //     case 'child':
+    //       newEventActions.addBlock({ type, parentBid: this.curBlockId })
+    //       break
+    //   }
+    // } else {
+    //   newEventActions.addBlock({ type })
+    // }
+  }
+
+  render() {
+    return (
+      <div className="jenga-type-block-operator f--hlc">
+        <div className="show-comment-select-wrap f--hlc">
+          <Switch
+            className="jenga-type-switch"
+            checked={this.state.showComment}
+            onChange={this.onChangeShowComment}
+            size="small"
+          />
+          <div
+            className={cls('btn-clear', {
+              'btn-selected': this.state.showComment
+            })}
+          >
+            {i18n.t('EventView.jenga_nav.block_show_comment')}
+          </div>
+        </div>
+        <div className="type-select-wrap f--hlc">
+          <button
+            className={cls('btn-clear', {
+              'btn-selected': this.isSelectedType('child')
+            })}
+          >
+            {i18n.t('EventView.jenga_nav.block_type_child')}
+          </button>
+          <Switch
+            className="jenga-type-switch"
+            checked={this.state.type === 'bro'}
+            onChange={this.onChangeType}
+            size="small"
+          />
+          <div
+            className={cls('btn-clear', {
+              'btn-selected': this.isSelectedType('bro')
+            })}
+          >
+            {i18n.t('EventView.jenga_nav.block_type_bro')}
+          </div>
+        </div>
+        <div className="block-select-wrap f--hlc">
+          {this.buttons.map((btn, i) => {
+            return (
+              <button
+                key={i}
+                // disabled={!this.state.enables[btn.type]}
+                onClick={this.onAddBlock.bind(this, btn.type)}
+                className={cls('btn-clear btn-block', btn.class)}
+              >
+                {btn.name}
+              </button>
+            )
+          })}
+          <button
+            className="btn-clear btn-block btn-event f--hlc"
+            // disabled={!this.state.enables[EVENT_BLOCK_TYPE.ROOT]}
+            onClick={this.onAddBlock.bind(this, EVENT_BLOCK_TYPE.ROOT)}
+          >
+            <div>{i18n.t('EventView.jenga_nav.event')}</div>
+            <div className="event-icon">
+              <span className="horizon" />
+              <span className="vertical" />
+            </div>
+          </button>
+        </div>
+      </div>
+    )
+  }
+}

+ 97 - 0
coms/jenga/nav/debug/JengaDebugBtn.jsx

@@ -0,0 +1,97 @@
+import React from 'react'
+import i18n from 'i18next'
+import uiActions from 'actions/ui'
+import cooperateActions from 'actions/cooperate'
+import treeStores from 'stores/tree'
+import serviceStores from 'src/stores/service'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import { saveCaseBeforeActions } from 'stores/funcs/service'
+
+export default class JengaDebugBtn extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.saveCase = this.saveCase.bind(this)
+    this.debugService = this.debugService.bind(this)
+    this.onClick = this.onClick.bind(this)
+  }
+
+  saveCase(done) {
+    uiActions.toggleLoadingBar({
+      show: true,
+      title: i18n.t('LoadingBar.debugServiceTitle'),
+      content: i18n.t('LoadingBar.debugServiceContent'),
+      progress: 100,
+      type: 'dbLoading'
+    })
+    saveCaseBeforeActions({
+      success: () => {
+        done && done()
+      },
+      fail: () => {
+        uiActions.toggleLoadingBar({ show: false })
+      }
+    })
+  }
+
+  debugService(cb) {
+    let node = getEventNodeById(this.props.nodeId)
+    if (node) {
+      let { nid, eid, gid, uid, client } = treeStores.curCaseInfo
+      let data = {
+        _nid: nid,
+        _eid: eid,
+        _gid: gid,
+        _uid: uid,
+        _sid: node.id,
+        _locOffset: new Date().getTimezoneOffset() * -60
+      }
+      // 多人开发添加client
+      cooperateActions.getIsGroupWork(isGroupWork => {
+        if (isGroupWork) {
+          data._client = client
+        }
+      })
+      if (
+        node &&
+        node.props &&
+        node.props.inParams &&
+        node.props.inParams.length > 0
+      ) {
+        node.props.inParams.forEach(param => {
+          if (param.name && (param.default || param.default === 0)) {
+            data[param.name] = serviceStores.dealDefaultParam(
+              param.default,
+              param.type,
+              true
+            )
+          }
+        })
+      }
+      serviceStores.debugService(
+        data,
+        code => {
+          uiActions.toggleLoadingBar({ show: false })
+          cb && cb(code)
+        },
+        true
+      )
+    }
+  }
+
+  onClick() {
+    this.saveCase(() => {
+      this.debugService(code => {
+        uiActions.toggleServiceDebug({ show: true, code })
+      })
+    })
+  }
+
+  render() {
+    return (
+      <button className="btn-clear btn-debug" onClick={this.onClick}>
+        {i18n.t('EventView.jenga_nav.debug')}
+      </button>
+    )
+  }
+}

+ 120 - 0
coms/jenga/panel/JengaCite.jsx

@@ -0,0 +1,120 @@
+import React from 'react'
+import cls from 'classnames'
+import { shallowEqualImmutable } from 'src/utils/utils'
+import citeActions from 'actions/cite'
+import citeStores from 'stores/cite'
+import newEventStores from 'stores/newEvent'
+
+export default class JengaCite extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      eventPanelMode: newEventStores.eventPanelMode,
+      curEventNodeId: newEventStores.curEventNodeId,
+      isActive: false, // 是否处于引用激活态
+      memoExpanded: newEventStores.showMemo // 备注是否展开
+    }
+
+    this.getIsActive = this.getIsActive.bind(this)
+    this.onEventChange = this.onEventChange.bind(this)
+    this.onCiteChange = this.onCiteChange.bind(this)
+    this.onToggleJengaCite = this.onToggleJengaCite.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+    this.unsubscribeCite = citeStores.listen(this.onCiteChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+    this.unsubscribeCite()
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  componentDidUpdate(preProps) {
+    let isActive = this.getIsActive()
+    if (this.state.isActive !== isActive) {
+      this.setState({
+        isActive: isActive
+      })
+    }
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('curEventNodeId')) {
+        obj.curEventNodeId = status.data.curEventNodeId
+      }
+      if (status.types.includes('showMemo')) {
+        if (this.state.memoExpanded !== status.data.showMemo) {
+          obj.memoExpanded = status.data.showMemo
+        }
+      }
+      if (status.data && status.data.showEventView !== undefined) {
+        if (this.state.eventPanelMode !== status.data.eventPanelMode) {
+          obj.eventPanelMode = status.data.eventPanelMode
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  onCiteChange(status) {
+    let obj = {}
+    if (status.types.includes('activeJengaCite')) {
+      obj.isActive = status.data.eventNodeId === this.props.eventNodeId
+    }
+    if (
+      status.types.includes('deactiveCite') ||
+      status.types.includes('activeEventsCited') ||
+      status.types.includes('activePropsCite') ||
+      status.types.includes('activeBlockCite')
+    ) {
+      obj.isActive = false
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  getIsActive() {
+    return (
+      citeStores.curCite &&
+      citeStores.curCite.eventNodeId === this.props.eventNodeId &&
+      citeStores.curCite.type === 'jengaCite'
+    )
+  }
+
+  onToggleJengaCite() {
+    let curNodeId = this.props.eventNodeId || this.state.curEventNodeId
+    if (this.state.isActive) {
+      citeActions.deactiveAllCite({})
+    } else {
+      citeActions.activeJengaCite({
+        eventNodeId: curNodeId
+      })
+    }
+  }
+
+  render() {
+    return this.state.eventPanelMode === 'citedJengas' ? (
+      <button
+        className={cls('btn-clear btn-jenga-cite', {
+          active: this.state.isActive,
+          'memo-expanded': this.state.memoExpanded
+        })}
+        onClick={this.onToggleJengaCite}
+      />
+    ) : null
+  }
+}

+ 85 - 0
coms/jenga/panel/JengaDrag.jsx

@@ -0,0 +1,85 @@
+import React from 'react'
+import uiActions from 'actions/ui'
+import uiStores from 'stores/ui'
+import newEventStores from 'stores/newEvent'
+
+const minWidth = 920 // 最小宽度
+
+export default class JengaDrag extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.onEventChange = this.onEventChange.bind(this)
+    this.getMinWidth = this.getMinWidth.bind(this)
+    this.resetWidth = this.resetWidth.bind(this)
+
+    this.onDragStart = this.onDragStart.bind(this)
+    this.onDragEnd = this.onDragEnd.bind(this)
+    this.onDragMove = this.onDragMove.bind(this)
+  }
+
+  componentDidMount() {
+    this.resetWidth()
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onEventChange(status) {
+    if (status && status.types) {
+      if (status.types.includes('showMemo')) {
+        this.resetWidth()
+      }
+    }
+  }
+
+  getMinWidth() {
+    return minWidth + (newEventStores.showMemo ? 150 : 0)
+  }
+
+  resetWidth(setViewWidth = true, afterSetWidth) {
+    if (setViewWidth) {
+      this.viewWidth = this.getMinWidth() + uiStores.jengaViewWidthDelta
+    }
+    let eventView = document.querySelector('.event-view')
+    if (eventView) {
+      eventView.style.width = this.viewWidth + 'px'
+      afterSetWidth && afterSetWidth()
+    }
+  }
+
+  onDragStart(e) {
+    this.startX = e.pageX
+    let curMinWidth = this.getMinWidth()
+    this.startWidth =
+      this.viewWidth > curMinWidth ? this.viewWidth : curMinWidth
+    document.addEventListener('mousemove', this.onDragMove)
+    document.addEventListener('mouseup', this.onDragEnd)
+  }
+
+  onDragEnd() {
+    document.removeEventListener('mousemove', this.onDragMove)
+    document.removeEventListener('mouseup', this.onDragEnd)
+  }
+
+  onDragMove(e) {
+    this.viewWidth = this.startWidth + (e.pageX - this.startX)
+    let curMinWidth = this.getMinWidth()
+    if (this.viewWidth < curMinWidth) {
+      this.viewWidth = curMinWidth
+    }
+    // 使用style的方式
+    this.resetWidth(false, () => {
+      // 通知其他相关组件去更新宽度
+      uiActions.changeJengaViewWidthDelta({
+        delta: this.viewWidth - this.getMinWidth()
+      })
+    })
+  }
+
+  render() {
+    return <div className="jenga-drag" onMouseDown={this.onDragStart} />
+  }
+}

+ 238 - 0
coms/jenga/panel/JengaItem.jsx

@@ -0,0 +1,238 @@
+import React from 'react'
+import cls from 'classnames'
+import ResizeObserver from 'resize-observer-polyfill'
+import BlockContext from '../utils'
+import treeStores from 'stores/tree'
+import newEventStores from 'stores/newEvent'
+import { getEventNodeById, minScrollJengaPanel } from 'stores/funcs/newEvent'
+import { nodeIsSystem, nodeIsInModule } from 'stores/funcs/nodeType'
+import JengaItemTitle from './JengaItemTitle'
+import JengaItemEvent from './JengaItemEvent'
+
+export default class JengaItem extends React.Component {
+  constructor(props) {
+    super(props)
+    this.curEventNodeId = newEventStores.curEventNodeId
+    this.state = {
+      isActive: this.isCurEventNodeId(this.curEventNodeId),
+      isEnable: this.isEnable(),
+      sysList: this.getCurSysList()
+    }
+
+    this.onTreeChange = this.onTreeChange.bind(this)
+    this.onEventChange = this.onEventChange.bind(this)
+    this.onActiveObserver = this.onActiveObserver.bind(this)
+    this.onInactiveObserver = this.onInactiveObserver.bind(this)
+    this.onResize = this.onResize.bind(this)
+
+    this.isCurEventNodeId = this.isCurEventNodeId.bind(this)
+    this.isEnable = this.isEnable.bind(this)
+    this.getCurSysList = this.getCurSysList.bind(this)
+    this.updateCurSysList = this.updateCurSysList.bind(this)
+    this.getSysList = this.getSysList.bind(this)
+    this.getModuleSysList = this.getModuleSysList.bind(this)
+  }
+
+  componentDidMount() {
+    window.addEventListener('resize', this.onResize)
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+    this.unsubscribeTree = treeStores.listen(this.onTreeChange)
+    this.onActiveObserver()
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.onResize)
+    this.unsubscribe()
+    this.unsubscribeTree()
+    this.onInactiveObserver()
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.eventNodeId !== this.props.eventNodeId) {
+      this.setState({
+        isActive: this.isCurEventNodeId(this.curEventNodeId),
+        isEnable: this.isEnable(),
+        sysList: this.getCurSysList()
+      })
+    }
+  }
+
+  onTreeChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (
+        status.types.includes('curNodeIdList') ||
+        status.types.includes('curRootId')
+      ) {
+        obj.sysList = this.getCurSysList()
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('curEventNodeId')) {
+        if (this.curEventNodeId !== status.data.curEventNodeId) {
+          let curEventNodeId = status.data.curEventNodeId
+          if (this.isCurEventNodeId(status.data.curEventNodeId)) {
+            this.updateCurSysList(obj, curEventNodeId, this.curEventNodeId)
+          }
+          this.curEventNodeId = curEventNodeId
+          let isEnable = this.isEnable()
+          if (isEnable !== this.state.isEnable) {
+            obj.isEnable = isEnable
+          }
+          let isActive = this.isCurEventNodeId(this.curEventNodeId)
+          if (isActive !== this.state.isActive) {
+            obj.isActive = isActive
+          }
+        }
+      }
+      if (status.types.includes('toggleEvent')) {
+        if (status.data.affectEventNodeId === this.curEventNodeId) {
+          if (status.data.prop === 'enable') {
+            if (this.isCurEventNodeId(status.data.affectEventNodeId)) {
+              let isEnable = this.isEnable()
+              if (isEnable !== this.state.isEnable) {
+                obj.isEnable = isEnable
+              }
+            }
+          }
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  isCurEventNodeId(curEventNodeId) {
+    return !this.props.eventNodeId || this.props.eventNodeId === curEventNodeId
+  }
+
+  isEnable() {
+    let isEnable = true
+    let nodeId = this.props.eventNodeId || this.curEventNodeId
+    if (nodeId) {
+      let node = getEventNodeById(nodeId)
+      if (node && node.events && node.events.enable === false) {
+        isEnable = false
+      }
+    }
+    return isEnable
+  }
+
+  onActiveObserver() {
+    // 选择目标节点
+    let target = document.querySelector('.jenga-item')
+    // 创建观察者对象
+    this.observer = new ResizeObserver(entries => {
+      let entry = entries[0]
+      if (entry.contentRect.height !== undefined) {
+        minScrollJengaPanel()
+      }
+    })
+    // 传入目标节点和观察选项
+    this.observer.observe(target)
+  }
+
+  onInactiveObserver() {
+    this.observer && this.observer.disconnect()
+  }
+
+  onResize() {
+    minScrollJengaPanel()
+  }
+
+  getCurSysList() {
+    let sysList = []
+    let curEventNodeId = this.props.eventNodeId || this.curEventNodeId
+    let moduleRootId = nodeIsInModule(curEventNodeId, true)
+    // 当前的node是在module内刷新sysList
+    if (moduleRootId) {
+      sysList = this.getModuleSysList(moduleRootId)
+    } else {
+      // module内的重新获取sysList
+      sysList = this.getSysList()
+    }
+    return sysList
+  }
+
+  updateCurSysList(obj, curNodeId, preNodeId) {
+    let moduleRootId = nodeIsInModule(curNodeId, true)
+    // 当前的node是在module内刷新sysList
+    if (moduleRootId) {
+      obj.sysList = this.getModuleSysList(moduleRootId)
+    } else if (nodeIsInModule(preNodeId)) {
+      // module内的重新获取sysList
+      obj.sysList = this.getSysList()
+    }
+  }
+
+  getSysList() {
+    let result = []
+    let list = treeStores.curNodeIdList
+    for (let i = 0, len = list.length; i < len; i++) {
+      let node = treeStores.getNodeById(list[i])
+      if (node && node.type === 'data-module') {
+        // 不计算内部的sys
+        let lastNodeId = node.id
+        while (node && node.children && node.children.length > 0) {
+          node = treeStores.getNodeById(node.children[node.children.length - 1])
+        }
+        if (lastNodeId !== node.id) {
+          lastNodeId = node.id
+          let lastIndex = list.indexOf(lastNodeId)
+          if (i < lastIndex && lastIndex < len) {
+            i = lastIndex
+          }
+        }
+      } else if (node && nodeIsSystem(node.type)) {
+        result.push(list[i])
+      }
+    }
+    return result
+  }
+
+  getModuleSysList(moduleRootId) {
+    let result = []
+    let loopNode = nodeId => {
+      let node = treeStores.getNodeById(nodeId)
+      if (node) {
+        if (nodeIsSystem(node.type)) {
+          result.push(node.id)
+        }
+        if (node.children) {
+          node.children.forEach(childId => {
+            loopNode(childId)
+          })
+        }
+      }
+    }
+    loopNode(moduleRootId)
+    return result
+  }
+
+  render() {
+    return (
+      <div
+        className={cls('jenga-item', {
+          active: this.state.isActive,
+          disabled: !this.state.isEnable
+        })}
+      >
+        <JengaItemTitle eventNodeId={this.props.eventNodeId} />
+        <BlockContext.Provider value={{ sysList: this.state.sysList }}>
+          <JengaItemEvent
+            eventNodeId={this.props.eventNodeId}
+            visibleBlockIds={this.props.visibleBlockIds}
+          />
+        </BlockContext.Provider>
+      </div>
+    )
+  }
+}

+ 154 - 0
coms/jenga/panel/JengaItemEvent.jsx

@@ -0,0 +1,154 @@
+import React from 'react'
+import LazyLoad from 'react-lazyload'
+import newEventStores from 'stores/newEvent'
+import BlockEvent from '../block/BlockEvent'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import { shallowEqualImmutable } from 'utils/utils'
+
+export default class JengaItemEvent extends React.Component {
+  constructor(props) {
+    super(props)
+    this.curEventNodeId = newEventStores.curEventNodeId
+    this.state = {
+      eventList: this.getEventList()
+    }
+    this.onEventChange = this.onEventChange.bind(this)
+    this.isCurEventNodeId = this.isCurEventNodeId.bind(this)
+    this.getEventList = this.getEventList.bind(this)
+    this.getBlockRoot = this.getBlockRoot.bind(this)
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  componentDidMount() {
+    this.unsubscribe = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.eventNodeId !== this.props.eventNodeId) {
+      this.setState({
+        eventList: this.getEventList()
+      })
+    }
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    let forceUpdate = false
+    if (status) {
+      if (
+        status.types.includes('curEventNodeId') &&
+        this.curEventNodeId !== status.data.curEventNodeId
+      ) {
+        this.curEventNodeId = status.data.curEventNodeId
+        if (this.isCurEventNodeId(status.data.curEventNodeId)) {
+          obj.eventList = this.getEventList()
+          forceUpdate = true
+        }
+      }
+      if (
+        status.types.includes('changeEventList') &&
+        status.data &&
+        this.curEventNodeId === status.data.curEventNodeId
+      ) {
+        if (this.isCurEventNodeId(status.data.curEventNodeId)) {
+          obj.eventList = this.getEventList()
+          forceUpdate = true
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj, () => {
+        if (forceUpdate) {
+          // eventList是对象,所以shouldComponentUpdate不会被触发,需强制更新
+          this.forceUpdate()
+        }
+      })
+    }
+  }
+
+  isCurEventNodeId(curEventNodeId) {
+    return !this.props.eventNodeId || this.props.eventNodeId === curEventNodeId
+  }
+
+  getEventList() {
+    let nodeId = this.props.eventNodeId || this.curEventNodeId
+    if (nodeId) {
+      let node = getEventNodeById(nodeId)
+      if (node) {
+        return node.events.list
+      }
+    }
+    return []
+  }
+
+  getBlockRoot(event, index) {
+    return (
+      <BlockEvent
+        key={index}
+        layer={0}
+        groupLayer={0}
+        nestGroupLayer={0}
+        offspringInNestGroupLayer={0}
+        bid={event.eventId}
+        eventNodeId={this.props.eventNodeId}
+        visibleBlockIds={this.props.visibleBlockIds}
+        isLast={index === this.state.eventList.length - 1}
+      />
+    )
+  }
+
+  render() {
+    let jengaContent = document.getElementsByClassName('jenga-content')[0]
+
+    if (this.props.visibleBlockIds) {
+      return this.state.eventList.map((event, index) => {
+        if (
+          event.eventId &&
+          this.props.visibleBlockIds.indexOf(event.eventId) >= 0
+        ) {
+          // 加lazyload是为了避免load太多
+          return (
+            <LazyLoad
+              key={index}
+              height={200}
+              overflow={true}
+              scrollContainer={jengaContent}
+            >
+              {this.getBlockRoot(event, index)}
+            </LazyLoad>
+          )
+        } else {
+          return null
+        }
+      })
+    }
+
+    return this.state.eventList.map((event, index) => {
+      if (event.eventId) {
+        // 加lazyload是为了避免load太多
+        return (
+          <LazyLoad
+            key={index}
+            height={200}
+            overflow={true}
+            scrollContainer={jengaContent}
+          >
+            {this.getBlockRoot(event, index)}
+          </LazyLoad>
+        )
+      } else {
+        return null
+      }
+    })
+  }
+}

+ 166 - 0
coms/jenga/panel/JengaItemTitle.jsx

@@ -0,0 +1,166 @@
+import React from 'react'
+import cls from 'classnames'
+import newEventActions from 'actions/newEvent'
+import uiActions from 'actions/ui'
+import newEventStores from 'stores/newEvent'
+import treeStores from 'stores/tree'
+import { getEventNodeById } from 'stores/funcs/newEvent'
+import { shallowEqualImmutable } from 'utils/utils'
+import { CONTEXT_MENU_TYPE } from 'const/const'
+import JengaCite from './JengaCite'
+
+export default class JengaItemTitle extends React.Component {
+  constructor(props) {
+    super(props)
+    this.curEventNodeId = newEventStores.curEventNodeId
+    this.curBlockId = newEventStores.curBlockId
+    this.state = {
+      name: this.getName(),
+      isActive: this.isActive()
+    }
+
+    this.onTreeChange = this.onTreeChange.bind(this)
+    this.onEventChange = this.onEventChange.bind(this)
+    this.isCurEventNodeId = this.isCurEventNodeId.bind(this)
+    this.getName = this.getName.bind(this)
+    this.isActive = this.isActive.bind(this)
+
+    this.onSelectBlock = this.onSelectBlock.bind(this)
+    // 右键
+    this.onContextMenu = this.onContextMenu.bind(this)
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return (
+      !shallowEqualImmutable(this.props, nextProps) ||
+      !shallowEqualImmutable(this.state, nextState)
+    )
+  }
+
+  componentDidMount() {
+    this.unsubscribe = treeStores.listen(this.onTreeChange)
+    this.unsubscribeEvent = newEventStores.listen(this.onEventChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+    this.unsubscribeEvent()
+  }
+
+  componentDidUpdate(preProps) {
+    if (preProps.eventNodeId !== this.props.eventNodeId) {
+      this.setState({
+        name: this.getName(),
+        isActive: this.isActive()
+      })
+    }
+  }
+
+  onTreeChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('changeNodeUIs')) {
+        if (this.curEventNodeId === status.data.nodeId) {
+          let uis = status.data.uis
+          if (
+            Object.keys(uis).includes('name') &&
+            this.state.name !== uis.name
+          ) {
+            obj.name = uis.name
+          }
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status && status.types) {
+      if (status.types.includes('curEventNodeId')) {
+        if (this.curEventNodeId !== status.data.curEventNodeId) {
+          this.curEventNodeId = status.data.curEventNodeId
+          if (this.isCurEventNodeId(status.data.curEventNodeId)) {
+            obj.name = this.getName()
+          }
+          if (this.state.isActive !== this.isActive()) {
+            obj.isActive = this.isActive()
+          }
+        }
+      }
+      if (status.types.includes('curBlockId')) {
+        if (this.curBlockId !== status.data.curBlockId) {
+          this.curBlockId = status.data.curBlockId
+          if (this.state.isActive !== this.isActive()) {
+            obj.isActive = this.isActive()
+          }
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  isCurEventNodeId(curEventNodeId) {
+    return !this.props.eventNodeId || this.props.eventNodeId === curEventNodeId
+  }
+
+  getName() {
+    let nodeId = this.props.eventNodeId || this.curEventNodeId
+    if (nodeId) {
+      let node = getEventNodeById(nodeId)
+      if (node) {
+        return node.uis.name || node.type
+      }
+    }
+    return ''
+  }
+
+  isActive() {
+    return (
+      this.isCurEventNodeId(this.curEventNodeId) && this.curBlockId === null
+    )
+  }
+
+  onSelectBlock() {
+    if (
+      this.props.eventNodeId &&
+      this.props.eventNodeId !== newEventStores.curEventNodeId
+    ) {
+      newEventActions.selectEvent({ eventNodeId: this.props.eventNodeId })
+    }
+    if (!this.state.isActive) {
+      newEventActions.selectBlock({ blockId: null })
+    }
+  }
+
+  onContextMenu(e) {
+    e.preventDefault()
+    e.stopPropagation()
+    this.onSelectBlock()
+    let info = {
+      x: e.pageX,
+      y: e.pageY,
+      type: CONTEXT_MENU_TYPE.EVENT
+    }
+    uiActions.contextMenu(info)
+  }
+
+  render() {
+    return (
+      <div
+        className={cls('item-title-wrap f--hlc', {
+          active: this.state.isActive
+        })}
+        onClick={this.onSelectBlock}
+        onContextMenu={this.onContextMenu}
+      >
+        <div className="title flex-1">{this.state.name}</div>
+        <JengaCite eventNodeId={this.props.eventNodeId} />
+      </div>
+    )
+  }
+}

+ 65 - 0
coms/jenga/panel/JengaPanel.jsx

@@ -0,0 +1,65 @@
+import React from 'react'
+import uiStores from 'stores/ui'
+// import JengaExtra from '../extra/JengaExtra'
+import JengaItem from './JengaItem'
+
+const minWidth = 858 // 最小宽度
+
+export default class JengaPanel extends React.Component {
+  constructor(props) {
+    super(props)
+    this.jengaContent = React.createRef()
+
+    this.onUiChange = this.onUiChange.bind(this)
+    this.setWidth = this.setWidth.bind(this)
+  }
+
+  componentDidMount() {
+    this.setWidth(uiStores.jengaViewWidthDelta)
+    this.unsubscribe = uiStores.listen(this.onUiChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribe()
+  }
+
+  onUiChange(status) {
+    if (status && status.types) {
+      if (status.types.includes('changeJengaViewWidthDelta')) {
+        this.setWidth(status.data.jengaViewWidthDelta)
+      }
+    }
+  }
+
+  setWidth(delta) {
+    if (this.jengaContent && this.jengaContent.current) {
+      this.jengaContent.current.style.width = minWidth + delta + 'px'
+    }
+  }
+
+  render() {
+    return (
+      <div className="jenga-panel scroll-hide-bar">
+        {/* {this.props.curEventNodeList ? undefined : <JengaExtra />} */}
+        <div className="jenga-content" ref={this.jengaContent}>
+          {this.props.curEventNodeList ? (
+            this.props.curEventNodeList.map((nodeId, index) => {
+              let visibleBlockIds = this.props.filterBlockIdsMap
+                ? this.props.filterBlockIdsMap[nodeId] || []
+                : undefined
+              return (
+                <JengaItem
+                  key={index}
+                  eventNodeId={nodeId}
+                  visibleBlockIds={visibleBlockIds}
+                />
+              )
+            })
+          ) : (
+            <JengaItem />
+          )}
+        </div>
+      </div>
+    )
+  }
+}

+ 5 - 0
coms/jenga/utils.js

@@ -0,0 +1,5 @@
+import React from 'react'
+
+const BlockContext = React.createContext()
+
+export default BlockContext

+ 91 - 0
index.jsx

@@ -0,0 +1,91 @@
+import React from 'react'
+import newEventStores from 'stores/newEvent'
+import uiStores from 'stores/ui'
+import cls from 'classnames'
+import EventCodeJenga from './coms/EventJenga'
+
+export default class NewEventView extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      visible: false,
+      eventPanelMode: null,
+      sidebarExpand: true,
+      curEventNodeList: newEventStores.curEventNodeList,
+      filterBlockIdsMap: newEventStores.filterBlockIdsMap
+    }
+    this.onEventChange = this.onEventChange.bind(this)
+    this.onUIChange = this.onUIChange.bind(this)
+  }
+
+  componentDidMount() {
+    this.unsubscribeEvent = newEventStores.listen(this.onEventChange)
+    this.unUISubscribe = uiStores.listen(this.onUIChange)
+  }
+
+  componentWillUnmount() {
+    this.unsubscribeEvent()
+    this.unUISubscribe()
+  }
+
+  onEventChange(status) {
+    let obj = {}
+    if (status.data && status.data.showEventView !== undefined) {
+      if (this.state.visible !== status.data.showEventView) {
+        obj.visible = status.data.showEventView
+      }
+      if (this.state.eventPanelMode !== status.data.eventPanelMode) {
+        obj.eventPanelMode = status.data.eventPanelMode
+      }
+      if (this.state.curEventNodeList !== status.data.curEventNodeList) {
+        obj.curEventNodeList = status.data.curEventNodeList
+      }
+      if (this.state.filterBlockIdsMap !== status.data.filterBlockIdsMap) {
+        obj.filterBlockIdsMap = status.data.filterBlockIdsMap
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  onUIChange(status) {
+    let obj = {}
+    if (status.types) {
+      if (status.types.includes('sidebarToggle')) {
+        if (this.state.sidebarExpand !== status.data.sidebarExpand) {
+          obj.sidebarExpand = status.data.sidebarExpand
+        }
+      }
+    }
+    if (Object.keys(obj).length > 0) {
+      this.setState(obj)
+    }
+  }
+
+  render() {
+    let {
+      visible,
+      eventPanelMode,
+      sidebarExpand,
+      curEventNodeList,
+      filterBlockIdsMap
+    } = this.state
+    return visible && eventPanelMode ? (
+      <div
+        className={cls('event-view jenga-mode', {
+          'sidebar-collapse': !sidebarExpand
+        })}
+      >
+        {eventPanelMode === 'citedJengas' ? (
+          <EventCodeJenga
+            curEventNodeList={curEventNodeList}
+            filterBlockIdsMap={filterBlockIdsMap}
+          />
+        ) : eventPanelMode === 'jenga' ? (
+          <EventCodeJenga />
+        ) : null}
+      </div>
+    ) : null
+  }
+}