allow connecting to entire node

This commit is contained in:
Mutasem 2021-11-03 14:20:40 +01:00
parent 6859f85b9b
commit 42a5217893
4 changed files with 106 additions and 36 deletions

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="node-wrapper" :style="nodePosition"> <div class="node-wrapper" :style="nodePosition">
<div :class="{'selected': true, 'has-subtitles': !!nodeSubtitle}" v-show="isSelected"></div> <div :class="{'selected': true, 'has-subtitles': !!nodeSubtitle}" v-show="isSelected"></div>
<div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd"> <div class="node-default" :data-name="data.name" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
<div v-if="hasIssues" class="node-info-icon node-issues"> <div v-if="hasIssues" class="node-info-icon node-issues">
<n8n-tooltip placement="top" > <n8n-tooltip placement="top" >
<div slot="content" v-html="nodeIssues"></div> <div slot="content" v-html="nodeIssues"></div>
@ -23,7 +23,7 @@
<div class="node-executing-info" title="Node is executing"> <div class="node-executing-info" title="Node is executing">
<font-awesome-icon icon="sync-alt" spin /> <font-awesome-icon icon="sync-alt" spin />
</div> </div>
<div class="node-options no-select-on-click" v-if="!isReadOnly"> <div class="node-options no-select-on-click" v-if="!isReadOnly" v-show="!hideActions">
<div v-touch:tap="deleteNode" class="option" title="Delete Node" > <div v-touch:tap="deleteNode" class="option" title="Delete Node" >
<font-awesome-icon icon="trash" /> <font-awesome-icon icon="trash" />
</div> </div>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="node-icon-wrapper" :style="iconStyleData"> <div class="node-icon-wrapper" :style="iconStyleData">
<div v-if="nodeIconData !== null" class="icon"> <div v-if="nodeIconData !== null" class="icon">
<img v-if="nodeIconData.type === 'file'" :src="nodeIconData.fileBuffer || nodeIconData.path" style="max-width: 100%; max-height: 100%;" /> <img v-if="nodeIconData.type === 'file'" :src="nodeIconData.fileBuffer || nodeIconData.path" :style="imageStyleData" />
<font-awesome-icon v-else :icon="nodeIconData.icon || nodeIconData.path" :style="fontStyleData" /> <font-awesome-icon v-else :icon="nodeIconData.icon || nodeIconData.path" :style="fontStyleData" />
</div> </div>
<div v-else class="node-icon-placeholder"> <div v-else class="node-icon-placeholder">
@ -12,25 +12,37 @@
<script lang="ts"> <script lang="ts">
import { IVersionNode } from '@/Interface';
import { INodeTypeDescription } from 'n8n-workflow';
import Vue from 'vue'; import Vue from 'vue';
interface NodeIconData { interface NodeIconData {
type: string; type: string;
path: string; path?: string;
fileExtension?: string; fileExtension?: string;
fileBuffer?: string;
} }
export default Vue.extend({ export default Vue.extend({
name: 'NodeIcon', name: 'NodeIcon',
props: [ props: {
'nodeType', nodeType: {},
'size', size: {
'disabled', type: Number,
'circle', },
], disabled: {
type: Boolean,
default: false,
},
circle: {
type: Boolean,
default: false,
},
},
computed: { computed: {
iconStyleData (): object { iconStyleData (): object {
const color = this.disabled ? '#ccc' : this.nodeType.defaults && this.nodeType.defaults.color; const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
const color = this.disabled ? '#ccc' : (nodeType ? nodeType.defaults && nodeType!.defaults.color: '');
if (!this.size) { if (!this.size) {
return {color}; return {color};
} }
@ -49,6 +61,12 @@ export default Vue.extend({
'max-width': this.size + 'px', 'max-width': this.size + 'px',
}; };
}, },
imageStyleData (): object {
return {
'max-width': '100%',
'max-height': '100%',
};
},
isSvgIcon (): boolean { isSvgIcon (): boolean {
if (this.nodeIconData && this.nodeIconData.type === 'file' && this.nodeIconData.fileExtension === 'svg') { if (this.nodeIconData && this.nodeIconData.type === 'file' && this.nodeIconData.fileExtension === 'svg') {
return true; return true;
@ -56,26 +74,27 @@ export default Vue.extend({
return false; return false;
}, },
nodeIconData (): null | NodeIconData { nodeIconData (): null | NodeIconData {
if (this.nodeType === null) { const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
if (nodeType === null) {
return null; return null;
} }
if (this.nodeType.iconData) { if ((nodeType as IVersionNode).iconData) {
return this.nodeType.iconData; return (nodeType as IVersionNode).iconData;
} }
const restUrl = this.$store.getters.getRestUrl; const restUrl = this.$store.getters.getRestUrl;
if (this.nodeType.icon) { if (nodeType.icon) {
let type, path; let type, path;
[type, path] = this.nodeType.icon.split(':'); [type, path] = nodeType.icon.split(':');
const returnData: NodeIconData = { const returnData: NodeIconData = {
type, type,
path, path,
}; };
if (type === 'file') { if (type === 'file') {
returnData.path = restUrl + '/node-icon/' + this.nodeType.name; returnData.path = restUrl + '/node-icon/' + nodeType.name;
returnData.fileExtension = path.split('.').slice(-1).join(); returnData.fileExtension = path.split('.').slice(-1).join();
} }

View file

@ -82,13 +82,17 @@ export const nodeBase = mixins(
nodeIndex (): string { nodeIndex (): string {
return this.$store.getters.getNodeIndex(this.data.name).toString(); return this.$store.getters.getNodeIndex(this.data.name).toString();
}, },
position (): XYPosition {
const node = this.$store.getters.nodesByName[this.name] as INodeUi; // position responsive to store changes
return node.position;
},
nodePosition (): object { nodePosition (): object {
const node = this.$store.getters.nodesByName[this.name]; // position responsive to store changes
const returnStyles: { const returnStyles: {
[key: string]: string; [key: string]: string;
} = { } = {
left: node.position[0] + 'px', left: this.position[0] + 'px',
top: node.position[1] + 'px', top: this.position[1] + 'px',
}; };
return returnStyles; return returnStyles;
@ -100,6 +104,7 @@ export const nodeBase = mixins(
'instance', 'instance',
'isReadOnly', 'isReadOnly',
'isActive', 'isActive',
'hideActions',
], ],
methods: { methods: {
__addNode (node: INodeUi) { __addNode (node: INodeUi) {

View file

@ -29,6 +29,7 @@
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
:instance="instance" :instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name" :isActive="!!activeNode && activeNode.name === nodeData.name"
:hideActions="pullConnActive"
></node> ></node>
</div> </div>
</div> </div>
@ -309,7 +310,10 @@ export default mixins(
stopExecutionInProgress: false, stopExecutionInProgress: false,
blankRedirect: false, blankRedirect: false,
credentialsUpdated: false, credentialsUpdated: false,
newNodeInsertPosition: null as null | XYPosition, newNodeInsertPosition: null as XYPosition | null,
pullConnActiveNodeName: null as string | null,
pullConnActive: false,
dropPrevented: false,
}; };
}, },
beforeDestroy () { beforeDestroy () {
@ -1191,7 +1195,21 @@ export default mixins(
return newNodeData; return newNodeData;
}, },
getConnection (sourceNodeName: string, sourceNodeOutputIndex: number, targetNodeName: string, targetNodeOuputIndex: number): IConnection | undefined {
const nodeConnections = (this.$store.getters.outgoingConnectionsByNodeName(sourceNodeName) as INodeConnections).main;
if (nodeConnections) {
const connections: IConnection[] = nodeConnections[sourceNodeOutputIndex];
return connections.find((connection: IConnection) => connection.node === targetNodeName && connection.index === targetNodeOuputIndex);
}
return undefined;
},
connectTwoNodes (sourceNodeName: string, sourceNodeOutputIndex: number, targetNodeName: string, targetNodeOuputIndex: number) { connectTwoNodes (sourceNodeName: string, sourceNodeOutputIndex: number, targetNodeName: string, targetNodeOuputIndex: number) {
if (this.getConnection(sourceNodeName, sourceNodeOutputIndex, targetNodeName, targetNodeOuputIndex)) {
return;
}
const connectionData = [ const connectionData = [
{ {
node: sourceNodeName, node: sourceNodeName,
@ -1266,17 +1284,25 @@ export default mixins(
this.openNodeCreator(info.eventSource); this.openNodeCreator(info.eventSource);
}; };
let dropPrevented = false; this.instance.bind('connectionAborted', (connection) => {
this.pullConnActive = false;
this.instance.bind('connectionAborted', (info) => { if (this.dropPrevented) {
if (dropPrevented) { this.dropPrevented = false;
dropPrevented = false; return;
}
if (this.pullConnActiveNodeName) {
const sourceNodeName = this.$store.getters.getNodeNameByIndex(connection.sourceId.slice(NODE_NAME_PREFIX.length));
const outputIndex = connection.getParameters().index;
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
return; return;
} }
insertNodeAfterSelected({ insertNodeAfterSelected({
sourceId: info.sourceId, sourceId: connection.sourceId,
index: info.getParameters().index, index: connection.getParameters().index,
eventSource: 'node_connection_drop', eventSource: 'node_connection_drop',
}); });
}); });
@ -1290,8 +1316,8 @@ export default mixins(
const targetNodeName = this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex); const targetNodeName = this.$store.getters.getNodeNameByIndex(targetInfo.nodeIndex);
// check for duplicates // check for duplicates
if (this.getJSPlumbConnection(sourceNodeName, sourceInfo.index, targetNodeName, targetInfo.index)) { if (this.getConnection(sourceNodeName, sourceInfo.index, targetNodeName, targetInfo.index)) {
dropPrevented = true; this.dropPrevented = true;
return false; return false;
} }
@ -1445,23 +1471,43 @@ export default mixins(
// @ts-ignore // @ts-ignore
this.instance.bind('connectionDrag', (connection: Connection) => { this.instance.bind('connectionDrag', (connection: Connection) => {
this.pullConnActive = true;
this.newNodeInsertPosition = null; this.newNodeInsertPosition = null;
CanvasHelpers.addOverlays(connection, CanvasHelpers.CONNECTOR_DROP_NODE_OVERLAY); CanvasHelpers.addOverlays(connection, CanvasHelpers.CONNECTOR_DROP_NODE_OVERLAY);
const nodes = [...document.querySelectorAll('.node-default')];
let droppable = false; const onMouseMove = (e: MouseEvent) => {
const onMouseMove = () => {
if (!connection) { if (!connection) {
return; return;
} }
const elements = document.querySelector('div.jtk-endpoint.dropHover'); const elements = document.querySelector('.jtk-endpoint.dropHover');
if (elements && !droppable) { if (elements) {
droppable = true;
CanvasHelpers.showDropConnectionState(connection); CanvasHelpers.showDropConnectionState(connection);
return;
} }
else if (!elements && droppable) {
droppable = false; const intersecting = nodes.find((element: Element) => {
const {top, left, right, bottom} = element.getBoundingClientRect();
if (top <= e.pageY && bottom >= e.pageY && left <= e.pageX && right >= e.pageX) {
const nodeName = (element as HTMLElement).dataset['name'];
const node = this.$store.getters.getNodeByName(nodeName) as INodeUi | null;
if (node) {
const nodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription;
if (nodeType.inputs.length === 1) {
this.pullConnActiveNodeName = node.name;
CanvasHelpers.showDropConnectionState(connection);
return true;
}
}
}
return false;
});
if (!intersecting) {
CanvasHelpers.showPullConnectionState(connection); CanvasHelpers.showPullConnectionState(connection);
this.pullConnActiveNodeName = null;
} }
}; };