Merge branch 'master' into save-changes-warning

This commit is contained in:
Jan
2020-10-25 11:47:47 +01:00
committed by GitHub
697 changed files with 34774 additions and 7712 deletions

View File

@@ -126,6 +126,7 @@ export interface IRestApi {
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
getSettings(): Promise<IN8nUISettings>;
getNodeTypes(): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeList: string[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
@@ -399,6 +400,10 @@ export interface IN8nUISettings {
timezone: string;
executionTimeout: number;
maxExecutionTimeout: number;
oauthCallbackUrls: {
oauth1: string;
oauth2: string;
};
urlBaseWebhook: string;
versionCli: string;
}

View File

@@ -4,7 +4,7 @@
<div name="title" class="title-container" slot="title">
<div class="title-left">{{title}}</div>
<div class="title-right">
<div v-if="credentialType" class="docs-container">
<div v-if="credentialType && documentationUrl" class="docs-container">
<svg class="help-logo" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Node Documentation</title>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
@@ -20,7 +20,7 @@
</g>
</g>
</svg>
<span v-if="credentialType" class="doc-link-text">Need help? <a class="doc-hyperlink" :href="'https://docs.n8n.io/credentials/' + documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'" target="_blank">Open credential docs</a></span>
<span class="doc-link-text">Need help? <a class="doc-hyperlink" :href="'https://docs.n8n.io/credentials/' + documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'" target="_blank">Open credential docs</a></span>
</div>
</div>
</div>
@@ -109,27 +109,19 @@ export default mixins(
}
}
},
documentationUrl (): string {
documentationUrl (): string | undefined {
let credentialTypeName = '';
if (this.editCredentials) {
const credentialType = this.$store.getters.credentialType(this.editCredentials.type);
if (credentialType.documentationUrl === undefined) {
return credentialType.name;
} else {
return `${credentialType.documentationUrl}`;
}
credentialTypeName = this.editCredentials.type as string;
} else {
if (this.credentialType) {
const credentialType = this.$store.getters.credentialType(this.credentialType);
if (credentialType.documentationUrl === undefined) {
return credentialType.name;
} else {
return `${credentialType.documentationUrl}`;
}
} else {
return '';
}
credentialTypeName = this.credentialType as string;
}
const credentialType = this.$store.getters.credentialType(credentialTypeName);
if (credentialType.documentationUrl !== undefined) {
return `${credentialType.documentationUrl}`;
}
return undefined;
},
node (): INodeUi {
return this.$store.getters.activeNode;

View File

@@ -235,7 +235,7 @@ export default mixins(
oAuthCallbackUrl (): string {
const types = this.parentTypes(this.credentialTypeData.name);
const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1';
return this.$store.getters.getWebhookBaseUrl + `rest/${oauthType}-credential/callback`;
return this.$store.getters.oauthCallbackUrls[oauthType];
},
requiredPropertiesFilled (): boolean {
for (const property of this.credentialProperties) {
@@ -404,10 +404,11 @@ export default mixins(
message: 'Connected successfully!',
type: 'success',
});
// Make sure that the event gets removed again
window.removeEventListener('message', receiveMessage, false);
}
// Make sure that the event gets removed again
window.removeEventListener('message', receiveMessage, false);
};
window.addEventListener('message', receiveMessage, false);

View File

@@ -1,6 +1,6 @@
<template>
<div class="node-wrapper" :style="nodePosition">
<div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick">
<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">
<el-tooltip placement="top" effect="light">
<div slot="content" v-html="nodeIssues"></div>
@@ -13,19 +13,19 @@
<font-awesome-icon icon="sync-alt" spin />
</div>
<div class="node-options" v-if="!isReadOnly">
<div @click.stop.left="deleteNode" class="option" title="Delete Node" >
<div v-touch:tap="deleteNode" class="option" title="Delete Node" >
<font-awesome-icon icon="trash" />
</div>
<div @click.stop.left="disableNode" class="option" title="Activate/Deactivate Node" >
<div v-touch:tap="disableNode" class="option" title="Activate/Deactivate Node" >
<font-awesome-icon :icon="nodeDisabledIcon" />
</div>
<div @click.stop.left="duplicateNode" class="option" title="Duplicate Node" >
<div v-touch:tap="duplicateNode" class="option" title="Duplicate Node" >
<font-awesome-icon icon="clone" />
</div>
<div @click.stop.left="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly">
<div v-touch:tap="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly">
<font-awesome-icon class="execute-icon" icon="cog" />
</div>
<div @click.stop.left="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning">
<div v-touch:tap="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning">
<font-awesome-icon class="execute-icon" icon="play-circle" />
</div>
</div>
@@ -110,6 +110,10 @@ export default mixins(nodeBase, workflowHelpers).extend({
classes.push('is-touch-device');
}
if (this.isTouchActive) {
classes.push('touch-active');
}
return classes;
},
nodeIssues (): string {
@@ -134,7 +138,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
}
if (this.nodeType !== null && this.nodeType.subtitle !== undefined) {
return this.workflow.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle) as string | undefined;
return this.workflow.expression.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle) as string | undefined;
}
if (this.data.parameters.operation !== undefined) {
@@ -174,7 +178,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
},
data () {
return {
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints,
isTouchActive: false,
};
},
methods: {
@@ -199,6 +203,14 @@ export default mixins(nodeBase, workflowHelpers).extend({
setNodeActive () {
this.$store.commit('setActiveNode', this.data.name);
},
touchStart () {
if (this.isTouchDevice === true && this.isMacOs === false && this.isTouchActive === false) {
this.isTouchActive = true;
setTimeout(() => {
this.isTouchActive = false;
}, 2000);
}
},
},
});
@@ -268,6 +280,7 @@ export default mixins(nodeBase, workflowHelpers).extend({
}
}
&.touch-active,
&:hover {
.node-execute {
display: initial;

View File

@@ -1,5 +1,5 @@
<template>
<div class="node-icon-wrapper" :style="iconStyleData">
<div class="node-icon-wrapper" :style="iconStyleData" :class="{full: isSvgIcon}">
<div v-if="nodeIconData !== null" class="icon">
<img :src="nodeIconData.path" style="width: 100%; height: 100%;" v-if="nodeIconData.type === 'file'"/>
<font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" />
@@ -17,6 +17,7 @@ import Vue from 'vue';
interface NodeIconData {
type: string;
path: string;
fileExtension?: string;
}
export default Vue.extend({
@@ -41,6 +42,12 @@ export default Vue.extend({
'border-radius': Math.ceil(size / 2) + 'px',
};
},
isSvgIcon (): boolean {
if (this.nodeIconData && this.nodeIconData.type === 'file' && this.nodeIconData.fileExtension === 'svg') {
return true;
}
return false;
},
nodeIconData (): null | NodeIconData {
if (this.nodeType === null) {
return null;
@@ -51,13 +58,14 @@ export default Vue.extend({
if (this.nodeType.icon) {
let type, path;
[type, path] = this.nodeType.icon.split(':');
const returnData = {
const returnData: NodeIconData = {
type,
path,
};
if (type === 'file') {
returnData.path = restUrl + '/node-icon/' + this.nodeType.name;
returnData.fileExtension = path.split('.').slice(-1).join();
}
return returnData;
@@ -83,6 +91,10 @@ export default Vue.extend({
font-weight: bold;
font-size: 20px;
&.full .icon {
margin: 0.24em;
}
.node-icon-placeholder {
text-align: center;
}

View File

@@ -248,7 +248,7 @@ export default mixins(
type: 'boolean',
default: false,
noDataExpression: true,
description: 'If active, the workflow continues even if this node\'s <br /execution fails. When this occurs, the node passes along input data from<br />previous nodes - so your workflow should account for unexpected output data.',
description: 'If active, the workflow continues even if this node\'s <br />execution fails. When this occurs, the node passes along input data from<br />previous nodes - so your workflow should account for unexpected output data.',
},
] as INodeProperties[],

View File

@@ -15,7 +15,7 @@ import Vue from 'vue';
export default Vue.extend(
{
name: 'PageContentWrapper',
}
},
);
</script>

View File

@@ -250,7 +250,7 @@ export default mixins(
* @returns
* @memberof Workflow
*/
getNodeOutputData (runData: IRunData, nodeName: string, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0): IVariableSelectorOption[] | null {
getNodeOutputData (runData: IRunData, nodeName: string, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0, useShort = false): IVariableSelectorOption[] | null {
if (!runData.hasOwnProperty(nodeName)) {
// No data found for node
return null;
@@ -291,9 +291,12 @@ export default mixins(
// Get json data
if (outputData.hasOwnProperty('json')) {
const jsonPropertyPrefix = useShort === true ? '$json' : `$node["${nodeName}"].json`;
const jsonDataOptions: IVariableSelectorOption[] = [];
for (const propertyName of Object.keys(outputData.json)) {
jsonDataOptions.push.apply(jsonDataOptions, this.jsonDataToFilterOption(outputData.json[propertyName], `$node["${nodeName}"].json`, propertyName, filterText));
jsonDataOptions.push.apply(jsonDataOptions, this.jsonDataToFilterOption(outputData.json[propertyName], jsonPropertyPrefix, propertyName, filterText));
}
if (jsonDataOptions.length) {
@@ -308,6 +311,9 @@ export default mixins(
// Get binary data
if (outputData.hasOwnProperty('binary')) {
const binaryPropertyPrefix = useShort === true ? '$binary' : `$node["${nodeName}"].binary`;
const binaryData = [];
let binaryPropertyData = [];
@@ -326,7 +332,7 @@ export default mixins(
binaryPropertyData.push(
{
name: propertyName,
key: `$node["${nodeName}"].binary.${dataPropertyName}.${propertyName}`,
key: `${binaryPropertyPrefix}.${dataPropertyName}.${propertyName}`,
value: outputData.binary![dataPropertyName][propertyName],
},
);
@@ -336,7 +342,7 @@ export default mixins(
binaryData.push(
{
name: dataPropertyName,
key: `$node["${nodeName}"].binary.${dataPropertyName}`,
key: `${binaryPropertyPrefix}.${dataPropertyName}`,
options: this.sortOptions(binaryPropertyData),
allowParentSelect: true,
},
@@ -347,7 +353,7 @@ export default mixins(
returnData.push(
{
name: 'Binary',
key: `$node["${nodeName}"].binary`,
key: binaryPropertyPrefix,
options: this.sortOptions(binaryData),
allowParentSelect: true,
},
@@ -474,7 +480,7 @@ export default mixins(
// (example "IF" node. If node is connected to "true" or to "false" output)
const outputIndex = this.workflow.getNodeConnectionOutputIndex(activeNode.name, parentNode[0], 'main');
tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex) as IVariableSelectorOption[];
tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];
if (tempOutputData) {
if (JSON.stringify(tempOutputData).length < 102400) {

View File

@@ -0,0 +1,30 @@
import Vue from 'vue';
export const deviceSupportHelpers = Vue.extend({
data() {
return {
isTouchDevice: 'ontouchstart' in window || navigator.msMaxTouchPoints,
isMacOs: /(ipad|iphone|ipod|mac)/i.test(navigator.platform),
};
},
computed: {
// TODO: Check if used anywhere
controlKeyCode(): string {
if (this.isMacOs) {
return 'Meta';
}
return 'Control';
},
},
methods: {
isCtrlKeyPressed(e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
if (this.isMacOs) {
return e.metaKey;
}
return e.ctrlKey;
},
},
});

View File

@@ -2,20 +2,19 @@ import { INodeUi } from '@/Interface';
import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
export const mouseSelect = mixins(nodeIndex).extend({
export const mouseSelect = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () {
return {
selectActive: false,
selectBox: document.createElement('span'),
};
},
computed: {
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
},
mounted () {
this.createSelectBox();
},
@@ -34,6 +33,9 @@ export const mouseSelect = mixins(nodeIndex).extend({
this.$el.appendChild(this.selectBox);
},
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
if (this.isMacOs) {
return e.metaKey;
}
@@ -125,6 +127,13 @@ export const mouseSelect = mixins(nodeIndex).extend({
},
mouseUpMouseSelect (e: MouseEvent) {
if (this.selectActive === false) {
if (this.isTouchDevice === true) {
// @ts-ignore
if (e.target && e.target.id.includes('node-view')) {
// Deselect all nodes
this.deselectAllNodes();
}
}
// If it is not active return direcly.
// Else normal node dragging will not work.
return;

View File

@@ -1,41 +1,43 @@
import mixins from 'vue-typed-mixins';
// @ts-ignore
import normalizeWheel from 'normalize-wheel';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
export const moveNodeWorkflow = mixins(nodeIndex).extend({
export const moveNodeWorkflow = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
data () {
return {
moveLastPosition: [0, 0],
};
},
computed: {
controlKeyCode (): string {
if (this.isMacOs) {
return 'Meta';
}
return 'Control';
},
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
},
methods: {
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isMacOs) {
return e.metaKey;
}
return e.ctrlKey;
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 nodeViewOffsetPositionX = offsetPosition[0] + (e.pageX - this.moveLastPosition[0]);
const nodeViewOffsetPositionY = offsetPosition[1] + (e.pageY - this.moveLastPosition[1]);
const position = this.getMousePosition(e);
const nodeViewOffsetPositionX = offsetPosition[0] + (position.x - this.moveLastPosition[0]);
const nodeViewOffsetPositionY = offsetPosition[1] + (position.y - this.moveLastPosition[1]);
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true});
// Update the last position
this.moveLastPosition[0] = e.pageX;
this.moveLastPosition[1] = e.pageY;
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
},
mouseDownMoveWorkflow (e: MouseEvent) {
if (this.isCtrlKeyPressed(e) === false) {
@@ -51,8 +53,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
this.$store.commit('setNodeViewMoveInProgress', true);
this.moveLastPosition[0] = e.pageX;
this.moveLastPosition[1] = e.pageY;
const position = this.getMousePosition(e);
this.moveLastPosition[0] = position.x;
this.moveLastPosition[1] = position.y;
// @ts-ignore
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
@@ -72,6 +76,15 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
// Nothing else to do. Simply leave the node view at the current offset
},
mouseMoveNodeWorkflow (e: MouseEvent) {
// @ts-ignore
if (e.target && !e.target.id.includes('node-view')) {
return;
}
if (this.$store.getters.isActionActive('dragActive')) {
return;
}
if (e.buttons === 0) {
// Mouse button is not pressed anymore so stop selection mode
// Happens normally when mouse leave the view pressed and then
@@ -84,9 +97,10 @@ export const moveNodeWorkflow = mixins(nodeIndex).extend({
this.moveWorkflow(e);
},
wheelMoveWorkflow (e: WheelEvent) {
const normalized = normalizeWheel(e);
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const nodeViewOffsetPositionX = offsetPosition[0] - e.deltaX;
const nodeViewOffsetPositionY = offsetPosition[1] - e.deltaY;
const nodeViewOffsetPositionX = offsetPosition[0] - normalized.pixelX;
const nodeViewOffsetPositionY = offsetPosition[1] - normalized.pixelY;
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY], setStateDirty: true});
},
},

View File

@@ -2,20 +2,20 @@ import { IConnectionsUi, IEndpointOptions, INodeUi, XYPositon } from '@/Interfac
import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { NODE_NAME_PREFIX } from '@/constants';
export const nodeBase = mixins(nodeIndex).extend({
export const nodeBase = mixins(
deviceSupportHelpers,
nodeIndex,
).extend({
mounted () {
// Initialize the node
if (this.data !== null) {
this.__addNode(this.data);
}
},
data () {
return {
};
},
computed: {
data (): INodeUi {
return this.$store.getters.nodeByName(this.name);
@@ -26,9 +26,6 @@ export const nodeBase = mixins(nodeIndex).extend({
}
return false;
},
isMacOs (): boolean {
return /(ipad|iphone|ipod|mac)/i.test(navigator.platform);
},
nodeName (): string {
return NODE_NAME_PREFIX + this.nodeIndex;
},
@@ -336,26 +333,27 @@ export const nodeBase = mixins(nodeIndex).extend({
});
},
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
if (this.isMacOs) {
return e.metaKey;
}
return e.ctrlKey;
},
mouseLeftClick (e: MouseEvent) {
if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive');
} else {
if (this.isCtrlKeyPressed(e) === false) {
this.$emit('deselectAllNodes');
touchEnd(e: MouseEvent) {
if (this.isTouchDevice) {
if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive');
}
if (this.$store.getters.isNodeSelected(this.data.name)) {
this.$emit('deselectNode', this.name);
}
},
mouseLeftClick (e: MouseEvent) {
if (!this.isTouchDevice) {
if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive');
} else {
this.$emit('nodeSelected', this.name);
if (this.isCtrlKeyPressed(e) === false) {
this.$emit('deselectAllNodes');
}
if (this.$store.getters.isNodeSelected(this.data.name)) {
this.$emit('deselectNode', this.name);
} else {
this.$emit('nodeSelected', this.name);
}
}
}
},

View File

@@ -152,6 +152,10 @@ export const restApi = Vue.extend({
return self.restApi().makeRestApiRequest('GET', `/node-types`);
},
getNodesInformation: (nodeList: string[]): Promise<INodeTypeDescription[]> => {
return self.restApi().makeRestApiRequest('POST', `/node-types`, {nodeNames: nodeList});
},
// Returns all the parameter options from the server
getNodeParameterOptions: (nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
const sendData = {

View File

@@ -360,7 +360,7 @@ export const workflowHelpers = mixins(
connectionInputData = [];
}
return workflow.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, true);
return workflow.expression.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, true);
},
// Saves the currently loaded workflow to the database.

View File

@@ -5,6 +5,7 @@ import Vue from 'vue';
import 'prismjs';
import 'prismjs/themes/prism.css';
import 'vue-prism-editor/dist/VuePrismEditor.css';
import Vue2TouchEvents from 'vue2-touch-events';
import * as ElementUI from 'element-ui';
// @ts-ignore
@@ -91,6 +92,9 @@ import {
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { store } from './store';
Vue.use(Vue2TouchEvents);
Vue.use(ElementUI, { locale });
library.add(faAngleDoubleLeft);

View File

@@ -8,6 +8,7 @@ import {
IConnection,
IConnections,
ICredentialType,
IDataObject,
INodeConnections,
INodeIssueData,
INodeTypeDescription,
@@ -56,6 +57,7 @@ export const store = new Vuex.Store({
executionTimeout: -1,
maxExecutionTimeout: Number.MAX_SAFE_INTEGER,
versionCli: '0.0.0',
oauthCallbackUrls: {},
workflowExecutionData: null as IExecutionResponse | null,
lastSelectedNode: null as string | null,
lastSelectedNodeOutputIndex: null as number | null,
@@ -535,6 +537,9 @@ export const store = new Vuex.Store({
setVersionCli (state, version: string) {
Vue.set(state, 'versionCli', version);
},
setOauthCallbackUrls(state, urls: IDataObject) {
Vue.set(state, 'oauthCallbackUrls', urls);
},
addNodeType (state, typeData: INodeTypeDescription) {
if (!typeData.hasOwnProperty('name')) {
@@ -602,6 +607,14 @@ export const store = new Vuex.Store({
Vue.set(state.workflow, 'settings', {});
}
},
updateNodeTypes (state, nodeTypes: INodeTypeDescription[]) {
const updatedNodeNames = nodeTypes.map(node => node.name) as string[];
const oldNodesNotChanged = state.nodeTypes.filter(node => !updatedNodeNames.includes(node.name));
const updatedNodes = [...oldNodesNotChanged, ...nodeTypes];
Vue.set(state, 'nodeTypes', updatedNodes);
state.nodeTypes = updatedNodes;
},
},
getters: {
@@ -658,6 +671,9 @@ export const store = new Vuex.Store({
versionCli: (state): string => {
return state.versionCli;
},
oauthCallbackUrls: (state): object => {
return state.oauthCallbackUrls;
},
// Push Connection
pushConnectionActive: (state): boolean => {

View File

@@ -3,11 +3,15 @@
<div
class="node-view-wrapper"
:class="workflowClasses"
@touchstart="mouseDown"
@touchend="mouseUp"
@touchmove="mouseMoveNodeWorkflow"
@mousedown="mouseDown"
v-touch:tap="touchTap"
@mouseup="mouseUp"
@wheel="wheelScroll"
>
<div class="node-view-background" :style="backgroundStyle"></div>
<div id="node-view-background" class="node-view-background" :style="backgroundStyle"></div>
<div id="node-view" class="node-view" :style="workflowStyle">
<node
v-for="nodeData in nodes"
@@ -356,14 +360,20 @@ export default mixins(
return data;
},
mouseDown (e: MouseEvent) {
touchTap (e: MouseEvent | TouchEvent) {
if (this.isTouchDevice) {
this.mouseDown(e);
}
},
mouseDown (e: MouseEvent | TouchEvent) {
// Save the location of the mouse click
const position = this.getMousePosition(e);
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
this.lastClickPosition[0] = e.pageX - offsetPosition[0];
this.lastClickPosition[1] = e.pageY - offsetPosition[1];
this.lastClickPosition[0] = position.x - offsetPosition[0];
this.lastClickPosition[1] = position.y - offsetPosition[1];
this.mouseDownMouseSelect(e);
this.mouseDownMoveWorkflow(e);
this.mouseDownMouseSelect(e as MouseEvent);
this.mouseDownMoveWorkflow(e as MouseEvent);
// Hide the node-creator
this.createNodeActive = false;
@@ -962,7 +972,7 @@ export default mixins(
// If a node is active then add the new node directly after the current one
// newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60];
newNodeData.position = this.getNewNodePosition(
[lastSelectedNode.position[0] + 150, lastSelectedNode.position[1]],
[lastSelectedNode.position[0] + 200, lastSelectedNode.position[1]],
[100, 0],
);
} else {
@@ -1456,6 +1466,11 @@ export default mixins(
[0, 150],
);
if (newNodeData.webhookId) {
// Make sure that the node gets a new unique webhook-ID
newNodeData.webhookId = uuidv4();
}
await this.addNodes([newNodeData]);
// Automatically deselect all nodes and select the current one and also active
@@ -1593,6 +1608,11 @@ export default mixins(
return;
}
// Before proceeding we must check if all nodes contain the `properties` attribute.
// Nodes are loaded without this information so we must make sure that all nodes
// being added have this information.
await this.loadNodesProperties(nodes.map(node => node.type));
// Add the node to the node-list
let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null;
@@ -1703,6 +1723,9 @@ export default mixins(
let oldName: string;
let newName: string;
const createNodes: INode[] = [];
await this.loadNodesProperties(data.nodes.map(node => node.type));
data.nodes.forEach(node => {
if (nodeTypesCount[node.type] !== undefined) {
if (nodeTypesCount[node.type].exist >= nodeTypesCount[node.type].max) {
@@ -1745,6 +1768,10 @@ export default mixins(
for (type of Object.keys(currentConnections[sourceNode])) {
connection[type] = [];
for (sourceIndex = 0; sourceIndex < currentConnections[sourceNode][type].length; sourceIndex++) {
if (!currentConnections[sourceNode][type][sourceIndex]) {
// There is so something wrong with the data so ignore
continue;
}
const nodeSourceConnections = [];
for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) {
const nodeConnection: NodeInputConnections = [];
@@ -1908,6 +1935,7 @@ export default mixins(
this.$store.commit('setExecutionTimeout', settings.executionTimeout);
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
this.$store.commit('setVersionCli', settings.versionCli);
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
},
async loadNodeTypes (): Promise<void> {
const nodeTypes = await this.restApi().getNodeTypes();
@@ -1921,6 +1949,17 @@ export default mixins(
const credentials = await this.restApi().getAllCredentials();
this.$store.commit('setCredentials', credentials);
},
async loadNodesProperties(nodeNames: string[]): Promise<void> {
const allNodes = this.$store.getters.allNodeTypes;
const nodesToBeFetched = allNodes.filter((node: INodeTypeDescription) => nodeNames.includes(node.name) && !node.hasOwnProperty('properties')).map((node: INodeTypeDescription) => node.name) as string[];
if (nodesToBeFetched.length > 0) {
// Only call API if node information is actually missing
this.startLoading();
const nodeInfo = await this.restApi().getNodesInformation(nodesToBeFetched);
this.$store.commit('updateNodeTypes', nodeInfo);
this.stopLoading();
}
},
},
async mounted () {