🔀 Merge master

This commit is contained in:
Iván Ovejero
2021-11-19 15:35:38 +01:00
29 changed files with 2729 additions and 1058 deletions

View File

@@ -1,26 +1,37 @@
<template>
<div class="node-wrapper" :style="nodePosition">
<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 v-if="hasIssues" class="node-info-icon node-issues">
<n8n-tooltip placement="top" >
<div slot="content" v-html="nodeIssues"></div>
<font-awesome-icon icon="exclamation-triangle" />
</n8n-tooltip>
</div>
<el-badge v-else :hidden="workflowDataItems === 0" class="node-info-icon data-count" :value="workflowDataItems"></el-badge>
<div :class="{'node-wrapper': true, selected: isSelected}" :style="nodePosition">
<div class="select-background" v-show="isSelected"></div>
<div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name">
<div :class="nodeClass" :style="nodeStyle" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
<div v-if="!data.disabled" :class="{'node-info-icon': true, 'shift-icon': shiftOutputCount}">
<div v-if="hasIssues" class="node-issues">
<n8n-tooltip placement="bottom" >
<div slot="content" v-html="nodeIssues"></div>
<font-awesome-icon icon="exclamation-triangle" />
</n8n-tooltip>
</div>
<div v-else-if="waiting" class="waiting">
<n8n-tooltip placement="bottom">
<div slot="content" v-html="waiting"></div>
<font-awesome-icon icon="clock" />
</n8n-tooltip>
</div>
<span v-else-if="workflowDataItems" class="data-count">
<font-awesome-icon icon="check" />
<span v-if="workflowDataItems > 1" class="items-count"> {{ workflowDataItems }}</span>
</span>
</div>
<div v-if="waiting" class="node-info-icon waiting">
<n8n-tooltip placement="top">
<div slot="content" v-html="waiting"></div>
<font-awesome-icon icon="clock" />
</n8n-tooltip>
<div class="node-executing-info" title="Node is executing">
<font-awesome-icon icon="sync-alt" spin />
</div>
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" :disabled="this.data.disabled"/>
</div>
<div class="node-executing-info" :title="$baseText('node.nodeIsExecuting')">
<font-awesome-icon icon="sync-alt" spin />
</div>
<div class="node-options no-select-on-click" v-if="!isReadOnly">
<div v-touch:tap="deleteNode" class="option" :title="$baseText('node.deleteNode')">
<div class="node-options no-select-on-click" v-if="!isReadOnly" v-show="!hideActions">
<div v-touch:tap="deleteNode" class="option" :title="$baseText('node.deleteNode')" >
<font-awesome-icon icon="trash" />
</div>
<div v-touch:tap="disableNode" class="option" :title="$baseText('node.activateDeactivateNode')">
@@ -36,12 +47,12 @@
<font-awesome-icon class="execute-icon" icon="play-circle" />
</div>
</div>
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :circle="true" :shrink="true" :disabled="this.data.disabled"/>
<div :class="{'disabled-linethrough': true, success: workflowDataItems > 0}" v-if="showDisabledLinethrough"></div>
</div>
<div class="node-description">
<div class="node-name" :title="data.name">
{{data.name}}
<p>{{ nodeTitle }}</p>
<p v-if="data.disabled">(Disabled)</p>
</div>
<div v-if="nodeSubtitle !== undefined" class="node-subtitle" :title="nodeSubtitle">
{{nodeSubtitle}}
@@ -62,6 +73,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import {
INodeTypeDescription,
ITaskData,
NodeHelpers,
} from 'n8n-workflow';
@@ -70,6 +82,8 @@ import NodeIcon from '@/components/NodeIcon.vue';
import mixins from 'vue-typed-mixins';
import { get } from 'lodash';
import { getStyleTokenValue } from './helpers';
import { INodeUi, XYPosition } from '@/Interface';
export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflowHelpers).extend({
name: 'Node',
@@ -77,8 +91,17 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
NodeIcon,
},
computed: {
workflowDataItems () {
const workflowResultDataNode = this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
nodeRunData(): ITaskData[] {
return this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
},
hasIssues (): boolean {
if (this.data.issues !== undefined && Object.keys(this.data.issues).length) {
return true;
}
return false;
},
workflowDataItems (): number {
const workflowResultDataNode = this.nodeRunData;
if (workflowResultDataNode === null) {
return 0;
}
@@ -91,34 +114,12 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
nodeType (): INodeTypeDescription | null {
return this.$store.getters.nodeType(this.data.type);
},
nodeClass () {
const classes = [];
if (this.data.disabled) {
classes.push('disabled');
}
if (this.isExecuting) {
classes.push('executing');
}
if (this.workflowDataItems !== 0) {
classes.push('has-data');
}
if (this.hasIssues) {
classes.push('has-issues');
}
if (this.isTouchDevice) {
classes.push('is-touch-device');
}
if (this.isTouchActive) {
classes.push('touch-active');
}
return classes;
nodeClass (): object {
return {
'node-box': true,
disabled: this.data.disabled,
executing: this.isExecuting,
};
},
nodeIssues (): string {
if (this.data.issues === undefined) {
@@ -136,6 +137,27 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
return 'play';
}
},
position (): XYPosition {
const node = this.$store.getters.nodesByName[this.name] as INodeUi; // position responsive to store changes
return node.position;
},
showDisabledLinethrough(): boolean {
return !!(this.data.disabled && this.nodeType && this.nodeType.inputs.length === 1 && this.nodeType.outputs.length === 1);
},
nodePosition (): object {
const returnStyles: {
[key: string]: string;
} = {
left: this.position[0] + 'px',
top: this.position[1] + 'px',
};
return returnStyles;
},
nodeTitle (): string {
return this.data.name;
},
waiting (): string | undefined {
const workflowExecution = this.$store.getters.getWorkflowExecution;
@@ -155,6 +177,38 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
workflowRunning (): boolean {
return this.$store.getters.isActionActive('workflowRunning');
},
nodeStyle (): object {
let borderColor = getStyleTokenValue('--color-foreground-xdark');
if (this.data.disabled) {
borderColor = getStyleTokenValue('--color-foreground-base');
}
else if (!this.isExecuting) {
if (this.hasIssues) {
borderColor = getStyleTokenValue('--color-danger');
}
else if (this.waiting) {
borderColor = getStyleTokenValue('--color-secondary');
}
else if (this.workflowDataItems) {
borderColor = getStyleTokenValue('--color-success');
}
}
const returnStyles: {
[key: string]: string;
} = {
'border-color': borderColor,
};
return returnStyles;
},
isSelected (): boolean {
return this.$store.getters.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name);
},
shiftOutputCount (): boolean {
return !!(this.nodeType && this.nodeType.outputs.length > 2);
},
},
watch: {
isActive(newValue, oldValue) {
@@ -162,9 +216,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
this.setSubtitle();
}
},
nodeRunData(newValue) {
this.$emit('run', {name: this.data.name, data: newValue, waiting: !!this.waiting});
},
},
mounted() {
this.setSubtitle();
setTimeout(() => {
this.$emit('run', {name: this.data.name, data: this.nodeRunData, waiting: !!this.waiting});
}, 0);
},
data () {
return {
@@ -214,7 +274,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
</script>
<style lang="scss">
<style lang="scss" scoped>
.node-wrapper {
position: absolute;
@@ -222,20 +282,25 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
height: 100px;
.node-description {
line-height: 1.5;
position: absolute;
bottom: -55px;
top: 100px;
left: -50px;
width: 200px;
height: 50px;
line-height: 1.5;
text-align: center;
cursor: default;
padding: 8px;
width: 200px;
pointer-events: none; // prevent container from being draggable
.node-name {
white-space: nowrap;
overflow: hidden;
.node-name > p { // must be paragraph tag to have two lines in safari
text-overflow: ellipsis;
font-weight: 500;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
overflow-wrap: anywhere;
font-weight: var(--font-weight-bold);
line-height: var(--font-line-height-compact);
}
.node-subtitle {
@@ -249,33 +314,24 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
}
.node-default {
position: absolute;
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 25px;
text-align: center;
z-index: 24;
cursor: pointer;
color: #444;
border: 1px dashed grey;
&.has-data {
border-style: solid;
}
.node-box {
width: 100%;
height: 100%;
border: 2px solid var(--color-foreground-xdark);
border-radius: var(--border-radius-large);
background-color: var(--color-background-xlight);
&.disabled {
color: #a0a0a0;
text-decoration: line-through;
border: 1px solid #eee !important;
background-color: #eee;
}
&.executing {
background-color: $--color-primary-light !important;
&.executing {
background-color: $--color-primary-light !important;
border-color: $--color-primary !important;
.node-executing-info {
display: inline-block;
.node-executing-info {
display: inline-block;
}
}
}
@@ -306,39 +362,35 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
.node-icon {
position: absolute;
top: calc(50% - 30px);
left: calc(50% - 30px);
top: calc(50% - 20px);
left: calc(50% - 20px);
}
.node-info-icon {
position: absolute;
top: -14px;
right: 12px;
z-index: 11;
bottom: 6px;
right: 6px;
&.data-count {
&.shift-icon {
right: 12px;
}
.data-count {
font-weight: 600;
top: -12px;
color: var(--color-success);
}
&.waiting {
left: 10px;
top: -12px;
.node-issues {
color: var(--color-danger);
}
}
.node-issues {
width: 25px;
height: 25px;
font-size: 20px;
color: #ff0000;
.items-count {
font-size: var(--font-size-s);
}
}
.waiting {
width: 25px;
height: 25px;
font-size: 20px;
color: #5e5efa;
color: var(--color-secondary);
}
.node-options {
@@ -347,7 +399,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
top: -25px;
left: -10px;
width: 120px;
height: 45px;
height: 24px;
font-size: 0.9em;
text-align: left;
z-index: 10;
@@ -382,45 +434,94 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
display: initial;
}
}
}
}
&.has-data .node-options,
&.has-issues .node-options {
top: -35px;
}
.select-background {
display: block;
background-color: hsla(var(--color-foreground-base-h), var(--color-foreground-base-s), var(--color-foreground-base-l), 60%);
border-radius: var(--border-radius-xlarge);
overflow: hidden;
position: absolute;
left: -8px !important;
top: -8px !important;
height: 116px;
width: 116px !important;
}
.disabled-linethrough {
border: 1px solid var(--color-foreground-dark);
position: absolute;
top: 49px;
left: -3px;
width: 111px;
pointer-events: none;
&.success {
border-color: var(--color-success-light);
}
}
</style>
<style>
.el-badge__content {
border-width: 2px;
background-color: #67c23a;
<style lang="scss">
/** node */
.node-wrapper.selected {
z-index: 2;
}
/** connector */
.jtk-connector {
z-index:4;
z-index: 3;
}
.jtk-connector path {
transition: stroke .1s ease-in-out;
}
.jtk-connector.success {
z-index: 4;
}
/** node endpoints */
.jtk-endpoint {
z-index:5;
}
.jtk-connector.jtk-hover {
z-index: 6;
}
.disabled-linethrough {
z-index: 6;
}
.jtk-endpoint.jtk-hover {
z-index: 7;
}
.jtk-overlay {
z-index:6;
z-index:7;
}
.jtk-endpoint.dropHover {
border: 2px solid #ff2244;
.jtk-connector.jtk-dragging {
z-index: 8;
}
.jtk-drag-selected .node-default {
/* https://www.cssmatic.com/box-shadow */
-webkit-box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
-moz-box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
.jtk-endpoint.jtk-drag-active {
z-index: 9;
}
.disabled .node-icon img {
-webkit-filter: contrast(40%) brightness(1.5) grayscale(100%);
filter: contrast(40%) brightness(1.5) grayscale(100%);
.connection-actions {
z-index: 10;
}
.node-options {
z-index: 10;
}
.drop-add-node-label {
z-index: 10;
}
</style>

View File

@@ -108,7 +108,7 @@ export default mixins(
credentialTypesNodeDescription (): INodeCredentialDescription[] {
const node = this.node as INodeUi;
const activeNodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription;
const activeNodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
if (activeNodeType && activeNodeType.credentials) {
return activeNodeType.credentials;
}

View File

@@ -1,8 +1,8 @@
<template>
<div class="node-icon-wrapper" :style="iconStyleData" :class="{shrink: isSvgIcon && shrink, full: !shrink}">
<div class="node-icon-wrapper" :style="iconStyleData">
<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%;" />
<font-awesome-icon v-else :icon="nodeIconData.icon || nodeIconData.path" />
<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" />
</div>
<div v-else class="node-icon-placeholder">
{{nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
@@ -12,39 +12,65 @@
<script lang="ts">
import { IVersionNode } from '@/Interface';
import { INodeTypeDescription } from 'n8n-workflow';
import Vue from 'vue';
interface NodeIconData {
type: string;
path: string;
path?: string;
fileExtension?: string;
fileBuffer?: string;
}
export default Vue.extend({
name: 'NodeIcon',
props: [
'nodeType',
'size',
'shrink',
'disabled',
'circle',
],
props: {
nodeType: {},
size: {
type: Number,
},
disabled: {
type: Boolean,
default: false,
},
circle: {
type: Boolean,
default: false,
},
},
computed: {
iconStyleData (): object {
const color = this.disabled ? '#ccc' : this.nodeType.defaults && this.nodeType.defaults.color;
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
const color = nodeType ? nodeType.defaults && nodeType!.defaults.color : '';
if (!this.size) {
return {color};
}
const size = parseInt(this.size, 10);
return {
color,
width: size + 'px',
height: size + 'px',
'font-size': Math.floor(parseInt(this.size, 10) * 0.6) + 'px',
'line-height': size + 'px',
'border-radius': this.circle ? '50%': '4px',
width: this.size + 'px',
height: this.size + 'px',
'font-size': this.size + 'px',
'line-height': this.size + 'px',
'border-radius': this.circle ? '50%': '2px',
...(this.disabled && {
color: '#ccc',
'-webkit-filter': 'contrast(40%) brightness(1.5) grayscale(100%)',
'filter': 'contrast(40%) brightness(1.5) grayscale(100%)',
}),
};
},
fontStyleData (): object {
return {
'max-width': this.size + 'px',
};
},
imageStyleData (): object {
return {
width: '100%',
'max-width': '100%',
'max-height': '100%',
};
},
isSvgIcon (): boolean {
@@ -54,26 +80,27 @@ export default Vue.extend({
return false;
},
nodeIconData (): null | NodeIconData {
if (this.nodeType === null) {
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
if (nodeType === null) {
return null;
}
if (this.nodeType.iconData) {
return this.nodeType.iconData;
if ((nodeType as IVersionNode).iconData) {
return (nodeType as IVersionNode).iconData;
}
const restUrl = this.$store.getters.getRestUrl;
if (this.nodeType.icon) {
if (nodeType.icon) {
let type, path;
[type, path] = this.nodeType.icon.split(':');
[type, path] = nodeType.icon.split(':');
const returnData: NodeIconData = {
type,
path,
};
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();
}
@@ -90,7 +117,7 @@ export default Vue.extend({
.node-icon-wrapper {
width: 26px;
height: 26px;
border-radius: 4px;
border-radius: 2px;
color: #444;
line-height: 26px;
font-size: 1.1em;
@@ -99,7 +126,7 @@ export default Vue.extend({
font-weight: bold;
font-size: 20px;
&.full .icon {
.icon {
height: 100%;
width: 100%;
@@ -108,10 +135,6 @@ export default Vue.extend({
align-items: center;
}
&.shrink .icon {
margin: 0.24em;
}
.node-icon-placeholder {
text-align: center;
}

View File

@@ -148,13 +148,6 @@ export default mixins(
return this.nodeType.properties;
},
isColorDefaultValue (): boolean {
if (this.nodeType === null) {
return false;
}
return this.node.color === this.nodeType.defaults.color;
},
workflowRunning (): boolean {
return this.$store.getters.isActionActive('workflowRunning');
},
@@ -343,7 +336,7 @@ export default mixins(
// Update the values on the node
this.$store.commit('updateNodeProperties', updateInformation);
const node = this.$store.getters.nodeByName(updateInformation.name);
const node = this.$store.getters.getNodeByName(updateInformation.name);
// Update the issues
this.updateNodeCredentialIssues(node);
@@ -363,7 +356,7 @@ export default mixins(
// Save the node name before we commit the change because
// we need the old name to rename the node properly
const nodeNameBefore = parameterData.node || this.node.name;
const node = this.$store.getters.nodeByName(nodeNameBefore);
const node = this.$store.getters.getNodeByName(nodeNameBefore);
if (parameterData.name === 'name') {
// Name of node changed so we have to set also the new node name as active
@@ -379,7 +372,10 @@ export default mixins(
} else if (parameterData.name.startsWith('parameters.')) {
// A node parameter changed
const nodeType = this.$store.getters.nodeType(node.type);
const nodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
if (!nodeType) {
return;
}
// Get only the parameters which are different to the defaults
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
@@ -517,10 +513,6 @@ export default mixins(
// Set default value
Vue.set(this.nodeValues, nodeSetting.name, nodeSetting.default);
}
if (nodeSetting.name === 'color') {
// For color also apply the default node color to the node settings
nodeSetting.default = this.nodeType.defaults.color;
}
}
Vue.set(this.nodeValues, 'parameters', JSON.parse(JSON.stringify(this.node.parameters)));

View File

@@ -66,7 +66,7 @@ export default mixins(
],
data () {
return {
isMinimized: this.nodeType.name !== WEBHOOK_NODE_TYPE,
isMinimized: this.nodeType && this.nodeType.name !== WEBHOOK_NODE_TYPE,
showUrlFor: 'test',
};
},

View File

@@ -213,6 +213,7 @@ import {
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeTypeDescription,
IRunData,
IRunExecutionData,
ITaskData,
@@ -539,8 +540,8 @@ export default mixins(
return outputIndex + 1;
}
const nodeType = this.$store.getters.nodeType(this.node.type);
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
const nodeType = this.$store.getters.nodeType(this.node.type) as INodeTypeDescription | null;
if (!nodeType || !nodeType.outputNames || nodeType.outputNames.length <= outputIndex) {
return outputIndex + 1;
}

View File

@@ -13,3 +13,8 @@ export function convertToHumanReadableDate (epochTime: number) {
export function getAppNameFromCredType(name: string) {
return name.split(' ').filter((word) => !KEYWORDS_TO_FILTER.includes(word)).join(' ');
}
export function getStyleTokenValue(name: string): string {
const style = getComputedStyle(document.body);
return style.getPropertyValue(name);
}

View File

@@ -1,9 +1,10 @@
import { INodeUi } from '@/Interface';
import { INodeUi, XYPosition } from '@/Interface';
import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { getMousePosition, getRelativePosition } from '@/views/canvasHelpers';
export const mouseSelect = mixins(
deviceSupportHelpers,
@@ -42,23 +43,14 @@ export const mouseSelect = mixins(
}
return e.ctrlKey;
},
/**
* Gets mouse position within the node view. Both node view offset and scale (zoom) are considered when
* calculating position.
*
* @param event - mouse event within node view
*/
getMousePositionWithinNodeView (event: MouseEvent) {
getMousePositionWithinNodeView (event: MouseEvent | TouchEvent): XYPosition {
const [x, y] = getMousePosition(event);
// @ts-ignore
const nodeViewScale = this.nodeViewScale;
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
return {
x: (event.pageX - offsetPosition[0]) / nodeViewScale,
y: (event.pageY - offsetPosition[1]) / nodeViewScale,
};
return getRelativePosition(x, y, this.nodeViewScale, this.$store.getters.getNodeViewOffsetPosition);
},
showSelectBox (event: MouseEvent) {
this.selectBox = Object.assign(this.selectBox, this.getMousePositionWithinNodeView(event));
const [x, y] = this.getMousePositionWithinNodeView(event);
this.selectBox = Object.assign(this.selectBox, {x, y});
// @ts-ignore
this.selectBox.style.left = this.selectBox.x + 'px';
@@ -90,7 +82,7 @@ export const mouseSelect = mixins(
this.selectActive = false;
},
getSelectionBox (event: MouseEvent) {
const {x, y} = this.getMousePositionWithinNodeView(event);
const [x, y] = this.getMousePositionWithinNodeView(event);
return {
// @ts-ignore
x: Math.min(x, this.selectBox.x),
@@ -162,6 +154,10 @@ export const mouseSelect = mixins(
this.nodeSelected(node);
});
if (selectedNodes.length === 1) {
this.$store.commit('setLastSelectedNode', selectedNodes[0].name);
}
this.hideSelectBox();
},
mouseMoveSelect (e: MouseEvent) {
@@ -195,6 +191,10 @@ export const mouseSelect = mixins(
this.$store.commit('setLastSelectedNode', null);
this.$store.commit('setLastSelectedNodeOutputIndex', null);
this.$store.commit('setActiveNode', null);
// @ts-ignore
this.lastSelectedConnection = null;
// @ts-ignore
this.newNodeInsertPosition = null;
},
},
});

View File

@@ -3,6 +3,7 @@ import mixins from 'vue-typed-mixins';
import normalizeWheel from 'normalize-wheel';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { getMousePosition } from '@/views/canvasHelpers';
export const moveNodeWorkflow = mixins(
deviceSupportHelpers,
@@ -15,29 +16,18 @@ export const moveNodeWorkflow = mixins(
},
methods: {
getMousePosition(e: MouseEvent | TouchEvent) {
// @ts-ignore
const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
// @ts-ignore
const y = e.pageY !== undefined ? e.pageY : (e.touches && e.touches[0] && e.touches[0].pageY ? e.touches[0].pageY : 0);
return {
x,
y,
};
},
moveWorkflow (e: MouseEvent) {
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const position = this.getMousePosition(e);
const [x, y] = getMousePosition(e);
const nodeViewOffsetPositionX = offsetPosition[0] + (position.x - this.moveLastPosition[0]);
const nodeViewOffsetPositionY = offsetPosition[1] + (position.y - this.moveLastPosition[1]);
const nodeViewOffsetPositionX = offsetPosition[0] + (x - this.moveLastPosition[0]);
const nodeViewOffsetPositionY = offsetPosition[1] + (y - this.moveLastPosition[1]);
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY]});
// Update the last position
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
this.moveLastPosition[0] = x;
this.moveLastPosition[1] = y;
},
mouseDownMoveWorkflow (e: MouseEvent) {
if (this.isCtrlKeyPressed(e) === false) {
@@ -53,10 +43,10 @@ export const moveNodeWorkflow = mixins(
this.$store.commit('setNodeViewMoveInProgress', true);
const position = this.getMousePosition(e);
const [x, y] = getMousePosition(e);
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
this.moveLastPosition[0] = x;
this.moveLastPosition[1] = y;
// @ts-ignore
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);

View File

@@ -1,10 +1,16 @@
import { IConnectionsUi, IEndpointOptions, INodeUi, XYPositon } from '@/Interface';
import { IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE } from '@/constants';
import * as CanvasHelpers from '@/views/canvasHelpers';
import { Endpoint } from 'jsplumb';
import {
INodeTypeDescription,
} from 'n8n-workflow';
export const nodeBase = mixins(
deviceSupportHelpers,
@@ -18,145 +24,31 @@ export const nodeBase = mixins(
},
computed: {
data (): INodeUi {
return this.$store.getters.nodeByName(this.name);
return this.$store.getters.getNodeByName(this.name);
},
hasIssues (): boolean {
if (this.data.issues !== undefined && Object.keys(this.data.issues).length) {
return true;
}
return false;
},
nodeName (): string {
nodeId (): string {
return NODE_NAME_PREFIX + this.nodeIndex;
},
nodeIndex (): string {
return this.$store.getters.getNodeIndex(this.data.name).toString();
},
nodePosition (): object {
const returnStyles: {
[key: string]: string;
} = {
left: this.data.position[0] + 'px',
top: this.data.position[1] + 'px',
};
return returnStyles;
},
nodeStyle (): object {
const returnStyles: {
[key: string]: string;
} = {
'border-color': this.data.color as string,
};
return returnStyles;
},
},
props: [
'name',
'nodeId',
'instance',
'isReadOnly',
'isActive',
'hideActions',
],
methods: {
__addNode (node: INodeUi) {
// TODO: Later move the node-connection definitions to a special file
let nodeTypeData = this.$store.getters.nodeType(node.type);
const nodeConnectors: IConnectionsUi = {
main: {
input: {
uuid: '-input',
maxConnections: -1,
endpoint: 'Rectangle',
endpointStyle: {
width: nodeTypeData && nodeTypeData.outputs.length > 2 ? 9 : 10,
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 24,
fill: '#777',
stroke: '#777',
lineWidth: 0,
},
dragAllowedWhenFull: true,
},
output: {
uuid: '-output',
maxConnections: -1,
endpoint: 'Dot',
endpointStyle: {
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 11,
fill: '#555',
outlineStroke: 'none',
},
dragAllowedWhenFull: true,
},
},
};
if (!nodeTypeData) {
// If node type is not know use by default the base.noOp data to display it
nodeTypeData = this.$store.getters.nodeType(NO_OP_NODE_TYPE);
}
const anchorPositions: {
[key: string]: {
[key: number]: string[] | number[][];
}
} = {
input: {
1: [
'Left',
],
2: [
[0, 0.3, -1, 0],
[0, 0.7, -1, 0],
],
3: [
[0, 0.25, -1, 0],
[0, 0.5, -1, 0],
[0, 0.75, -1, 0],
],
4: [
[0, 0.2, -1, 0],
[0, 0.4, -1, 0],
[0, 0.6, -1, 0],
[0, 0.8, -1, 0],
],
},
output: {
1: [
'Right',
],
2: [
[1, 0.3, 1, 0],
[1, 0.7, 1, 0],
],
3: [
[1, 0.25, 1, 0],
[1, 0.5, 1, 0],
[1, 0.75, 1, 0],
],
4: [
[1, 0.2, 1, 0],
[1, 0.4, 1, 0],
[1, 0.6, 1, 0],
[1, 0.8, 1, 0],
],
},
};
__addInputEndpoints (node: INodeUi, nodeTypeData: INodeTypeDescription) {
// Add Inputs
let index, inputData, anchorPosition;
let newEndpointData: IEndpointOptions;
let indexData: {
let index;
const indexData: {
[key: string]: number;
} = {};
nodeTypeData.inputs.forEach((inputName: string) => {
// @ts-ignore
inputData = nodeConnectors[inputName].input;
nodeTypeData.inputs.forEach((inputName: string, i: number) => {
// Increment the index for inputs with current name
if (indexData.hasOwnProperty(inputName)) {
indexData[inputName]++;
@@ -166,14 +58,15 @@ export const nodeBase = mixins(
index = indexData[inputName];
// Get the position of the anchor depending on how many it has
anchorPosition = anchorPositions.input[nodeTypeData.inputs.length][index];
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
newEndpointData = {
uuid: `${this.nodeIndex}` + inputData.uuid + index,
const newEndpointData: IEndpointOptions = {
uuid: CanvasHelpers.getInputEndpointUUID(this.nodeIndex, index),
anchor: anchorPosition,
maxConnections: inputData.maxConnections,
endpoint: inputData.endpoint,
endpointStyle: inputData.endpointStyle,
maxConnections: -1,
endpoint: 'Rectangle',
endpointStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
endpointHoverStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-primary'),
isSource: false,
isTarget: !this.isReadOnly,
parameters: {
@@ -181,7 +74,8 @@ export const nodeBase = mixins(
type: inputName,
index,
},
dragAllowedWhenFull: inputData.dragAllowedWhenFull,
enabled: !this.isReadOnly,
dragAllowedWhenFull: true,
dropOptions: {
tolerance: 'touch',
hoverClass: 'dropHover',
@@ -191,19 +85,15 @@ export const nodeBase = mixins(
if (nodeTypeData.inputNames) {
// Apply input names if they got set
newEndpointData.overlays = [
['Label',
{
id: 'input-name-label',
location: [-2, 0.5],
label: nodeTypeData.inputNames[index],
cssClass: 'node-input-endpoint-label',
visible: true,
},
],
CanvasHelpers.getInputNameOverlay(nodeTypeData.inputNames[index]),
];
}
this.instance.addEndpoint(this.nodeName, newEndpointData);
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
endpoint.__meta = {
nodeName: node.name,
index: i,
};
// TODO: Activate again if it makes sense. Currently makes problems when removing
// connection on which the input has a name. It does not get hidden because
@@ -213,15 +103,17 @@ export const nodeBase = mixins(
// if (index === 0 && inputName === 'main') {
// // Make the first main-input the default one to connect to when connection gets dropped on node
// this.instance.makeTarget(this.nodeName, newEndpointData);
// this.instance.makeTarget(this.nodeId, newEndpointData);
// }
});
},
__addOutputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
let index;
const indexData: {
[key: string]: number;
} = {};
// Add Outputs
indexData = {};
nodeTypeData.outputs.forEach((inputName: string) => {
inputData = nodeConnectors[inputName].output;
nodeTypeData.outputs.forEach((inputName: string, i: number) => {
// Increment the index for outputs with current name
if (indexData.hasOwnProperty(inputName)) {
indexData[inputName]++;
@@ -231,49 +123,48 @@ export const nodeBase = mixins(
index = indexData[inputName];
// Get the position of the anchor depending on how many it has
anchorPosition = anchorPositions.output[nodeTypeData.outputs.length][index];
const anchorPosition = CanvasHelpers.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
newEndpointData = {
uuid: `${this.nodeIndex}` + inputData.uuid + index,
const newEndpointData: IEndpointOptions = {
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index),
anchor: anchorPosition,
maxConnections: inputData.maxConnections,
endpoint: inputData.endpoint,
endpointStyle: inputData.endpointStyle,
isSource: !this.isReadOnly,
maxConnections: -1,
endpoint: 'Dot',
endpointStyle: CanvasHelpers.getOutputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
endpointHoverStyle: CanvasHelpers.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
isSource: true,
isTarget: false,
enabled: !this.isReadOnly,
parameters: {
nodeIndex: this.nodeIndex,
type: inputName,
index,
},
dragAllowedWhenFull: inputData.dragAllowedWhenFull,
dragAllowedWhenFull: false,
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
};
if (nodeTypeData.outputNames) {
// Apply output names if they got set
newEndpointData.overlays = [
['Label',
{
id: 'output-name-label',
location: [1.75, 0.5],
label: nodeTypeData.outputNames[index],
cssClass: 'node-output-endpoint-label',
visible: true,
},
],
CanvasHelpers.getOutputNameOverlay(nodeTypeData.outputNames[index]),
];
}
this.instance.addEndpoint(this.nodeName, newEndpointData);
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
endpoint.__meta = {
nodeName: node.name,
index: i,
};
});
},
__makeInstanceDraggable(node: INodeUi) {
// TODO: This caused problems with displaying old information
// https://github.com/jsplumb/katavorio/wiki
// https://jsplumb.github.io/jsplumb/home.html
// Make nodes draggable
this.instance.draggable(this.nodeName, {
grid: [10, 10],
this.instance.draggable(this.nodeId, {
grid: [CanvasHelpers.GRID_SIZE, CanvasHelpers.GRID_SIZE],
start: (params: { e: MouseEvent }) => {
if (this.isReadOnly === true) {
// Do not allow to move nodes in readOnly mode
@@ -305,7 +196,7 @@ export const nodeBase = mixins(
// even though "start" and "drag" gets called for all. So lets do for now
// some dirty DOM query to get the new positions till I have more time to
// create a proper solution
let newNodePositon: XYPositon;
let newNodePositon: XYPosition;
moveNodes.forEach((node: INodeUi) => {
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
const element = document.getElementById(nodeElement);
@@ -328,11 +219,23 @@ export const nodeBase = mixins(
this.$store.commit('updateNodeProperties', updateInformation);
});
this.$emit('moved', node);
}
},
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
});
},
__addNode (node: INodeUi) {
let nodeTypeData = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
if (!nodeTypeData) {
// If node type is not know use by default the base.noOp data to display it
nodeTypeData = this.$store.getters.nodeType(NO_OP_NODE_TYPE) as INodeTypeDescription;
}
this.__addInputEndpoints(node, nodeTypeData);
this.__addOutputEndpoints(node, nodeTypeData);
this.__makeInstanceDraggable(node);
},
touchEnd(e: MouseEvent) {
if (this.isTouchDevice) {

View File

@@ -344,6 +344,7 @@ export const nodeHelpers = mixins(
};
this.$store.commit('updateNodeProperties', updateInformation);
this.$store.commit('clearNodeExecutionData', node.name);
this.updateNodeParameterIssues(node);
this.updateNodeCredentialIssues(node);
}

View File

@@ -111,7 +111,8 @@ export const showMessage = mixins(externalHooks, renderText).extend({
return errorMessage;
},
$showError(error: Error, title: string, message?: string) {
$showError(e: Error | unknown, title: string, message?: string) {
const error = e as Error;
const messageLine = message ? `${message}<br/>` : '';
this.$showMessage({
title,

View File

@@ -36,7 +36,7 @@ import {
IWorkflowData,
IWorkflowDb,
IWorkflowDataUpdate,
XYPositon,
XYPosition,
ITag,
IUpdateInformation,
} from '../../Interface';
@@ -227,7 +227,7 @@ export const workflowHelpers = mixins(
return [];
},
getByName: (nodeType: string): INodeType | INodeVersionedType | undefined => {
const nodeTypeDescription = this.$store.getters.nodeType(nodeType);
const nodeTypeDescription = this.$store.getters.nodeType(nodeType) as INodeTypeDescription | null;
if (nodeTypeDescription === null) {
return undefined;
@@ -238,7 +238,7 @@ export const workflowHelpers = mixins(
};
},
getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
const nodeTypeDescription = this.$store.getters.nodeType(nodeType, version);
const nodeTypeDescription = this.$store.getters.nodeType(nodeType, version) as INodeTypeDescription | null;
if (nodeTypeDescription === null) {
return undefined;
@@ -331,7 +331,7 @@ export const workflowHelpers = mixins(
// Get the data of the node type that we can get the default values
// TODO: Later also has to care about the node-type-version as defaults could be different
const nodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription;
const nodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription | null;
if (nodeType !== null) {
// Node-Type is known so we can save the parameters correctly
@@ -364,11 +364,6 @@ export const workflowHelpers = mixins(
nodeData.credentials = saveCredenetials;
}
}
// Save the node color only if it is different to the default color
if (node.color && node.color !== nodeType.defaults.color) {
nodeData.color = node.color;
}
} else {
// Node-Type is not known so save the data as it is
nodeData.credentials = node.credentials;
@@ -570,7 +565,7 @@ export const workflowHelpers = mixins(
// Updates the position of all the nodes that the top-left node
// is at the given position
updateNodePositions (workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPositon): void {
updateNodePositions (workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPosition): void {
if (workflowData.nodes === undefined) {
return;
}

View File

@@ -195,6 +195,7 @@ export const workflowRun = mixins(
},
};
this.$store.commit('setWorkflowExecutionData', executionData);
this.updateNodesExecutionIssues();
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);