/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'jquery-ui/ui/widgets/draggable'
import 'jquery-ui/ui/widgets/droppable'
import 'jquery-ui/ui/widgets/resizable'
import Vue from 'vue'
import _ from 'lodash'
import i18n from '@/module/i18n'
import { jsPlumb } from 'jsplumb'
import DragZoom from './dragZoom'
import store from '@/conf/home/store'
import router from '@/conf/home/router'
import { uuid, findComponentDownward } from '@/module/util/'
import {
tasksAll,
rtTasksTpl,
setSvgColor,
saveTargetarr,
rtTargetarrArr,
computeScale
} from './util'
import mStart from '@/conf/home/pages/projects/pages/definition/pages/list/_source/start'
import multiDrag from './multiDrag'
const JSP = function () {
this.dag = {}
this.selectedElement = {}
this.config = {
// Whether to drag
isDrag: true,
// Whether to allow connection
isAttachment: false,
// Whether to drag a new node
isNewNodes: true,
// Whether to support double-click node events
isDblclick: true,
// Whether to support right-click menu events
isContextmenu: true,
// Whether to allow click events
isClick: false
}
}
/**
* dag init
*/
JSP.prototype.init = function ({ dag, instance, options }) {
// Get the dag component instance
this.dag = dag
// Get jsplumb instance
this.JspInstance = instance
// Get JSP options
this.options = options || {}
// Register jsplumb connection type and configuration
this.JspInstance.registerConnectionType('basic', {
anchor: 'Continuous',
connector: 'Bezier' // Line type
})
// Initial configuration
this.setConfig({
isDrag: !store.state.dag.isDetails,
isAttachment: false,
isNewNodes: !store.state.dag.isDetails, // Permissions.getAuth() === false ? false : !store.state.dag.isDetails,
isDblclick: true,
isContextmenu: true,
isClick: false
})
// Monitor line click
this.JspInstance.bind('click', e => {
// Untie event
if (this.config.isClick) {
this.connectClick(e)
} else {
findComponentDownward(this.dag.$root, 'dag-chart')._createLineLabel({id: e._jsPlumb.overlays.label.canvas.id, sourceId: e.sourceId, targetId: e.targetId})
}
})
// Drag and drop
if (this.config.isNewNodes) {
DragZoom.init()
}
// support multi drag
multiDrag()
}
/**
* set config attribute
*/
JSP.prototype.setConfig = function (o) {
this.config = Object.assign(this.config, {}, o)
}
/**
* Node binding event
*/
JSP.prototype.tasksEvent = function (selfId) {
const tasks = $(`#${selfId}`)
// Bind right event
tasks.on('contextmenu', e => {
this.tasksContextmenu(e)
return false
})
// Binding double click event
tasks.find('.icos').bind('dblclick', e => {
this.tasksDblclick(e)
})
// Binding click event
tasks.on('click', e => {
this.tasksClick(e)
})
}
/**
* Dag node drag and drop processing
*/
JSP.prototype.draggable = function () {
if (this.config.isNewNodes) {
let selfId
const self = this
$('.toolbar-btn .roundedRect').draggable({
scope: 'plant',
helper: 'clone',
containment: $('.dag-model'),
stop: function (e, ui) {
},
drag: function () {
$('body').find('.tooltip.fade.top.in').remove()
}
})
$('#canvas').droppable({
scope: 'plant',
drop: function (ev, ui) {
let id = 'tasks-' + Math.ceil(Math.random() * 100000) // eslint-disable-line
let scale = computeScale($(this))
scale = scale || 1
// Get mouse coordinates and after scale coordinate
const left = parseInt(ui.offset.left - $(this).offset().left) / scale
const top = parseInt(ui.offset.top - $(this).offset().top) / scale
// Generate template node
$('#canvas').append(rtTasksTpl({
id: id,
name: id,
x: left,
y: top,
isAttachment: self.config.isAttachment,
taskType: findComponentDownward(self.dag.$root, 'dag-chart').dagBarId
}))
// Get the generated node
const thisDom = jsPlumb.getSelector('.statemachine-demo .w')
// Generating a connection node
self.JspInstance.batch(() => {
self.initNode(thisDom[thisDom.length - 1])
})
selfId = id
self.tasksEvent(selfId)
// Dom structure is not generated without pop-up form form
if ($(`#${selfId}`).html()) {
// dag event
findComponentDownward(self.dag.$root, 'dag-chart')._createNodes({
id: selfId
})
}
}
})
}
}
/**
* Echo json processing and old data structure processing
*/
JSP.prototype.jsonHandle = function ({ largeJson, locations }) {
_.map(largeJson, v => {
// Generate template
$('#canvas').append(rtTasksTpl({
id: v.id,
name: v.name,
x: locations[v.id].x,
y: locations[v.id].y,
targetarr: locations[v.id].targetarr,
isAttachment: this.config.isAttachment,
taskType: v.type,
runFlag: v.runFlag,
nodenumber: locations[v.id].nodenumber,
successNode: v.conditionResult === undefined? '' : v.conditionResult.successNode[0],
failedNode: v.conditionResult === undefined? '' : v.conditionResult.failedNode[0]
}))
// contextmenu event
$(`#${v.id}`).on('contextmenu', e => {
this.tasksContextmenu(e)
return false
})
// dblclick event
$(`#${v.id}`).find('.icos').bind('dblclick', e => {
this.tasksDblclick(e)
})
// click event
$(`#${v.id}`).bind('click', e => {
this.tasksClick(e)
})
})
}
/**
* Initialize a single node
*/
JSP.prototype.initNode = function (el) {
// Whether to drag
if (this.config.isDrag) {
this.JspInstance.draggable(el, {
containment: 'dag-container'
})
}
// Node attribute configuration
this.JspInstance.makeSource(el, {
filter: '.ep',
anchor: 'Continuous',
connectorStyle: {
stroke: '#2d8cf0',
strokeWidth: 2,
outlineStroke: 'transparent',
outlineWidth: 4
},
// This place is leaking
// connectionType: "basic",
extract: {
action: 'the-action'
},
maxConnections: -1
})
// Node connection property configuration
this.JspInstance.makeTarget(el, {
dropOptions: { hoverClass: 'dragHover' },
anchor: 'Continuous',
allowLoopback: false // Forbid yourself to connect yourself
})
this.JspInstance.fire('jsPlumbDemoNodeAdded', el)
}
/**
* Node right click menu
*/
JSP.prototype.tasksContextmenu = function (event) {
if (this.config.isContextmenu) {
const routerName = router.history.current.name
// state
const isOne = routerName === 'projects-definition-details' && this.dag.releaseState !== 'NOT_RELEASE'
// hide
const isTwo = store.state.dag.isDetails
const html = [
`${i18n.$t('Start')}`,
`${i18n.$t('Edit')}`,
`${i18n.$t('Copy')}`,
`${i18n.$t('Delete')}`
]
const operationHtml = () => {
return html.splice(',')
}
const e = event
const $id = e.currentTarget.id
const $contextmenu = $('#contextmenu')
const $name = $(`#${$id}`).find('.name-p').text()
const $left = e.pageX + document.body.scrollLeft - 5
const $top = e.pageY + document.body.scrollTop - 5
$contextmenu.css({
left: $left,
top: $top,
visibility: 'visible'
})
// Action bar
$contextmenu.html('').append(operationHtml)
if (isOne) {
// start run
$('#startRunning').on('click', () => {
const name = store.state.dag.name
const id = router.history.current.params.id
store.dispatch('dag/getStartCheck', { processDefinitionId: id }).then(res => {
const modal = Vue.$modal.dialog({
closable: false,
showMask: true,
escClose: true,
className: 'v-modal-custom',
transitionName: 'opacityp',
render (h) {
return h(mStart, {
on: {
onUpdate () {
modal.remove()
},
close () {
modal.remove()
}
},
props: {
item: {
id: id,
name: name
},
startNodeList: $name,
sourceType: 'contextmenu'
}
})
}
})
}).catch(e => {
Vue.$message.error(e.msg || '')
})
})
}
if (!isTwo) {
// edit node
$('#editNodes').click(ev => {
findComponentDownward(this.dag.$root, 'dag-chart')._createNodes({
id: $id,
type: $(`#${$id}`).attr('data-tasks-type')
})
})
// delete node
$('#removeNodes').click(ev => {
this.removeNodes($id)
})
// copy node
$('#copyNodes').click(res => {
this.copyNodes($id)
})
}
}
}
/**
* Node double click event
*/
JSP.prototype.tasksDblclick = function (e) {
// Untie event
if (this.config.isDblclick) {
const id = $(e.currentTarget.offsetParent).attr('id')
findComponentDownward(this.dag.$root, 'dag-chart')._createNodes({
id: id,
type: $(`#${id}`).attr('data-tasks-type')
})
}
}
/**
* Node click event
*/
JSP.prototype.tasksClick = function (e) {
let $id
const self = this
const $body = $('body')
if (this.config.isClick) {
const $connect = this.selectedElement.connect
$('.w').removeClass('jtk-tasks-active')
$(e.currentTarget).addClass('jtk-tasks-active')
if ($connect) {
setSvgColor($connect, '#2d8cf0')
this.selectedElement.connect = null
}
this.selectedElement.id = $(e.currentTarget).attr('id')
// Unbind copy and paste events
$body.unbind('copy').unbind('paste')
// Copy binding id
$id = self.selectedElement.id
$body.bind({
copy: function () {
$id = self.selectedElement.id
},
paste: function () {
$id && self.copyNodes($id)
}
})
}
}
/**
* Remove binding events
* paste
*/
JSP.prototype.removePaste = function () {
const $body = $('body')
// Unbind copy and paste events
$body.unbind('copy').unbind('paste')
// Remove selected node parameters
this.selectedElement.id = null
// Remove node selection effect
$('.w').removeClass('jtk-tasks-active')
}
/**
* Line click event
*/
JSP.prototype.connectClick = function (e) {
// Set svg color
setSvgColor(e, '#0097e0')
const $id = this.selectedElement.id
if ($id) {
$(`#${$id}`).removeClass('jtk-tasks-active')
this.selectedElement.id = null
}
this.selectedElement.connect = e
}
/**
* toolbarEvent
* @param {Pointer}
*/
JSP.prototype.handleEventPointer = function (is) {
this.setConfig({
isClick: is,
isAttachment: false
})
}
/**
* toolbarEvent
* @param {Line}
*/
JSP.prototype.handleEventLine = function (is) {
const wDom = $('.w')
this.setConfig({
isAttachment: is
})
is ? wDom.addClass('jtk-ep') : wDom.removeClass('jtk-ep')
}
/**
* toolbarEvent
* @param {Remove}
*/
JSP.prototype.handleEventRemove = function () {
const $id = this.selectedElement.id || null
const $connect = this.selectedElement.connect || null
if ($id) {
this.removeNodes(this.selectedElement.id)
} else {
this.removeConnect($connect)
}
// Monitor whether to edit DAG
store.commit('dag/setIsEditDag', true)
}
/**
* Delete node
*/
JSP.prototype.removeNodes = function ($id) {
// Delete node processing(data-targetarr)
_.map(tasksAll(), v => {
const targetarr = v.targetarr.split(',')
if (targetarr.length) {
const newArr = _.filter(targetarr, v1 => v1 !== $id)
$(`#${v.id}`).attr('data-targetarr', newArr.toString())
}
})
// delete node
this.JspInstance.remove($id)
// delete dom
$(`#${$id}`).remove()
// callback onRemoveNodes event
this.options && this.options.onRemoveNodes && this.options.onRemoveNodes($id)
let connects = []
_.map(this.JspInstance.getConnections(), v => {
connects.push({
endPointSourceId: v.sourceId,
endPointTargetId: v.targetId,
label: v._jsPlumb.overlays.label.canvas.innerText
})
})
// Storage line dependence
store.commit('dag/setConnects', connects)
}
/**
* Delete connection
*/
JSP.prototype.removeConnect = function ($connect) {
if (!$connect) {
return
}
// Remove connections and remove node and node dependencies
const targetId = $connect.targetId
const sourceId = $connect.sourceId
let targetarr = rtTargetarrArr(targetId)
if (targetarr.length) {
targetarr = _.filter(targetarr, v => v !== sourceId)
$(`#${targetId}`).attr('data-targetarr', targetarr.toString())
}
if ($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS') {
$(`#${sourceId}`).attr('data-nodenumber', Number($(`#${sourceId}`).attr('data-nodenumber')) - 1)
}
this.JspInstance.deleteConnection($connect)
this.selectedElement = {}
}
/**
* Copy node
*/
JSP.prototype.copyNodes = function ($id) {
let newNodeInfo = _.cloneDeep(_.find(store.state.dag.tasks, v => v.id === $id))
const newNodePors = store.state.dag.locations[$id]
// Unstored nodes do not allow replication
if (!newNodePors) {
return
}
// Generate random id
const newUuId = `${uuid() + uuid()}`
const id = newNodeInfo.id.length > 8 ? newNodeInfo.id.substr(0, 7) : newNodeInfo.id
const name = newNodeInfo.name.length > 8 ? newNodeInfo.name.substr(0, 7) : newNodeInfo.name
// new id
const newId = `${id || ''}-${newUuId}`
// new name
const newName = `${name || ''}-${newUuId}`
// coordinate x
const newX = newNodePors.x + 100
// coordinate y
const newY = newNodePors.y + 40
// Generate template node
$('#canvas').append(rtTasksTpl({
id: newId,
name: newName,
x: newX,
y: newY,
isAttachment: this.config.isAttachment,
taskType: newNodeInfo.type
}))
// Get the generated node
const thisDom = jsPlumb.getSelector('.statemachine-demo .w')
// Copy node information
newNodeInfo = Object.assign(newNodeInfo, {
id: newId,
name: newName
})
// Add new node
store.commit('dag/addTasks', newNodeInfo)
// Add node location information
store.commit('dag/addLocations', {
[newId]: {
name: newName,
targetarr: '',
nodenumber: 0,
x: newX,
y: newY
}
})
// Generating a connection node
this.JspInstance.batch(() => {
this.initNode(thisDom[thisDom.length - 1])
// Add events to nodes
this.tasksEvent(newId)
})
}
/**
* toolbarEvent
* @param {Screen}
*/
JSP.prototype.handleEventScreen = function ({ item, is }) {
let screenOpen = true
if (is) {
item.icon = 'ans-icon-min'
screenOpen = true
} else {
item.icon = 'ans-icon-max'
screenOpen = false
}
const $mainLayoutModel = $('.main-layout-model')
if (screenOpen) {
$mainLayoutModel.addClass('dag-screen')
} else {
$mainLayoutModel.removeClass('dag-screen')
}
}
/**
* save task
* @param tasks
* @param locations
* @param connects
*/
JSP.prototype.saveStore = function () {
return new Promise((resolve, reject) => {
const connects = []
const locations = {}
const tasks = []
const is = (id) => {
return !!_.filter(tasksAll(), v => v.id === id).length
}
// task
_.map(_.cloneDeep(store.state.dag.tasks), v => {
if (is(v.id)) {
const preTasks = []
const id = $(`#${v.id}`)
const tar = id.attr('data-targetarr')
const idDep = tar ? id.attr('data-targetarr').split(',') : []
if (idDep.length) {
_.map(idDep, v1 => {
preTasks.push($(`#${v1}`).find('.name-p').text())
})
}
let tasksParam = _.assign(v, {
preTasks: preTasks
})
// Sub-workflow has no retries and interval
if (v.type === 'SUB_PROCESS') {
tasksParam = _.omit(tasksParam, ['maxRetryTimes', 'retryInterval'])
}
tasks.push(tasksParam)
}
})
if(store.state.dag.connects.length ===this.JspInstance.getConnections().length) {
_.map(store.state.dag.connects, u => {
connects.push({
endPointSourceId: u.endPointSourceId,
endPointTargetId: u.endPointTargetId,
label: u.label
})
})
} else if(store.state.dag.connects.length>0 && store.state.dag.connects.length < this.JspInstance.getConnections().length) {
_.map(this.JspInstance.getConnections(), v => {
connects.push({
endPointSourceId: v.sourceId,
endPointTargetId: v.targetId,
label: v._jsPlumb.overlays.label.canvas.innerText
})
})
_.map(store.state.dag.connects, u => {
_.map(connects, v => {
if(u.label && u.endPointSourceId === v.endPointSourceId && u.endPointTargetId===v.endPointTargetId) {
v.label = u.label
}
})
})
} else if(store.state.dag.connects.length===0) {
_.map(this.JspInstance.getConnections(), v => {
connects.push({
endPointSourceId: v.sourceId,
endPointTargetId: v.targetId,
label: v._jsPlumb.overlays.label.canvas.innerText
})
})
}
_.map(tasksAll(), v => {
locations[v.id] = {
name: v.name,
targetarr: v.targetarr,
nodenumber: v.nodenumber,
x: v.x,
y: v.y
}
})
// Storage node
store.commit('dag/setTasks', tasks)
// Store coordinate information
store.commit('dag/setLocations', locations)
// Storage line dependence
store.commit('dag/setConnects', connects)
resolve({
connects: connects,
tasks: tasks,
locations: locations
})
})
}
/**
* Event processing
*/
JSP.prototype.handleEvent = function () {
this.JspInstance.bind('beforeDrop', function (info) {
const sourceId = info.sourceId// 出
const targetId = info.targetId// 入
/**
* Recursive search for nodes
*/
let recursiveVal
const recursiveTargetarr = (arr, targetId) => {
for (const i in arr) {
if (arr[i] === targetId) {
recursiveVal = targetId
} else {
recursiveTargetarr(rtTargetarrArr(arr[i]), targetId)
}
}
return recursiveVal
}
// Connection to connected nodes is not allowed
if (_.findIndex(rtTargetarrArr(targetId), v => v === sourceId) !== -1) {
return false
}
// Recursive form to find if the target Targetarr has a sourceId
if (recursiveTargetarr(rtTargetarrArr(sourceId), targetId)) {
return false
}
if ($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS' && $(`#${sourceId}`).attr('data-nodenumber') === 2) {
return false
} else {
$(`#${sourceId}`).attr('data-nodenumber', Number($(`#${sourceId}`).attr('data-nodenumber')) + 1)
}
// Storage node dependency information
saveTargetarr(sourceId, targetId)
// Monitor whether to edit DAG
store.commit('dag/setIsEditDag', true)
return true
})
}
/**
* Backfill data processing
*/
JSP.prototype.jspBackfill = function ({ connects, locations, largeJson }) {
// Backfill nodes
this.jsonHandle({
largeJson: largeJson,
locations: locations
})
const wNodes = jsPlumb.getSelector('.statemachine-demo .w')
// Backfill line
this.JspInstance.batch(() => {
for (let i = 0; i < wNodes.length; i++) {
this.initNode(wNodes[i])
}
_.map(connects, v => {
let sourceId = v.endPointSourceId.split('-')
let targetId = v.endPointTargetId.split('-')
let labels = v.label
if (sourceId.length === 4 && targetId.length === 4) {
sourceId = `${sourceId[0]}-${sourceId[1]}-${sourceId[2]}`
targetId = `${targetId[0]}-${targetId[1]}-${targetId[2]}`
} else {
sourceId = v.endPointSourceId
targetId = v.endPointTargetId
}
if($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS' && $(`#${sourceId}`).attr('data-successnode') === $(`#${targetId}`).find('.name-p').text()) {
this.JspInstance.connect({
source: sourceId,
target: targetId,
type: 'basic',
paintStyle: { strokeWidth: 2, stroke: '#4caf50' },
HoverPaintStyle: {stroke: '#ccc', strokeWidth: 3},
overlays:[["Label", { label: labels} ]]
})
} else if($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS' && $(`#${sourceId}`).attr('data-failednode') === $(`#${targetId}`).find('.name-p').text()) {
this.JspInstance.connect({
source: sourceId,
target: targetId,
type: 'basic',
paintStyle: { strokeWidth: 2, stroke: '#252d39' },
HoverPaintStyle: {stroke: '#ccc', strokeWidth: 3},
overlays:[["Label", { label: labels} ]]
})
} else {
this.JspInstance.connect({
source: sourceId,
target: targetId,
type: 'basic',
paintStyle: { strokeWidth: 2, stroke: '#2d8cf0' },
HoverPaintStyle: {stroke: '#ccc', strokeWidth: 3},
overlays:[["Label", { label: labels} ]]
})
}
})
})
jsPlumb.fire('jsPlumbDemoLoaded', this.JspInstance)
// Connection monitoring
this.handleEvent()
// Drag and drop new nodes
this.draggable()
}
export default new JSP()