mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
🔀 Merge master
This commit is contained in:
commit
7518617eee
|
@ -83,7 +83,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oclif/command": "^1.5.18",
|
"@oclif/command": "^1.5.18",
|
||||||
"@oclif/errors": "^1.2.2",
|
"@oclif/errors": "^1.2.2",
|
||||||
"@rudderstack/rudder-sdk-node": "^1.0.2",
|
"@rudderstack/rudder-sdk-node": "1.0.6",
|
||||||
"@types/json-diff": "^0.5.1",
|
"@types/json-diff": "^0.5.1",
|
||||||
"@types/jsonwebtoken": "^8.5.2",
|
"@types/jsonwebtoken": "^8.5.2",
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
|
|
|
@ -16,7 +16,7 @@ import VariableTable from './VariableTable.vue';
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="border-radius">
|
<Story name="border-radius">
|
||||||
{{
|
{{
|
||||||
template: `<variable-table :variables="['--border-radius-small','--border-radius-base']" />`,
|
template: `<variable-table :variables="['--border-radius-small','--border-radius-base', '--border-radius-large', '--border-radius-xlarge']" />`,
|
||||||
components: {
|
components: {
|
||||||
VariableTable,
|
VariableTable,
|
||||||
},
|
},
|
||||||
|
|
|
@ -44,7 +44,7 @@ import ColorCircles from './ColorCircles.vue';
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="success">
|
<Story name="success">
|
||||||
{{
|
{{
|
||||||
template: `<color-circles :colors="['--color-success', '--color-success-tint-1', '--color-success-tint-2']" />`,
|
template: `<color-circles :colors="['--color-success', '--color-success-tint-1', '--color-success-tint-2', '--color-success-light']" />`,
|
||||||
components: {
|
components: {
|
||||||
ColorCircles,
|
ColorCircles,
|
||||||
},
|
},
|
||||||
|
@ -109,7 +109,7 @@ import ColorCircles from './ColorCircles.vue';
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="foreground">
|
<Story name="foreground">
|
||||||
{{
|
{{
|
||||||
template: `<color-circles :colors="['--color-foreground-base', '--color-foreground-light', '--color-foreground-xlight']" />`,
|
template: `<color-circles :colors="['--color-foreground-xdark', '--color-foreground-dark', '--color-foreground-base', '--color-foreground-light', '--color-foreground-xlight']" />`,
|
||||||
components: {
|
components: {
|
||||||
ColorCircles,
|
ColorCircles,
|
||||||
},
|
},
|
||||||
|
@ -129,3 +129,16 @@ import ColorCircles from './ColorCircles.vue';
|
||||||
}}
|
}}
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
## Canvas
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story name="canvas">
|
||||||
|
{{
|
||||||
|
template: `<color-circles :colors="['--color-canvas-background', '--color-canvas-dot']" />`,
|
||||||
|
components: {
|
||||||
|
ColorCircles,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
|
@ -75,6 +75,15 @@
|
||||||
var(--color-success-tint-2-l)
|
var(--color-success-tint-2-l)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--color-success-light-h: 150;
|
||||||
|
--color-success-light-s: 54%;
|
||||||
|
--color-success-light-l: 70%;
|
||||||
|
--color-success-light: hsl(
|
||||||
|
var(--color-success-light-h),
|
||||||
|
var(--color-success-light-s),
|
||||||
|
var(--color-success-light-l)
|
||||||
|
);
|
||||||
|
|
||||||
--color-warning-h: 36;
|
--color-warning-h: 36;
|
||||||
--color-warning-s: 77%;
|
--color-warning-s: 77%;
|
||||||
--color-warning-l: 57%;
|
--color-warning-l: 57%;
|
||||||
|
@ -187,6 +196,24 @@
|
||||||
var(--color-text-xlight-l)
|
var(--color-text-xlight-l)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--color-foreground-xdark-h: 220;
|
||||||
|
--color-foreground-xdark-s: 7.4%;
|
||||||
|
--color-foreground-xdark-l: 52.5%;
|
||||||
|
--color-foreground-xdark: hsl(
|
||||||
|
var(--color-foreground-xdark-h),
|
||||||
|
var(--color-foreground-xdark-s),
|
||||||
|
var(--color-foreground-xdark-l)
|
||||||
|
);
|
||||||
|
|
||||||
|
--color-foreground-dark-h: 228;
|
||||||
|
--color-foreground-dark-s: 9.6%;
|
||||||
|
--color-foreground-dark-l: 79.6%;
|
||||||
|
--color-foreground-dark: hsl(
|
||||||
|
var(--color-foreground-dark-h),
|
||||||
|
var(--color-foreground-dark-s),
|
||||||
|
var(--color-foreground-dark-l)
|
||||||
|
);
|
||||||
|
|
||||||
--color-foreground-base-h: 220;
|
--color-foreground-base-h: 220;
|
||||||
--color-foreground-base-s: 20%;
|
--color-foreground-base-s: 20%;
|
||||||
--color-foreground-base-l: 88.2%;
|
--color-foreground-base-l: 88.2%;
|
||||||
|
@ -259,6 +286,25 @@
|
||||||
var(--color-background-xlight-l)
|
var(--color-background-xlight-l)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--color-canvas-dot-h: 204;
|
||||||
|
--color-canvas-dot-s: 15.6%;
|
||||||
|
--color-canvas-dot-l: 87.5%;
|
||||||
|
--color-canvas-dot: hsl(
|
||||||
|
var(--color-canvas-dot-h),
|
||||||
|
var(--color-canvas-dot-s),
|
||||||
|
var(--color-canvas-dot-l)
|
||||||
|
);
|
||||||
|
|
||||||
|
--color-canvas-background-h: 260;
|
||||||
|
--color-canvas-background-s: 100%;
|
||||||
|
--color-canvas-background-l: 99.4%;
|
||||||
|
--color-canvas-background: hsl(
|
||||||
|
var(--color-canvas-background-h),
|
||||||
|
var(--color-canvas-background-s),
|
||||||
|
var(--color-canvas-background-l)
|
||||||
|
);
|
||||||
|
|
||||||
|
--border-radius-xlarge: 12px;
|
||||||
--border-radius-large: 8px;
|
--border-radius-large: 8px;
|
||||||
--border-radius-base: 4px;
|
--border-radius-base: 4px;
|
||||||
--border-radius-small: 2px;
|
--border-radius-small: 2px;
|
||||||
|
|
|
@ -22,32 +22,61 @@ import {
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
|
||||||
PaintStyle,
|
|
||||||
} from 'jsplumb';
|
|
||||||
|
|
||||||
declare module 'jsplumb' {
|
declare module 'jsplumb' {
|
||||||
|
interface PaintStyle {
|
||||||
|
stroke?: string;
|
||||||
|
fill?: string;
|
||||||
|
strokeWidth?: number;
|
||||||
|
outlineStroke?: string;
|
||||||
|
outlineWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface Anchor {
|
interface Anchor {
|
||||||
lastReturnValue: number[];
|
lastReturnValue: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Connection {
|
interface Connection {
|
||||||
|
__meta?: {
|
||||||
|
sourceNodeName: string,
|
||||||
|
sourceOutputIndex: number,
|
||||||
|
targetNodeName: string,
|
||||||
|
targetOutputIndex: number,
|
||||||
|
};
|
||||||
|
canvas?: HTMLElement;
|
||||||
|
connector?: {
|
||||||
|
setTargetEndpoint: (endpoint: Endpoint) => void;
|
||||||
|
resetTargetEndpoint: () => void;
|
||||||
|
bounds: {
|
||||||
|
minX: number;
|
||||||
|
maxX: number;
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// bind(event: string, (connection: Connection): void;): void; // tslint:disable-line:no-any
|
// bind(event: string, (connection: Connection): void;): void; // tslint:disable-line:no-any
|
||||||
bind(event: string, callback: Function): void; // tslint:disable-line:no-any
|
bind(event: string, callback: Function): void;
|
||||||
removeOverlay(name: string): void;
|
removeOverlay(name: string): void;
|
||||||
removeOverlays(): void;
|
removeOverlays(): void;
|
||||||
setParameter(name: string, value: any): void; // tslint:disable-line:no-any
|
setParameter(name: string, value: any): void; // tslint:disable-line:no-any
|
||||||
setPaintStyle(arg0: PaintStyle): void;
|
setPaintStyle(arg0: PaintStyle): void;
|
||||||
addOverlay(arg0: any[]): void; // tslint:disable-line:no-any
|
addOverlay(arg0: any[]): void; // tslint:disable-line:no-any
|
||||||
setConnector(arg0: any[]): void; // tslint:disable-line:no-any
|
setConnector(arg0: any[]): void; // tslint:disable-line:no-any
|
||||||
|
getUuids(): [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Endpoint {
|
interface Endpoint {
|
||||||
|
__meta?: {
|
||||||
|
nodeName: string,
|
||||||
|
index: number,
|
||||||
|
};
|
||||||
getOverlay(name: string): any; // tslint:disable-line:no-any
|
getOverlay(name: string): any; // tslint:disable-line:no-any
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Overlay {
|
interface Overlay {
|
||||||
setVisible(visible: boolean): void;
|
setVisible(visible: boolean): void;
|
||||||
|
setLocation(location: number): void;
|
||||||
|
canvas?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnConnectionBindInfo {
|
interface OnConnectionBindInfo {
|
||||||
|
@ -66,18 +95,14 @@ export interface IEndpointOptions {
|
||||||
dragProxy?: any; // tslint:disable-line:no-any
|
dragProxy?: any; // tslint:disable-line:no-any
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
endpointStyle?: object;
|
endpointStyle?: object;
|
||||||
|
endpointHoverStyle?: object;
|
||||||
isSource?: boolean;
|
isSource?: boolean;
|
||||||
isTarget?: boolean;
|
isTarget?: boolean;
|
||||||
maxConnections?: number;
|
maxConnections?: number;
|
||||||
overlays?: any; // tslint:disable-line:no-any
|
overlays?: any; // tslint:disable-line:no-any
|
||||||
parameters?: any; // tslint:disable-line:no-any
|
parameters?: any; // tslint:disable-line:no-any
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
}
|
enabled?: boolean;
|
||||||
|
|
||||||
export interface IConnectionsUi {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: IEndpointOptions;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdateInformation {
|
export interface IUpdateInformation {
|
||||||
|
@ -95,20 +120,16 @@ export interface INodeUpdatePropertiesInformation {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type XYPositon = [number, number];
|
export type XYPosition = [number, number];
|
||||||
|
|
||||||
export type MessageType = 'success' | 'warning' | 'info' | 'error';
|
export type MessageType = 'success' | 'warning' | 'info' | 'error';
|
||||||
|
|
||||||
export interface INodeUi extends INode {
|
export interface INodeUi extends INode {
|
||||||
position: XYPositon;
|
position: XYPosition;
|
||||||
color?: string;
|
color?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
issues?: INodeIssues;
|
issues?: INodeIssues;
|
||||||
_jsPlumb?: {
|
name: string;
|
||||||
endpoints?: {
|
|
||||||
[key: string]: IEndpointOptions[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeTypesMaxCount {
|
export interface INodeTypesMaxCount {
|
||||||
|
@ -617,7 +638,7 @@ export interface IRootState {
|
||||||
lastSelectedNodeOutputIndex: number | null;
|
lastSelectedNodeOutputIndex: number | null;
|
||||||
nodeIndex: Array<string | null>;
|
nodeIndex: Array<string | null>;
|
||||||
nodeTypes: INodeTypeDescription[];
|
nodeTypes: INodeTypeDescription[];
|
||||||
nodeViewOffsetPosition: XYPositon;
|
nodeViewOffsetPosition: XYPosition;
|
||||||
nodeViewMoveInProgress: boolean;
|
nodeViewMoveInProgress: boolean;
|
||||||
selectedNodes: INodeUi[];
|
selectedNodes: INodeUi[];
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
@ -683,5 +704,13 @@ export interface IRestApiContext {
|
||||||
|
|
||||||
export interface IZoomConfig {
|
export interface IZoomConfig {
|
||||||
scale: number;
|
scale: number;
|
||||||
offset: XYPositon;
|
offset: XYPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IBounds {
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="node-wrapper" :style="nodePosition">
|
<div :class="{'node-wrapper': true, selected: isSelected}" :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 class="select-background" v-show="isSelected"></div>
|
||||||
<div v-if="hasIssues" class="node-info-icon node-issues">
|
<div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name">
|
||||||
<n8n-tooltip placement="top" >
|
<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>
|
<div slot="content" v-html="nodeIssues"></div>
|
||||||
<font-awesome-icon icon="exclamation-triangle" />
|
<font-awesome-icon icon="exclamation-triangle" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<el-badge v-else :hidden="workflowDataItems === 0" class="node-info-icon data-count" :value="workflowDataItems"></el-badge>
|
<div v-else-if="waiting" class="waiting">
|
||||||
|
<n8n-tooltip placement="bottom">
|
||||||
<div v-if="waiting" class="node-info-icon waiting">
|
|
||||||
<n8n-tooltip placement="top">
|
|
||||||
<div slot="content" v-html="waiting"></div>
|
<div slot="content" v-html="waiting"></div>
|
||||||
<font-awesome-icon icon="clock" />
|
<font-awesome-icon icon="clock" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</div>
|
</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 class="node-executing-info" :title="$baseText('node.nodeIsExecuting')">
|
<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 v-touch:tap="deleteNode" class="option" :title="$baseText('node.deleteNode')">
|
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" :disabled="this.data.disabled"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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" />
|
<font-awesome-icon icon="trash" />
|
||||||
</div>
|
</div>
|
||||||
<div v-touch:tap="disableNode" class="option" :title="$baseText('node.activateDeactivateNode')">
|
<div v-touch:tap="disableNode" class="option" :title="$baseText('node.activateDeactivateNode')">
|
||||||
|
@ -36,12 +47,12 @@
|
||||||
<font-awesome-icon class="execute-icon" icon="play-circle" />
|
<font-awesome-icon class="execute-icon" icon="play-circle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div :class="{'disabled-linethrough': true, success: workflowDataItems > 0}" v-if="showDisabledLinethrough"></div>
|
||||||
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :circle="true" :shrink="true" :disabled="this.data.disabled"/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="node-description">
|
<div class="node-description">
|
||||||
<div class="node-name" :title="data.name">
|
<div class="node-name" :title="data.name">
|
||||||
{{data.name}}
|
<p>{{ nodeTitle }}</p>
|
||||||
|
<p v-if="data.disabled">(Disabled)</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="nodeSubtitle !== undefined" class="node-subtitle" :title="nodeSubtitle">
|
<div v-if="nodeSubtitle !== undefined" class="node-subtitle" :title="nodeSubtitle">
|
||||||
{{nodeSubtitle}}
|
{{nodeSubtitle}}
|
||||||
|
@ -62,6 +73,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
|
ITaskData,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -70,6 +82,8 @@ import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
import { getStyleTokenValue } from './helpers';
|
||||||
|
import { INodeUi, XYPosition } from '@/Interface';
|
||||||
|
|
||||||
export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflowHelpers).extend({
|
export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflowHelpers).extend({
|
||||||
name: 'Node',
|
name: 'Node',
|
||||||
|
@ -77,8 +91,17 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
NodeIcon,
|
NodeIcon,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
workflowDataItems () {
|
nodeRunData(): ITaskData[] {
|
||||||
const workflowResultDataNode = this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
|
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) {
|
if (workflowResultDataNode === null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -91,34 +114,12 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
nodeType (): INodeTypeDescription | null {
|
nodeType (): INodeTypeDescription | null {
|
||||||
return this.$store.getters.nodeType(this.data.type);
|
return this.$store.getters.nodeType(this.data.type);
|
||||||
},
|
},
|
||||||
nodeClass () {
|
nodeClass (): object {
|
||||||
const classes = [];
|
return {
|
||||||
|
'node-box': true,
|
||||||
if (this.data.disabled) {
|
disabled: this.data.disabled,
|
||||||
classes.push('disabled');
|
executing: this.isExecuting,
|
||||||
}
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
nodeIssues (): string {
|
nodeIssues (): string {
|
||||||
if (this.data.issues === undefined) {
|
if (this.data.issues === undefined) {
|
||||||
|
@ -136,6 +137,27 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
return 'play';
|
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 {
|
waiting (): string | undefined {
|
||||||
const workflowExecution = this.$store.getters.getWorkflowExecution;
|
const workflowExecution = this.$store.getters.getWorkflowExecution;
|
||||||
|
|
||||||
|
@ -155,6 +177,38 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
workflowRunning (): boolean {
|
workflowRunning (): boolean {
|
||||||
return this.$store.getters.isActionActive('workflowRunning');
|
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: {
|
watch: {
|
||||||
isActive(newValue, oldValue) {
|
isActive(newValue, oldValue) {
|
||||||
|
@ -162,9 +216,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
this.setSubtitle();
|
this.setSubtitle();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
nodeRunData(newValue) {
|
||||||
|
this.$emit('run', {name: this.data.name, data: newValue, waiting: !!this.waiting});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setSubtitle();
|
this.setSubtitle();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit('run', {name: this.data.name, data: this.nodeRunData, waiting: !!this.waiting});
|
||||||
|
}, 0);
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -214,7 +274,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.node-wrapper {
|
.node-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -222,20 +282,25 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
||||||
.node-description {
|
.node-description {
|
||||||
line-height: 1.5;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -55px;
|
top: 100px;
|
||||||
left: -50px;
|
left: -50px;
|
||||||
width: 200px;
|
line-height: 1.5;
|
||||||
height: 50px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
padding: 8px;
|
||||||
|
width: 200px;
|
||||||
|
pointer-events: none; // prevent container from being draggable
|
||||||
|
|
||||||
.node-name {
|
.node-name > p { // must be paragraph tag to have two lines in safari
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
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 {
|
.node-subtitle {
|
||||||
|
@ -249,35 +314,26 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-default {
|
.node-default {
|
||||||
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 25px;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 24;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #444;
|
|
||||||
border: 1px dashed grey;
|
|
||||||
|
|
||||||
&.has-data {
|
.node-box {
|
||||||
border-style: solid;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
border: 2px solid var(--color-foreground-xdark);
|
||||||
&.disabled {
|
border-radius: var(--border-radius-large);
|
||||||
color: #a0a0a0;
|
background-color: var(--color-background-xlight);
|
||||||
text-decoration: line-through;
|
|
||||||
border: 1px solid #eee !important;
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.executing {
|
&.executing {
|
||||||
background-color: $--color-primary-light !important;
|
background-color: $--color-primary-light !important;
|
||||||
border-color: $--color-primary !important;
|
|
||||||
|
|
||||||
.node-executing-info {
|
.node-executing-info {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.touch-active,
|
&.touch-active,
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -306,39 +362,35 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
|
|
||||||
.node-icon {
|
.node-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(50% - 30px);
|
top: calc(50% - 20px);
|
||||||
left: calc(50% - 30px);
|
left: calc(50% - 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-info-icon {
|
.node-info-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -14px;
|
bottom: 6px;
|
||||||
|
right: 6px;
|
||||||
|
|
||||||
|
&.shift-icon {
|
||||||
right: 12px;
|
right: 12px;
|
||||||
z-index: 11;
|
}
|
||||||
|
|
||||||
&.data-count {
|
.data-count {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
top: -12px;
|
color: var(--color-success);
|
||||||
}
|
|
||||||
|
|
||||||
&.waiting {
|
|
||||||
left: 10px;
|
|
||||||
top: -12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-issues {
|
.node-issues {
|
||||||
width: 25px;
|
color: var(--color-danger);
|
||||||
height: 25px;
|
}
|
||||||
font-size: 20px;
|
|
||||||
color: #ff0000;
|
.items-count {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.waiting {
|
.waiting {
|
||||||
width: 25px;
|
color: var(--color-secondary);
|
||||||
height: 25px;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #5e5efa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-options {
|
.node-options {
|
||||||
|
@ -347,7 +399,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
top: -25px;
|
top: -25px;
|
||||||
left: -10px;
|
left: -10px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 45px;
|
height: 24px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
@ -382,45 +434,94 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, renderText, workflow
|
||||||
display: initial;
|
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>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.el-badge__content {
|
/** node */
|
||||||
border-width: 2px;
|
.node-wrapper.selected {
|
||||||
background-color: #67c23a;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** connector */
|
||||||
.jtk-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 {
|
.jtk-endpoint {
|
||||||
z-index:5;
|
z-index:5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jtk-connector.jtk-hover {
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-linethrough {
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jtk-endpoint.jtk-hover {
|
||||||
|
z-index: 7;
|
||||||
|
}
|
||||||
|
|
||||||
.jtk-overlay {
|
.jtk-overlay {
|
||||||
z-index:6;
|
z-index:7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jtk-endpoint.dropHover {
|
.jtk-connector.jtk-dragging {
|
||||||
border: 2px solid #ff2244;
|
z-index: 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jtk-drag-selected .node-default {
|
.jtk-endpoint.jtk-drag-active {
|
||||||
/* https://www.cssmatic.com/box-shadow */
|
z-index: 9;
|
||||||
-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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled .node-icon img {
|
.connection-actions {
|
||||||
-webkit-filter: contrast(40%) brightness(1.5) grayscale(100%);
|
z-index: 10;
|
||||||
filter: contrast(40%) brightness(1.5) grayscale(100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-options {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-add-node-label {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -108,7 +108,7 @@ export default mixins(
|
||||||
credentialTypesNodeDescription (): INodeCredentialDescription[] {
|
credentialTypesNodeDescription (): INodeCredentialDescription[] {
|
||||||
const node = this.node as INodeUi;
|
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) {
|
if (activeNodeType && activeNodeType.credentials) {
|
||||||
return activeNodeType.credentials;
|
return activeNodeType.credentials;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<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">
|
<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" />
|
<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">
|
||||||
{{nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
{{nodeType !== null ? nodeType.displayName.charAt(0) : '?' }}
|
||||||
|
@ -12,39 +12,65 @@
|
||||||
|
|
||||||
<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: {
|
||||||
'shrink',
|
type: Number,
|
||||||
'disabled',
|
},
|
||||||
'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 = nodeType ? nodeType.defaults && nodeType!.defaults.color : '';
|
||||||
if (!this.size) {
|
if (!this.size) {
|
||||||
return {color};
|
return {color};
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = parseInt(this.size, 10);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
color,
|
color,
|
||||||
width: size + 'px',
|
width: this.size + 'px',
|
||||||
height: size + 'px',
|
height: this.size + 'px',
|
||||||
'font-size': Math.floor(parseInt(this.size, 10) * 0.6) + 'px',
|
'font-size': this.size + 'px',
|
||||||
'line-height': size + 'px',
|
'line-height': this.size + 'px',
|
||||||
'border-radius': this.circle ? '50%': '4px',
|
'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 {
|
isSvgIcon (): boolean {
|
||||||
|
@ -54,26 +80,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +117,7 @@ export default Vue.extend({
|
||||||
.node-icon-wrapper {
|
.node-icon-wrapper {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
border-radius: 4px;
|
border-radius: 2px;
|
||||||
color: #444;
|
color: #444;
|
||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
|
@ -99,7 +126,7 @@ export default Vue.extend({
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
&.full .icon {
|
.icon {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -108,10 +135,6 @@ export default Vue.extend({
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.shrink .icon {
|
|
||||||
margin: 0.24em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-icon-placeholder {
|
.node-icon-placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,13 +148,6 @@ export default mixins(
|
||||||
|
|
||||||
return this.nodeType.properties;
|
return this.nodeType.properties;
|
||||||
},
|
},
|
||||||
isColorDefaultValue (): boolean {
|
|
||||||
if (this.nodeType === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.node.color === this.nodeType.defaults.color;
|
|
||||||
},
|
|
||||||
workflowRunning (): boolean {
|
workflowRunning (): boolean {
|
||||||
return this.$store.getters.isActionActive('workflowRunning');
|
return this.$store.getters.isActionActive('workflowRunning');
|
||||||
},
|
},
|
||||||
|
@ -343,7 +336,7 @@ export default mixins(
|
||||||
// Update the values on the node
|
// Update the values on the node
|
||||||
this.$store.commit('updateNodeProperties', updateInformation);
|
this.$store.commit('updateNodeProperties', updateInformation);
|
||||||
|
|
||||||
const node = this.$store.getters.nodeByName(updateInformation.name);
|
const node = this.$store.getters.getNodeByName(updateInformation.name);
|
||||||
|
|
||||||
// Update the issues
|
// Update the issues
|
||||||
this.updateNodeCredentialIssues(node);
|
this.updateNodeCredentialIssues(node);
|
||||||
|
@ -363,7 +356,7 @@ export default mixins(
|
||||||
// Save the node name before we commit the change because
|
// Save the node name before we commit the change because
|
||||||
// we need the old name to rename the node properly
|
// we need the old name to rename the node properly
|
||||||
const nodeNameBefore = parameterData.node || this.node.name;
|
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') {
|
if (parameterData.name === 'name') {
|
||||||
// Name of node changed so we have to set also the new node name as active
|
// 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.')) {
|
} else if (parameterData.name.startsWith('parameters.')) {
|
||||||
// A node parameter changed
|
// 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
|
// Get only the parameters which are different to the defaults
|
||||||
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
|
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
|
||||||
|
@ -517,10 +513,6 @@ export default mixins(
|
||||||
// Set default value
|
// Set default value
|
||||||
Vue.set(this.nodeValues, nodeSetting.name, nodeSetting.default);
|
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)));
|
Vue.set(this.nodeValues, 'parameters', JSON.parse(JSON.stringify(this.node.parameters)));
|
||||||
|
|
|
@ -66,7 +66,7 @@ export default mixins(
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isMinimized: this.nodeType.name !== WEBHOOK_NODE_TYPE,
|
isMinimized: this.nodeType && this.nodeType.name !== WEBHOOK_NODE_TYPE,
|
||||||
showUrlFor: 'test',
|
showUrlFor: 'test',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -213,6 +213,7 @@ import {
|
||||||
IBinaryKeyData,
|
IBinaryKeyData,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
INodeTypeDescription,
|
||||||
IRunData,
|
IRunData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
@ -539,8 +540,8 @@ export default mixins(
|
||||||
return outputIndex + 1;
|
return outputIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeType = this.$store.getters.nodeType(this.node.type);
|
const nodeType = this.$store.getters.nodeType(this.node.type) as INodeTypeDescription | null;
|
||||||
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
|
if (!nodeType || !nodeType.outputNames || nodeType.outputNames.length <= outputIndex) {
|
||||||
return outputIndex + 1;
|
return outputIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,3 +13,8 @@ export function convertToHumanReadableDate (epochTime: number) {
|
||||||
export function getAppNameFromCredType(name: string) {
|
export function getAppNameFromCredType(name: string) {
|
||||||
return name.split(' ').filter((word) => !KEYWORDS_TO_FILTER.includes(word)).join(' ');
|
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);
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { INodeUi } from '@/Interface';
|
import { INodeUi, XYPosition } from '@/Interface';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||||
|
import { getMousePosition, getRelativePosition } from '@/views/canvasHelpers';
|
||||||
|
|
||||||
export const mouseSelect = mixins(
|
export const mouseSelect = mixins(
|
||||||
deviceSupportHelpers,
|
deviceSupportHelpers,
|
||||||
|
@ -42,23 +43,14 @@ export const mouseSelect = mixins(
|
||||||
}
|
}
|
||||||
return e.ctrlKey;
|
return e.ctrlKey;
|
||||||
},
|
},
|
||||||
/**
|
getMousePositionWithinNodeView (event: MouseEvent | TouchEvent): XYPosition {
|
||||||
* Gets mouse position within the node view. Both node view offset and scale (zoom) are considered when
|
const [x, y] = getMousePosition(event);
|
||||||
* calculating position.
|
|
||||||
*
|
|
||||||
* @param event - mouse event within node view
|
|
||||||
*/
|
|
||||||
getMousePositionWithinNodeView (event: MouseEvent) {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const nodeViewScale = this.nodeViewScale;
|
return getRelativePosition(x, y, this.nodeViewScale, this.$store.getters.getNodeViewOffsetPosition);
|
||||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
|
||||||
return {
|
|
||||||
x: (event.pageX - offsetPosition[0]) / nodeViewScale,
|
|
||||||
y: (event.pageY - offsetPosition[1]) / nodeViewScale,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
showSelectBox (event: MouseEvent) {
|
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
|
// @ts-ignore
|
||||||
this.selectBox.style.left = this.selectBox.x + 'px';
|
this.selectBox.style.left = this.selectBox.x + 'px';
|
||||||
|
@ -90,7 +82,7 @@ export const mouseSelect = mixins(
|
||||||
this.selectActive = false;
|
this.selectActive = false;
|
||||||
},
|
},
|
||||||
getSelectionBox (event: MouseEvent) {
|
getSelectionBox (event: MouseEvent) {
|
||||||
const {x, y} = this.getMousePositionWithinNodeView(event);
|
const [x, y] = this.getMousePositionWithinNodeView(event);
|
||||||
return {
|
return {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
x: Math.min(x, this.selectBox.x),
|
x: Math.min(x, this.selectBox.x),
|
||||||
|
@ -162,6 +154,10 @@ export const mouseSelect = mixins(
|
||||||
this.nodeSelected(node);
|
this.nodeSelected(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (selectedNodes.length === 1) {
|
||||||
|
this.$store.commit('setLastSelectedNode', selectedNodes[0].name);
|
||||||
|
}
|
||||||
|
|
||||||
this.hideSelectBox();
|
this.hideSelectBox();
|
||||||
},
|
},
|
||||||
mouseMoveSelect (e: MouseEvent) {
|
mouseMoveSelect (e: MouseEvent) {
|
||||||
|
@ -195,6 +191,10 @@ export const mouseSelect = mixins(
|
||||||
this.$store.commit('setLastSelectedNode', null);
|
this.$store.commit('setLastSelectedNode', null);
|
||||||
this.$store.commit('setLastSelectedNodeOutputIndex', null);
|
this.$store.commit('setLastSelectedNodeOutputIndex', null);
|
||||||
this.$store.commit('setActiveNode', null);
|
this.$store.commit('setActiveNode', null);
|
||||||
|
// @ts-ignore
|
||||||
|
this.lastSelectedConnection = null;
|
||||||
|
// @ts-ignore
|
||||||
|
this.newNodeInsertPosition = null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ import mixins from 'vue-typed-mixins';
|
||||||
import normalizeWheel from 'normalize-wheel';
|
import normalizeWheel from 'normalize-wheel';
|
||||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||||
|
import { getMousePosition } from '@/views/canvasHelpers';
|
||||||
|
|
||||||
export const moveNodeWorkflow = mixins(
|
export const moveNodeWorkflow = mixins(
|
||||||
deviceSupportHelpers,
|
deviceSupportHelpers,
|
||||||
|
@ -15,29 +16,18 @@ export const moveNodeWorkflow = mixins(
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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) {
|
moveWorkflow (e: MouseEvent) {
|
||||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
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 nodeViewOffsetPositionX = offsetPosition[0] + (x - this.moveLastPosition[0]);
|
||||||
const nodeViewOffsetPositionY = offsetPosition[1] + (position.y - this.moveLastPosition[1]);
|
const nodeViewOffsetPositionY = offsetPosition[1] + (y - this.moveLastPosition[1]);
|
||||||
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY]});
|
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [nodeViewOffsetPositionX, nodeViewOffsetPositionY]});
|
||||||
|
|
||||||
// Update the last position
|
// Update the last position
|
||||||
this.moveLastPosition[0] = position.x;
|
this.moveLastPosition[0] = x;
|
||||||
this.moveLastPosition[1] = position.y;
|
this.moveLastPosition[1] = y;
|
||||||
},
|
},
|
||||||
mouseDownMoveWorkflow (e: MouseEvent) {
|
mouseDownMoveWorkflow (e: MouseEvent) {
|
||||||
if (this.isCtrlKeyPressed(e) === false) {
|
if (this.isCtrlKeyPressed(e) === false) {
|
||||||
|
@ -53,10 +43,10 @@ export const moveNodeWorkflow = mixins(
|
||||||
|
|
||||||
this.$store.commit('setNodeViewMoveInProgress', true);
|
this.$store.commit('setNodeViewMoveInProgress', true);
|
||||||
|
|
||||||
const position = this.getMousePosition(e);
|
const [x, y] = getMousePosition(e);
|
||||||
|
|
||||||
this.moveLastPosition[0] = position.x;
|
this.moveLastPosition[0] = x;
|
||||||
this.moveLastPosition[1] = position.y;
|
this.moveLastPosition[1] = y;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
|
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import { IConnectionsUi, IEndpointOptions, INodeUi, XYPositon } from '@/Interface';
|
import { IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||||
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE } from '@/constants';
|
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(
|
export const nodeBase = mixins(
|
||||||
deviceSupportHelpers,
|
deviceSupportHelpers,
|
||||||
|
@ -18,145 +24,31 @@ export const nodeBase = mixins(
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
data (): INodeUi {
|
data (): INodeUi {
|
||||||
return this.$store.getters.nodeByName(this.name);
|
return this.$store.getters.getNodeByName(this.name);
|
||||||
},
|
},
|
||||||
hasIssues (): boolean {
|
nodeId (): string {
|
||||||
if (this.data.issues !== undefined && Object.keys(this.data.issues).length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
nodeName (): string {
|
|
||||||
return NODE_NAME_PREFIX + this.nodeIndex;
|
return NODE_NAME_PREFIX + this.nodeIndex;
|
||||||
},
|
},
|
||||||
nodeIndex (): string {
|
nodeIndex (): string {
|
||||||
return this.$store.getters.getNodeIndex(this.data.name).toString();
|
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: [
|
props: [
|
||||||
'name',
|
'name',
|
||||||
'nodeId',
|
|
||||||
'instance',
|
'instance',
|
||||||
'isReadOnly',
|
'isReadOnly',
|
||||||
'isActive',
|
'isActive',
|
||||||
|
'hideActions',
|
||||||
],
|
],
|
||||||
methods: {
|
methods: {
|
||||||
__addNode (node: INodeUi) {
|
__addInputEndpoints (node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
||||||
// 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],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add Inputs
|
// Add Inputs
|
||||||
let index, inputData, anchorPosition;
|
let index;
|
||||||
let newEndpointData: IEndpointOptions;
|
const indexData: {
|
||||||
let indexData: {
|
|
||||||
[key: string]: number;
|
[key: string]: number;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
nodeTypeData.inputs.forEach((inputName: string) => {
|
nodeTypeData.inputs.forEach((inputName: string, i: number) => {
|
||||||
// @ts-ignore
|
|
||||||
inputData = nodeConnectors[inputName].input;
|
|
||||||
|
|
||||||
// Increment the index for inputs with current name
|
// Increment the index for inputs with current name
|
||||||
if (indexData.hasOwnProperty(inputName)) {
|
if (indexData.hasOwnProperty(inputName)) {
|
||||||
indexData[inputName]++;
|
indexData[inputName]++;
|
||||||
|
@ -166,14 +58,15 @@ export const nodeBase = mixins(
|
||||||
index = indexData[inputName];
|
index = indexData[inputName];
|
||||||
|
|
||||||
// Get the position of the anchor depending on how many it has
|
// 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 = {
|
const newEndpointData: IEndpointOptions = {
|
||||||
uuid: `${this.nodeIndex}` + inputData.uuid + index,
|
uuid: CanvasHelpers.getInputEndpointUUID(this.nodeIndex, index),
|
||||||
anchor: anchorPosition,
|
anchor: anchorPosition,
|
||||||
maxConnections: inputData.maxConnections,
|
maxConnections: -1,
|
||||||
endpoint: inputData.endpoint,
|
endpoint: 'Rectangle',
|
||||||
endpointStyle: inputData.endpointStyle,
|
endpointStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
|
||||||
|
endpointHoverStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||||
isSource: false,
|
isSource: false,
|
||||||
isTarget: !this.isReadOnly,
|
isTarget: !this.isReadOnly,
|
||||||
parameters: {
|
parameters: {
|
||||||
|
@ -181,7 +74,8 @@ export const nodeBase = mixins(
|
||||||
type: inputName,
|
type: inputName,
|
||||||
index,
|
index,
|
||||||
},
|
},
|
||||||
dragAllowedWhenFull: inputData.dragAllowedWhenFull,
|
enabled: !this.isReadOnly,
|
||||||
|
dragAllowedWhenFull: true,
|
||||||
dropOptions: {
|
dropOptions: {
|
||||||
tolerance: 'touch',
|
tolerance: 'touch',
|
||||||
hoverClass: 'dropHover',
|
hoverClass: 'dropHover',
|
||||||
|
@ -191,19 +85,15 @@ export const nodeBase = mixins(
|
||||||
if (nodeTypeData.inputNames) {
|
if (nodeTypeData.inputNames) {
|
||||||
// Apply input names if they got set
|
// Apply input names if they got set
|
||||||
newEndpointData.overlays = [
|
newEndpointData.overlays = [
|
||||||
['Label',
|
CanvasHelpers.getInputNameOverlay(nodeTypeData.inputNames[index]),
|
||||||
{
|
|
||||||
id: 'input-name-label',
|
|
||||||
location: [-2, 0.5],
|
|
||||||
label: nodeTypeData.inputNames[index],
|
|
||||||
cssClass: 'node-input-endpoint-label',
|
|
||||||
visible: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 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
|
// 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') {
|
// if (index === 0 && inputName === 'main') {
|
||||||
// // Make the first main-input the default one to connect to when connection gets dropped on node
|
// // 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
|
nodeTypeData.outputs.forEach((inputName: string, i: number) => {
|
||||||
indexData = {};
|
|
||||||
nodeTypeData.outputs.forEach((inputName: string) => {
|
|
||||||
inputData = nodeConnectors[inputName].output;
|
|
||||||
|
|
||||||
// Increment the index for outputs with current name
|
// Increment the index for outputs with current name
|
||||||
if (indexData.hasOwnProperty(inputName)) {
|
if (indexData.hasOwnProperty(inputName)) {
|
||||||
indexData[inputName]++;
|
indexData[inputName]++;
|
||||||
|
@ -231,49 +123,48 @@ export const nodeBase = mixins(
|
||||||
index = indexData[inputName];
|
index = indexData[inputName];
|
||||||
|
|
||||||
// Get the position of the anchor depending on how many it has
|
// 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 = {
|
const newEndpointData: IEndpointOptions = {
|
||||||
uuid: `${this.nodeIndex}` + inputData.uuid + index,
|
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index),
|
||||||
anchor: anchorPosition,
|
anchor: anchorPosition,
|
||||||
maxConnections: inputData.maxConnections,
|
maxConnections: -1,
|
||||||
endpoint: inputData.endpoint,
|
endpoint: 'Dot',
|
||||||
endpointStyle: inputData.endpointStyle,
|
endpointStyle: CanvasHelpers.getOutputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
|
||||||
isSource: !this.isReadOnly,
|
endpointHoverStyle: CanvasHelpers.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||||
|
isSource: true,
|
||||||
isTarget: false,
|
isTarget: false,
|
||||||
|
enabled: !this.isReadOnly,
|
||||||
parameters: {
|
parameters: {
|
||||||
nodeIndex: this.nodeIndex,
|
nodeIndex: this.nodeIndex,
|
||||||
type: inputName,
|
type: inputName,
|
||||||
index,
|
index,
|
||||||
},
|
},
|
||||||
dragAllowedWhenFull: inputData.dragAllowedWhenFull,
|
dragAllowedWhenFull: false,
|
||||||
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
|
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (nodeTypeData.outputNames) {
|
if (nodeTypeData.outputNames) {
|
||||||
// Apply output names if they got set
|
// Apply output names if they got set
|
||||||
newEndpointData.overlays = [
|
newEndpointData.overlays = [
|
||||||
['Label',
|
CanvasHelpers.getOutputNameOverlay(nodeTypeData.outputNames[index]),
|
||||||
{
|
|
||||||
id: 'output-name-label',
|
|
||||||
location: [1.75, 0.5],
|
|
||||||
label: nodeTypeData.outputNames[index],
|
|
||||||
cssClass: 'node-output-endpoint-label',
|
|
||||||
visible: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// TODO: This caused problems with displaying old information
|
||||||
// https://github.com/jsplumb/katavorio/wiki
|
// https://github.com/jsplumb/katavorio/wiki
|
||||||
// https://jsplumb.github.io/jsplumb/home.html
|
// https://jsplumb.github.io/jsplumb/home.html
|
||||||
// Make nodes draggable
|
// Make nodes draggable
|
||||||
this.instance.draggable(this.nodeName, {
|
this.instance.draggable(this.nodeId, {
|
||||||
grid: [10, 10],
|
grid: [CanvasHelpers.GRID_SIZE, CanvasHelpers.GRID_SIZE],
|
||||||
start: (params: { e: MouseEvent }) => {
|
start: (params: { e: MouseEvent }) => {
|
||||||
if (this.isReadOnly === true) {
|
if (this.isReadOnly === true) {
|
||||||
// Do not allow to move nodes in readOnly mode
|
// 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
|
// 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
|
// some dirty DOM query to get the new positions till I have more time to
|
||||||
// create a proper solution
|
// create a proper solution
|
||||||
let newNodePositon: XYPositon;
|
let newNodePositon: XYPosition;
|
||||||
moveNodes.forEach((node: INodeUi) => {
|
moveNodes.forEach((node: INodeUi) => {
|
||||||
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
|
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
|
||||||
const element = document.getElementById(nodeElement);
|
const element = document.getElementById(nodeElement);
|
||||||
|
@ -328,11 +219,23 @@ export const nodeBase = mixins(
|
||||||
|
|
||||||
this.$store.commit('updateNodeProperties', updateInformation);
|
this.$store.commit('updateNodeProperties', updateInformation);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$emit('moved', node);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
|
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) {
|
touchEnd(e: MouseEvent) {
|
||||||
if (this.isTouchDevice) {
|
if (this.isTouchDevice) {
|
||||||
|
|
|
@ -344,6 +344,7 @@ export const nodeHelpers = mixins(
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$store.commit('updateNodeProperties', updateInformation);
|
this.$store.commit('updateNodeProperties', updateInformation);
|
||||||
|
this.$store.commit('clearNodeExecutionData', node.name);
|
||||||
this.updateNodeParameterIssues(node);
|
this.updateNodeParameterIssues(node);
|
||||||
this.updateNodeCredentialIssues(node);
|
this.updateNodeCredentialIssues(node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,8 @@ export const showMessage = mixins(externalHooks, renderText).extend({
|
||||||
return errorMessage;
|
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/>` : '';
|
const messageLine = message ? `${message}<br/>` : '';
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -36,7 +36,7 @@ import {
|
||||||
IWorkflowData,
|
IWorkflowData,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IWorkflowDataUpdate,
|
IWorkflowDataUpdate,
|
||||||
XYPositon,
|
XYPosition,
|
||||||
ITag,
|
ITag,
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
} from '../../Interface';
|
} from '../../Interface';
|
||||||
|
@ -227,7 +227,7 @@ export const workflowHelpers = mixins(
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
getByName: (nodeType: string): INodeType | INodeVersionedType | undefined => {
|
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) {
|
if (nodeTypeDescription === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -238,7 +238,7 @@ export const workflowHelpers = mixins(
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
|
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) {
|
if (nodeTypeDescription === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -331,7 +331,7 @@ export const workflowHelpers = mixins(
|
||||||
|
|
||||||
// Get the data of the node type that we can get the default values
|
// 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
|
// 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) {
|
if (nodeType !== null) {
|
||||||
// Node-Type is known so we can save the parameters correctly
|
// Node-Type is known so we can save the parameters correctly
|
||||||
|
@ -364,11 +364,6 @@ export const workflowHelpers = mixins(
|
||||||
nodeData.credentials = saveCredenetials;
|
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 {
|
} else {
|
||||||
// Node-Type is not known so save the data as it is
|
// Node-Type is not known so save the data as it is
|
||||||
nodeData.credentials = node.credentials;
|
nodeData.credentials = node.credentials;
|
||||||
|
@ -570,7 +565,7 @@ export const workflowHelpers = mixins(
|
||||||
|
|
||||||
// Updates the position of all the nodes that the top-left node
|
// Updates the position of all the nodes that the top-left node
|
||||||
// is at the given position
|
// is at the given position
|
||||||
updateNodePositions (workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPositon): void {
|
updateNodePositions (workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPosition): void {
|
||||||
if (workflowData.nodes === undefined) {
|
if (workflowData.nodes === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,6 +195,7 @@ export const workflowRun = mixins(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.$store.commit('setWorkflowExecutionData', executionData);
|
this.$store.commit('setWorkflowExecutionData', executionData);
|
||||||
|
this.updateNodesExecutionIssues();
|
||||||
|
|
||||||
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
|
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
|
||||||
|
|
||||||
|
|
|
@ -116,3 +116,4 @@ export const COMPANY_SIZE_PERSONAL_USE = 'personalUser';
|
||||||
|
|
||||||
export const CODING_SKILL_KEY = 'codingSkill';
|
export const CODING_SKILL_KEY = 'codingSkill';
|
||||||
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,6 @@ $--badge-warning-color: #6b5900;
|
||||||
// Warning tooltip
|
// Warning tooltip
|
||||||
$--warning-tooltip-color: #ff8080;
|
$--warning-tooltip-color: #ff8080;
|
||||||
|
|
||||||
$--custom-node-view-background : #faf9fe;
|
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
$--custom-table-background-main: $--custom-header-background;
|
$--custom-table-background-main: $--custom-header-background;
|
||||||
$--custom-table-background-stripe-color: #f6f6f6;
|
$--custom-table-background-stripe-color: #f6f6f6;
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
@import "~n8n-design-system/theme/dist/index.css";
|
@import "~n8n-design-system/theme/dist/index.css";
|
||||||
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: $--custom-node-view-background;
|
background-color: var(--color-canvas-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
|
|
779
packages/editor-ui/src/plugins/N8nCustomConnectorType.js
Normal file
779
packages/editor-ui/src/plugins/N8nCustomConnectorType.js
Normal file
|
@ -0,0 +1,779 @@
|
||||||
|
/**
|
||||||
|
* Custom connector type
|
||||||
|
* Based on jsplumb Flowchart and Bezier types
|
||||||
|
*
|
||||||
|
* Source GitHub repository:
|
||||||
|
* https://github.com/jsplumb/jsplumb
|
||||||
|
*
|
||||||
|
* Source files:
|
||||||
|
* https://github.com/jsplumb/jsplumb/blob/fb5fce52794fa52306825bdaa62bf3855cdfd7e0/src/connectors-flowchart.js
|
||||||
|
* https://github.com/jsplumb/jsplumb/blob/fb5fce52794fa52306825bdaa62bf3855cdfd7e0/src/connectors-bezier.js
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* All 1.x.x and 2.x.x versions of jsPlumb Community edition, and so also the
|
||||||
|
* content of this file, are dual-licensed under both MIT and GPLv2.
|
||||||
|
*
|
||||||
|
* MIT LICENSE
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010 - 2014 jsPlumb, http://jsplumbtoolkit.com/
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* ===============================================================================
|
||||||
|
* GNU GENERAL PUBLIC LICENSE
|
||||||
|
* Version 2, June 1991
|
||||||
|
*
|
||||||
|
* Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
* Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
* of this license document, but changing it is not allowed.
|
||||||
|
*
|
||||||
|
* Preamble
|
||||||
|
*
|
||||||
|
* The licenses for most software are designed to take away your
|
||||||
|
* freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
* License is intended to guarantee your freedom to share and change free
|
||||||
|
* software--to make sure the software is free for all its users. This
|
||||||
|
* General Public License applies to most of the Free Software
|
||||||
|
* Foundation's software and to any other program whose authors commit to
|
||||||
|
* using it. (Some other Free Software Foundation software is covered by
|
||||||
|
* the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
* your programs, too.
|
||||||
|
*
|
||||||
|
* When we speak of free software, we are referring to freedom, not
|
||||||
|
* price. Our General Public Licenses are designed to make sure that you
|
||||||
|
* have the freedom to distribute copies of free software (and charge for
|
||||||
|
* this service if you wish), that you receive source code or can get it
|
||||||
|
* if you want it, that you can change the software or use pieces of it
|
||||||
|
* in new free programs; and that you know you can do these things.
|
||||||
|
*
|
||||||
|
* To protect your rights, we need to make restrictions that forbid
|
||||||
|
* anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
* These restrictions translate to certain responsibilities for you if you
|
||||||
|
* distribute copies of the software, or if you modify it.
|
||||||
|
*
|
||||||
|
* For example, if you distribute copies of such a program, whether
|
||||||
|
* gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
* you have. You must make sure that they, too, receive or can get the
|
||||||
|
* source code. And you must show them these terms so they know their
|
||||||
|
* rights.
|
||||||
|
*
|
||||||
|
* We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
* (2) offer you this license which gives you legal permission to copy,
|
||||||
|
* distribute and/or modify the software.
|
||||||
|
*
|
||||||
|
* Also, for each author's protection and ours, we want to make certain
|
||||||
|
* that everyone understands that there is no warranty for this free
|
||||||
|
* software. If the software is modified by someone else and passed on, we
|
||||||
|
* want its recipients to know that what they have is not the original, so
|
||||||
|
* that any problems introduced by others will not reflect on the original
|
||||||
|
* authors' reputations.
|
||||||
|
*
|
||||||
|
* Finally, any free program is threatened constantly by software
|
||||||
|
* patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
* program will individually obtain patent licenses, in effect making the
|
||||||
|
* program proprietary. To prevent this, we have made it clear that any
|
||||||
|
* patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
*
|
||||||
|
* The precise terms and conditions for copying, distribution and
|
||||||
|
* modification follow.
|
||||||
|
*
|
||||||
|
* GNU GENERAL PUBLIC LICENSE
|
||||||
|
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
*
|
||||||
|
* 0. This License applies to any program or other work which contains
|
||||||
|
* a notice placed by the copyright holder saying it may be distributed
|
||||||
|
* under the terms of this General Public License. The "Program", below,
|
||||||
|
* refers to any such program or work, and a "work based on the Program"
|
||||||
|
* means either the Program or any derivative work under copyright law:
|
||||||
|
* that is to say, a work containing the Program or a portion of it,
|
||||||
|
* either verbatim or with modifications and/or translated into another
|
||||||
|
* language. (Hereinafter, translation is included without limitation in
|
||||||
|
* the term "modification".) Each licensee is addressed as "you".
|
||||||
|
*
|
||||||
|
* Activities other than copying, distribution and modification are not
|
||||||
|
* covered by this License; they are outside its scope. The act of
|
||||||
|
* running the Program is not restricted, and the output from the Program
|
||||||
|
* is covered only if its contents constitute a work based on the
|
||||||
|
* Program (independent of having been made by running the Program).
|
||||||
|
* Whether that is true depends on what the Program does.
|
||||||
|
*
|
||||||
|
* 1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
* source code as you receive it, in any medium, provided that you
|
||||||
|
* conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
* copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
* notices that refer to this License and to the absence of any warranty;
|
||||||
|
* and give any other recipients of the Program a copy of this License
|
||||||
|
* along with the Program.
|
||||||
|
*
|
||||||
|
* You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
* you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
*
|
||||||
|
* 2. You may modify your copy or copies of the Program or any portion
|
||||||
|
* of it, thus forming a work based on the Program, and copy and
|
||||||
|
* distribute such modifications or work under the terms of Section 1
|
||||||
|
* above, provided that you also meet all of these conditions:
|
||||||
|
*
|
||||||
|
* a) You must cause the modified files to carry prominent notices
|
||||||
|
* stating that you changed the files and the date of any change.
|
||||||
|
*
|
||||||
|
* b) You must cause any work that you distribute or publish, that in
|
||||||
|
* whole or in part contains or is derived from the Program or any
|
||||||
|
* part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
* parties under the terms of this License.
|
||||||
|
*
|
||||||
|
* c) If the modified program normally reads commands interactively
|
||||||
|
* when run, you must cause it, when started running for such
|
||||||
|
* interactive use in the most ordinary way, to print or display an
|
||||||
|
* announcement including an appropriate copyright notice and a
|
||||||
|
* notice that there is no warranty (or else, saying that you provide
|
||||||
|
* a warranty) and that users may redistribute the program under
|
||||||
|
* these conditions, and telling the user how to view a copy of this
|
||||||
|
* License. (Exception: if the Program itself is interactive but
|
||||||
|
* does not normally print such an announcement, your work based on
|
||||||
|
* the Program is not required to print an announcement.)
|
||||||
|
*
|
||||||
|
* These requirements apply to the modified work as a whole. If
|
||||||
|
* identifiable sections of that work are not derived from the Program,
|
||||||
|
* and can be reasonably considered independent and separate works in
|
||||||
|
* themselves, then this License, and its terms, do not apply to those
|
||||||
|
* sections when you distribute them as separate works. But when you
|
||||||
|
* distribute the same sections as part of a whole which is a work based
|
||||||
|
* on the Program, the distribution of the whole must be on the terms of
|
||||||
|
* this License, whose permissions for other licensees extend to the
|
||||||
|
* entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
*
|
||||||
|
* Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
* your rights to work written entirely by you; rather, the intent is to
|
||||||
|
* exercise the right to control the distribution of derivative or
|
||||||
|
* collective works based on the Program.
|
||||||
|
*
|
||||||
|
* In addition, mere aggregation of another work not based on the Program
|
||||||
|
* with the Program (or with a work based on the Program) on a volume of
|
||||||
|
* a storage or distribution medium does not bring the other work under
|
||||||
|
* the scope of this License.
|
||||||
|
*
|
||||||
|
* 3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
* under Section 2) in object code or executable form under the terms of
|
||||||
|
* Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
*
|
||||||
|
* a) Accompany it with the complete corresponding machine-readable
|
||||||
|
* source code, which must be distributed under the terms of Sections
|
||||||
|
* 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
*
|
||||||
|
* b) Accompany it with a written offer, valid for at least three
|
||||||
|
* years, to give any third party, for a charge no more than your
|
||||||
|
* cost of physically performing source distribution, a complete
|
||||||
|
* machine-readable copy of the corresponding source code, to be
|
||||||
|
* distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
* customarily used for software interchange; or,
|
||||||
|
*
|
||||||
|
* c) Accompany it with the information you received as to the offer
|
||||||
|
* to distribute corresponding source code. (This alternative is
|
||||||
|
* allowed only for noncommercial distribution and only if you
|
||||||
|
* received the program in object code or executable form with such
|
||||||
|
* an offer, in accord with Subsection b above.)
|
||||||
|
*
|
||||||
|
* The source code for a work means the preferred form of the work for
|
||||||
|
* making modifications to it. For an executable work, complete source
|
||||||
|
* code means all the source code for all modules it contains, plus any
|
||||||
|
* associated interface definition files, plus the scripts used to
|
||||||
|
* control compilation and installation of the executable. However, as a
|
||||||
|
* special exception, the source code distributed need not include
|
||||||
|
* anything that is normally distributed (in either source or binary
|
||||||
|
* form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
* operating system on which the executable runs, unless that component
|
||||||
|
* itself accompanies the executable.
|
||||||
|
*
|
||||||
|
* If distribution of executable or object code is made by offering
|
||||||
|
* access to copy from a designated place, then offering equivalent
|
||||||
|
* access to copy the source code from the same place counts as
|
||||||
|
* distribution of the source code, even though third parties are not
|
||||||
|
* compelled to copy the source along with the object code.
|
||||||
|
*
|
||||||
|
* 4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
* except as expressly provided under this License. Any attempt
|
||||||
|
* otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
* void, and will automatically terminate your rights under this License.
|
||||||
|
* However, parties who have received copies, or rights, from you under
|
||||||
|
* this License will not have their licenses terminated so long as such
|
||||||
|
* parties remain in full compliance.
|
||||||
|
*
|
||||||
|
* 5. You are not required to accept this License, since you have not
|
||||||
|
* signed it. However, nothing else grants you permission to modify or
|
||||||
|
* distribute the Program or its derivative works. These actions are
|
||||||
|
* prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
* modifying or distributing the Program (or any work based on the
|
||||||
|
* Program), you indicate your acceptance of this License to do so, and
|
||||||
|
* all its terms and conditions for copying, distributing or modifying
|
||||||
|
* the Program or works based on it.
|
||||||
|
*
|
||||||
|
* 6. Each time you redistribute the Program (or any work based on the
|
||||||
|
* Program), the recipient automatically receives a license from the
|
||||||
|
* original licensor to copy, distribute or modify the Program subject to
|
||||||
|
* these terms and conditions. You may not impose any further
|
||||||
|
* restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
* You are not responsible for enforcing compliance by third parties to
|
||||||
|
* this License.
|
||||||
|
*
|
||||||
|
* 7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
* infringement or for any other reason (not limited to patent issues),
|
||||||
|
* conditions are imposed on you (whether by court order, agreement or
|
||||||
|
* otherwise) that contradict the conditions of this License, they do not
|
||||||
|
* excuse you from the conditions of this License. If you cannot
|
||||||
|
* distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
* License and any other pertinent obligations, then as a consequence you
|
||||||
|
* may not distribute the Program at all. For example, if a patent
|
||||||
|
* license would not permit royalty-free redistribution of the Program by
|
||||||
|
* all those who receive copies directly or indirectly through you, then
|
||||||
|
* the only way you could satisfy both it and this License would be to
|
||||||
|
* refrain entirely from distribution of the Program.
|
||||||
|
*
|
||||||
|
* If any portion of this section is held invalid or unenforceable under
|
||||||
|
* any particular circumstance, the balance of the section is intended to
|
||||||
|
* apply and the section as a whole is intended to apply in other
|
||||||
|
* circumstances.
|
||||||
|
*
|
||||||
|
* It is not the purpose of this section to induce you to infringe any
|
||||||
|
* patents or other property right claims or to contest validity of any
|
||||||
|
* such claims; this section has the sole purpose of protecting the
|
||||||
|
* integrity of the free software distribution system, which is
|
||||||
|
* implemented by public license practices. Many people have made
|
||||||
|
* generous contributions to the wide range of software distributed
|
||||||
|
* through that system in reliance on consistent application of that
|
||||||
|
* system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
* to distribute software through any other system and a licensee cannot
|
||||||
|
* impose that choice.
|
||||||
|
*
|
||||||
|
* This section is intended to make thoroughly clear what is believed to
|
||||||
|
* be a consequence of the rest of this License.
|
||||||
|
*
|
||||||
|
* 8. If the distribution and/or use of the Program is restricted in
|
||||||
|
* certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
* original copyright holder who places the Program under this License
|
||||||
|
* may add an explicit geographical distribution limitation excluding
|
||||||
|
* those countries, so that distribution is permitted only in or among
|
||||||
|
* countries not thus excluded. In such case, this License incorporates
|
||||||
|
* the limitation as if written in the body of this License.
|
||||||
|
*
|
||||||
|
* 9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
* of the General Public License from time to time. Such new versions will
|
||||||
|
* be similar in spirit to the present version, but may differ in detail to
|
||||||
|
* address new problems or concerns.
|
||||||
|
*
|
||||||
|
* Each version is given a distinguishing version number. If the Program
|
||||||
|
* specifies a version number of this License which applies to it and "any
|
||||||
|
* later version", you have the option of following the terms and conditions
|
||||||
|
* either of that version or of any later version published by the Free
|
||||||
|
* Software Foundation. If the Program does not specify a version number of
|
||||||
|
* this License, you may choose any version ever published by the Free Software
|
||||||
|
* Foundation.
|
||||||
|
*
|
||||||
|
* 10. If you wish to incorporate parts of the Program into other free
|
||||||
|
* programs whose distribution conditions are different, write to the author
|
||||||
|
* to ask for permission. For software which is copyrighted by the Free
|
||||||
|
* Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
* make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
* of preserving the free status of all derivatives of our free software and
|
||||||
|
* of promoting the sharing and reuse of software generally.
|
||||||
|
*
|
||||||
|
* NO WARRANTY
|
||||||
|
*
|
||||||
|
* 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
* FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
* OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
* PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
* OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
* TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
* PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
* REPAIR OR CORRECTION.
|
||||||
|
*
|
||||||
|
* 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
* WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
* REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
* INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
* OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
* TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
* YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
* PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
var root = this, _jp = root.jsPlumb, _ju = root.jsPlumbUtil, _jg = root.Biltong;
|
||||||
|
var STRAIGHT = "Straight";
|
||||||
|
var ARC = "Arc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom connector type
|
||||||
|
*
|
||||||
|
* @param stub {number} length of stub segments in flowchart
|
||||||
|
* @param getEndpointOffset {Function} callback to offset stub length based on endpoint in flowchart
|
||||||
|
* @param midpoint {number} float percent of halfway point of segments in flowchart
|
||||||
|
* @param loopbackVerticalLength {number} height of vertical segment when looping in flowchart
|
||||||
|
* @param cornerRadius {number} radius of flowchart connectors
|
||||||
|
* @param loopbackMinimum {number} minimum threshold before looping behavior takes effect in flowchart
|
||||||
|
* @param targetGap {number} gap between connector and target endpoint in both flowchart and bezier
|
||||||
|
*/
|
||||||
|
const N8nCustom = function (params) {
|
||||||
|
params = params || {};
|
||||||
|
this.type = "N8nCustom";
|
||||||
|
|
||||||
|
params.stub = params.stub == null ? 30 : params.stub;
|
||||||
|
|
||||||
|
var _super = _jp.Connectors.AbstractConnector.apply(this, arguments),
|
||||||
|
minorAnchor = 0, // seems to be angle at which connector leaves endpoint
|
||||||
|
majorAnchor = 0, // translates to curviness of bezier curve
|
||||||
|
segments,
|
||||||
|
midpoint = params.midpoint == null ? 0.5 : params.midpoint,
|
||||||
|
alwaysRespectStubs = params.alwaysRespectStubs === true,
|
||||||
|
loopbackVerticalLength = params.loopbackVerticalLength || 0,
|
||||||
|
lastx = null, lasty = null,
|
||||||
|
cornerRadius = params.cornerRadius != null ? params.cornerRadius : 0,
|
||||||
|
loopbackMinimum = params.loopbackMinimum || 100,
|
||||||
|
curvinessCoeffient = 0.4,
|
||||||
|
zBezierOffset = 40,
|
||||||
|
targetGap = params.targetGap || 0,
|
||||||
|
stub = params.stub || 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set target endpoint
|
||||||
|
* (to override default behavior tracking mouse when dragging mouse)
|
||||||
|
* @param {Endpoint} endpoint
|
||||||
|
*/
|
||||||
|
this.setTargetEndpoint = function (endpoint) {
|
||||||
|
this.overrideTargetEndpoint = endpoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reset target endpoint overriding default behavior
|
||||||
|
*/
|
||||||
|
this.resetTargetEndpoint = function () {
|
||||||
|
this.overrideTargetEndpoint = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
this._compute = function (originalPaintInfo, connParams) {
|
||||||
|
const paintInfo = _getPaintInfo(connParams, { targetGap, stub, overrideTargetEndpoint: this.overrideTargetEndpoint, getEndpointOffset: params.getEndpointOffset });
|
||||||
|
Object.keys(paintInfo).forEach((key) => {
|
||||||
|
// override so that bounding box is calculated correctly wheen target override is set
|
||||||
|
originalPaintInfo[key] = paintInfo[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paintInfo.tx < 0) {
|
||||||
|
this._computeFlowchart(paintInfo);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._computeBezier(paintInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._computeBezier = function (paintInfo) {
|
||||||
|
var sp = paintInfo.sourcePos,
|
||||||
|
tp = paintInfo.targetPos,
|
||||||
|
_w = Math.abs(sp[0] - tp[0]) - paintInfo.targetGap,
|
||||||
|
_h = Math.abs(sp[1] - tp[1]);
|
||||||
|
|
||||||
|
var _CP, _CP2,
|
||||||
|
_sx = sp[0] < tp[0] ? _w : 0,
|
||||||
|
_sy = sp[1] < tp[1] ? _h : 0,
|
||||||
|
_tx = sp[0] < tp[0] ? 0 : _w,
|
||||||
|
_ty = sp[1] < tp[1] ? 0 : _h;
|
||||||
|
|
||||||
|
if (paintInfo.ySpan <= 20 || (paintInfo.ySpan <= 100 && paintInfo.xSpan <= 100)) {
|
||||||
|
majorAnchor = 0.1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
majorAnchor = paintInfo.xSpan * curvinessCoeffient + zBezierOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
_CP = _findControlPoint([_sx, _sy], sp, tp, paintInfo.sourceEndpoint, paintInfo.targetEndpoint, paintInfo.so, paintInfo.to, majorAnchor, minorAnchor);
|
||||||
|
_CP2 = _findControlPoint([_tx, _ty], tp, sp, paintInfo.targetEndpoint, paintInfo.sourceEndpoint, paintInfo.to, paintInfo.so, majorAnchor, minorAnchor);
|
||||||
|
|
||||||
|
_super.addSegment(this, "Bezier", {
|
||||||
|
x1: _sx, y1: _sy, x2: _tx, y2: _ty,
|
||||||
|
cp1x: _CP[0], cp1y: _CP[1], cp2x: _CP2[0], cp2y: _CP2[1],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helper method to add a segment.
|
||||||
|
*/
|
||||||
|
const addFlowchartSegment = function (segments, x, y, paintInfo) {
|
||||||
|
if (lastx === x && lasty === y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var lx = lastx == null ? paintInfo.sx : lastx,
|
||||||
|
ly = lasty == null ? paintInfo.sy : lasty,
|
||||||
|
o = lx === x ? "v" : "h";
|
||||||
|
|
||||||
|
lastx = x;
|
||||||
|
lasty = y;
|
||||||
|
segments.push([ lx, ly, x, y, o ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
this._computeFlowchart = function (paintInfo) {
|
||||||
|
|
||||||
|
segments = [];
|
||||||
|
lastx = null;
|
||||||
|
lasty = null;
|
||||||
|
|
||||||
|
// calculate Stubs.
|
||||||
|
var stubs = calcualteStubSegment(paintInfo, {alwaysRespectStubs});
|
||||||
|
|
||||||
|
// add the start stub segment. use stubs for loopback as it will look better, with the loop spaced
|
||||||
|
// away from the element.
|
||||||
|
addFlowchartSegment(segments, stubs[0], stubs[1], paintInfo);
|
||||||
|
|
||||||
|
// compute the rest of the line
|
||||||
|
var p = calculateLineSegment(paintInfo, stubs, {midpoint, loopbackMinimum, loopbackVerticalLength});
|
||||||
|
if (p) {
|
||||||
|
for (var i = 0; i < p.length; i++) {
|
||||||
|
addFlowchartSegment(segments, p[i][0], p[i][1], paintInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// line to end stub
|
||||||
|
addFlowchartSegment(segments, stubs[2], stubs[3], paintInfo);
|
||||||
|
|
||||||
|
// end stub to end (common)
|
||||||
|
addFlowchartSegment(segments, paintInfo.tx, paintInfo.ty, paintInfo);
|
||||||
|
|
||||||
|
// write out the segments.
|
||||||
|
writeFlowchartSegments(_super, this, segments, paintInfo, cornerRadius);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
_jp.Connectors.N8nCustom = N8nCustom;
|
||||||
|
_ju.extend(_jp.Connectors.N8nCustom, _jp.Connectors.AbstractConnector);
|
||||||
|
|
||||||
|
|
||||||
|
function _findControlPoint(point, sourceAnchorPosition, targetAnchorPosition, sourceEndpoint, targetEndpoint, soo, too, majorAnchor, minorAnchor) {
|
||||||
|
// determine if the two anchors are perpendicular to each other in their orientation. we swap the control
|
||||||
|
// points around if so (code could be tightened up)
|
||||||
|
var perpendicular = soo[0] !== too[0] || soo[1] === too[1],
|
||||||
|
p = [];
|
||||||
|
|
||||||
|
if (!perpendicular) {
|
||||||
|
if (soo[0] === 0) {
|
||||||
|
p.push(sourceAnchorPosition[0] < targetAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
p.push(point[0] - (majorAnchor * soo[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soo[1] === 0) {
|
||||||
|
p.push(sourceAnchorPosition[1] < targetAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
p.push(point[1] + (majorAnchor * too[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (too[0] === 0) {
|
||||||
|
p.push(targetAnchorPosition[0] < sourceAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
p.push(point[0] + (majorAnchor * too[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (too[1] === 0) {
|
||||||
|
p.push(targetAnchorPosition[1] < sourceAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
p.push(point[1] + (majorAnchor * soo[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function sgn(n) {
|
||||||
|
return n < 0 ? -1 : n === 0 ? 0 : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getFlowchartSegmentDirections(segment) {
|
||||||
|
return [
|
||||||
|
sgn( segment[2] - segment[0] ),
|
||||||
|
sgn( segment[3] - segment[1] ),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSegmentLength(s) {
|
||||||
|
return Math.sqrt(Math.pow(s[0] - s[2], 2) + Math.pow(s[1] - s[3], 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
function _cloneArray(a) {
|
||||||
|
var _a = [];
|
||||||
|
_a.push.apply(_a, a);
|
||||||
|
return _a;
|
||||||
|
};
|
||||||
|
|
||||||
|
function writeFlowchartSegments(_super, conn, segments, paintInfo, cornerRadius) {
|
||||||
|
var current = null, next, currentDirection, nextDirection;
|
||||||
|
for (var i = 0; i < segments.length - 1; i++) {
|
||||||
|
|
||||||
|
current = current || _cloneArray(segments[i]);
|
||||||
|
next = _cloneArray(segments[i + 1]);
|
||||||
|
|
||||||
|
currentDirection = getFlowchartSegmentDirections(current);
|
||||||
|
nextDirection = getFlowchartSegmentDirections(next);
|
||||||
|
|
||||||
|
if (cornerRadius > 0 && current[4] !== next[4]) {
|
||||||
|
|
||||||
|
var minSegLength = Math.min(getSegmentLength(current), getSegmentLength(next));
|
||||||
|
var radiusToUse = Math.min(cornerRadius, minSegLength / 2);
|
||||||
|
|
||||||
|
current[2] -= currentDirection[0] * radiusToUse;
|
||||||
|
current[3] -= currentDirection[1] * radiusToUse;
|
||||||
|
next[0] += nextDirection[0] * radiusToUse;
|
||||||
|
next[1] += nextDirection[1] * radiusToUse;
|
||||||
|
|
||||||
|
var ac = (currentDirection[1] === nextDirection[0] && nextDirection[0] === 1) ||
|
||||||
|
((currentDirection[1] === nextDirection[0] && nextDirection[0] === 0) && currentDirection[0] !== nextDirection[1]) ||
|
||||||
|
(currentDirection[1] === nextDirection[0] && nextDirection[0] === -1),
|
||||||
|
sgny = next[1] > current[3] ? 1 : -1,
|
||||||
|
sgnx = next[0] > current[2] ? 1 : -1,
|
||||||
|
sgnEqual = sgny === sgnx,
|
||||||
|
cx = (sgnEqual && ac || (!sgnEqual && !ac)) ? next[0] : current[2],
|
||||||
|
cy = (sgnEqual && ac || (!sgnEqual && !ac)) ? current[3] : next[1];
|
||||||
|
|
||||||
|
_super.addSegment(conn, STRAIGHT, {
|
||||||
|
x1: current[0], y1: current[1], x2: current[2], y2: current[3],
|
||||||
|
});
|
||||||
|
|
||||||
|
_super.addSegment(conn, ARC, {
|
||||||
|
r: radiusToUse,
|
||||||
|
x1: current[2],
|
||||||
|
y1: current[3],
|
||||||
|
x2: next[0],
|
||||||
|
y2: next[1],
|
||||||
|
cx: cx,
|
||||||
|
cy: cy,
|
||||||
|
ac: ac,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// dx + dy are used to adjust for line width.
|
||||||
|
var dx = (current[2] === current[0]) ? 0 : (current[2] > current[0]) ? (paintInfo.lw / 2) : -(paintInfo.lw / 2),
|
||||||
|
dy = (current[3] === current[1]) ? 0 : (current[3] > current[1]) ? (paintInfo.lw / 2) : -(paintInfo.lw / 2);
|
||||||
|
|
||||||
|
_super.addSegment(conn, STRAIGHT, {
|
||||||
|
x1: current[0] - dx, y1: current[1] - dy, x2: current[2] + dx, y2: current[3] + dy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
if (next != null) {
|
||||||
|
// last segment
|
||||||
|
_super.addSegment(conn, STRAIGHT, {
|
||||||
|
x1: next[0], y1: next[1], x2: next[2], y2: next[3],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineCalculators = {
|
||||||
|
opposite: function (paintInfo, {axis, startStub, endStub, idx, midx, midy}) {
|
||||||
|
var pi = paintInfo,
|
||||||
|
comparator = pi["is" + axis.toUpperCase() + "GreaterThanStubTimes2"];
|
||||||
|
|
||||||
|
if (!comparator || (pi.so[idx] === 1 && startStub > endStub) || (pi.so[idx] === -1 && startStub < endStub)) {
|
||||||
|
return {
|
||||||
|
"x": [
|
||||||
|
[startStub, midy],
|
||||||
|
[endStub, midy],
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
[midx, startStub],
|
||||||
|
[midx, endStub],
|
||||||
|
],
|
||||||
|
}[axis];
|
||||||
|
}
|
||||||
|
else if ((pi.so[idx] === 1 && startStub < endStub) || (pi.so[idx] === -1 && startStub > endStub)) {
|
||||||
|
return {
|
||||||
|
"x": [
|
||||||
|
[midx, pi.sy],
|
||||||
|
[midx, pi.ty],
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
[pi.sx, midy],
|
||||||
|
[pi.tx, midy],
|
||||||
|
],
|
||||||
|
}[axis];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const stubCalculators = {
|
||||||
|
opposite: function (paintInfo, {axis, alwaysRespectStubs}) {
|
||||||
|
var pi = paintInfo,
|
||||||
|
idx = axis === "x" ? 0 : 1,
|
||||||
|
areInProximity = {
|
||||||
|
"x": function () {
|
||||||
|
return ( (pi.so[idx] === 1 && (
|
||||||
|
( (pi.startStubX > pi.endStubX) && (pi.tx > pi.startStubX) ) ||
|
||||||
|
( (pi.sx > pi.endStubX) && (pi.tx > pi.sx))))) ||
|
||||||
|
|
||||||
|
( (pi.so[idx] === -1 && (
|
||||||
|
( (pi.startStubX < pi.endStubX) && (pi.tx < pi.startStubX) ) ||
|
||||||
|
( (pi.sx < pi.endStubX) && (pi.tx < pi.sx)))));
|
||||||
|
},
|
||||||
|
"y": function () {
|
||||||
|
return ( (pi.so[idx] === 1 && (
|
||||||
|
( (pi.startStubY > pi.endStubY) && (pi.ty > pi.startStubY) ) ||
|
||||||
|
( (pi.sy > pi.endStubY) && (pi.ty > pi.sy))))) ||
|
||||||
|
|
||||||
|
( (pi.so[idx] === -1 && (
|
||||||
|
( (pi.startStubY < pi.endStubY) && (pi.ty < pi.startStubY) ) ||
|
||||||
|
( (pi.sy < pi.endStubY) && (pi.ty < pi.sy)))));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!alwaysRespectStubs && areInProximity[axis]()) {
|
||||||
|
return {
|
||||||
|
"x": [(paintInfo.sx + paintInfo.tx) / 2, paintInfo.startStubY, (paintInfo.sx + paintInfo.tx) / 2, paintInfo.endStubY],
|
||||||
|
"y": [paintInfo.startStubX, (paintInfo.sy + paintInfo.ty) / 2, paintInfo.endStubX, (paintInfo.sy + paintInfo.ty) / 2],
|
||||||
|
}[axis];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [paintInfo.startStubX, paintInfo.startStubY, paintInfo.endStubX, paintInfo.endStubY];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function calcualteStubSegment(paintInfo, {alwaysRespectStubs}) {
|
||||||
|
return stubCalculators['opposite'](paintInfo, {axis: paintInfo.sourceAxis, alwaysRespectStubs});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLineSegment(paintInfo, stubs, { midpoint, loopbackVerticalLength, loopbackMinimum }) {
|
||||||
|
const axis = paintInfo.sourceAxis,
|
||||||
|
idx = paintInfo.sourceAxis === "x" ? 0 : 1,
|
||||||
|
oidx = paintInfo.sourceAxis === "x" ? 1 : 0,
|
||||||
|
startStub = stubs[idx],
|
||||||
|
otherStartStub = stubs[oidx],
|
||||||
|
endStub = stubs[idx + 2],
|
||||||
|
otherEndStub = stubs[oidx + 2];
|
||||||
|
|
||||||
|
const diffX = paintInfo.endStubX - paintInfo.startStubX;
|
||||||
|
const diffY = paintInfo.endStubY - paintInfo.startStubY;
|
||||||
|
const direction = diffY >= 0 ? 1 : -1; // vertical direction of loop, above or below source
|
||||||
|
|
||||||
|
var midx = paintInfo.startStubX + ((paintInfo.endStubX - paintInfo.startStubX) * midpoint),
|
||||||
|
midy;
|
||||||
|
|
||||||
|
if (diffX < (-1 * loopbackMinimum)) {
|
||||||
|
// loop backward behavior
|
||||||
|
midy = paintInfo.startStubY - (diffX < 0 ? direction * loopbackVerticalLength : 0);
|
||||||
|
} else {
|
||||||
|
// original flowchart behavior
|
||||||
|
midy = paintInfo.startStubY + ((paintInfo.endStubY - paintInfo.startStubY) * midpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineCalculators['opposite'](paintInfo, {axis, startStub, otherStartStub, endStub, otherEndStub, idx, oidx, midx, midy});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getPaintInfo(params, { targetGap, stub, overrideTargetEndpoint, getEndpointOffset }) {
|
||||||
|
let { targetPos, targetEndpoint } = params;
|
||||||
|
|
||||||
|
if (
|
||||||
|
overrideTargetEndpoint
|
||||||
|
) {
|
||||||
|
targetPos = overrideTargetEndpoint.anchor.getCurrentLocation();
|
||||||
|
targetEndpoint = overrideTargetEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceGap = 0;
|
||||||
|
|
||||||
|
stub = stub || 0;
|
||||||
|
const sourceStub = _ju.isArray(stub) ? stub[0] : stub;
|
||||||
|
const targetStub = _ju.isArray(stub) ? stub[1] : stub;
|
||||||
|
|
||||||
|
var segment = _jg.quadrant(params.sourcePos, targetPos),
|
||||||
|
swapX = targetPos[0] < params.sourcePos[0],
|
||||||
|
swapY = targetPos[1] < params.sourcePos[1],
|
||||||
|
lw = params.strokeWidth || 1,
|
||||||
|
so = params.sourceEndpoint.anchor.getOrientation(params.sourceEndpoint), // source orientation
|
||||||
|
to = targetEndpoint.anchor.getOrientation(targetEndpoint), // target orientation
|
||||||
|
x = swapX ? targetPos[0] : params.sourcePos[0],
|
||||||
|
y = swapY ? targetPos[1] : params.sourcePos[1],
|
||||||
|
w = Math.abs(targetPos[0] - params.sourcePos[0]),
|
||||||
|
h = Math.abs(targetPos[1] - params.sourcePos[1]);
|
||||||
|
|
||||||
|
// if either anchor does not have an orientation set, we derive one from their relative
|
||||||
|
// positions. we fix the axis to be the one in which the two elements are further apart, and
|
||||||
|
// point each anchor at the other element. this is also used when dragging a new connection.
|
||||||
|
if (so[0] === 0 && so[1] === 0 || to[0] === 0 && to[1] === 0) {
|
||||||
|
var index = w > h ? 0 : 1, oIndex = [1, 0][index];
|
||||||
|
so = [];
|
||||||
|
to = [];
|
||||||
|
so[index] = params.sourcePos[index] > targetPos[index] ? -1 : 1;
|
||||||
|
to[index] = params.sourcePos[index] > targetPos[index] ? 1 : -1;
|
||||||
|
so[oIndex] = 0;
|
||||||
|
to[oIndex] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sx = swapX ? w + (sourceGap * so[0]) : sourceGap * so[0],
|
||||||
|
sy = swapY ? h + (sourceGap * so[1]) : sourceGap * so[1],
|
||||||
|
tx = swapX ? targetGap * to[0] : w + (targetGap * to[0]),
|
||||||
|
ty = swapY ? targetGap * to[1] : h + (targetGap * to[1]),
|
||||||
|
oProduct = ((so[0] * to[0]) + (so[1] * to[1]));
|
||||||
|
|
||||||
|
const sourceStubWithOffset = sourceStub + (getEndpointOffset && params.sourceEndpoint ? getEndpointOffset(params.sourceEndpoint) : 0);
|
||||||
|
const targetStubWithOffset = targetStub + (getEndpointOffset && targetEndpoint ? getEndpointOffset(targetEndpoint) : 0);
|
||||||
|
|
||||||
|
// same as paintinfo generated by jsplumb AbstractConnector type
|
||||||
|
var result = {
|
||||||
|
sx: sx, sy: sy, tx: tx, ty: ty, lw: lw,
|
||||||
|
xSpan: Math.abs(tx - sx),
|
||||||
|
ySpan: Math.abs(ty - sy),
|
||||||
|
mx: (sx + tx) / 2,
|
||||||
|
my: (sy + ty) / 2,
|
||||||
|
so: so, to: to, x: x, y: y, w: w, h: h,
|
||||||
|
segment: segment,
|
||||||
|
startStubX: sx + (so[0] * sourceStubWithOffset),
|
||||||
|
startStubY: sy + (so[1] * sourceStubWithOffset),
|
||||||
|
endStubX: tx + (to[0] * targetStubWithOffset),
|
||||||
|
endStubY: ty + (to[1] * targetStubWithOffset),
|
||||||
|
isXGreaterThanStubTimes2: Math.abs(sx - tx) > (sourceStubWithOffset + targetStubWithOffset),
|
||||||
|
isYGreaterThanStubTimes2: Math.abs(sy - ty) > (sourceStubWithOffset + targetStubWithOffset),
|
||||||
|
opposite: oProduct === -1,
|
||||||
|
perpendicular: oProduct === 0,
|
||||||
|
orthogonal: oProduct === 1,
|
||||||
|
sourceAxis: so[0] === 0 ? "y" : "x",
|
||||||
|
points: [x, y, w, h, sx, sy, tx, ty ],
|
||||||
|
stubs:[sourceStubWithOffset, targetStubWithOffset],
|
||||||
|
anchorOrientation: "opposite", // always opposite since our endpoints are always opposite (source orientation is left (1) and target orientaiton is right (-1))
|
||||||
|
|
||||||
|
/** custom keys added */
|
||||||
|
sourceEndpoint: params.sourceEndpoint,
|
||||||
|
targetEndpoint: targetEndpoint,
|
||||||
|
sourcePos: params.sourcePos,
|
||||||
|
targetPos: targetEndpoint.anchor.getCurrentLocation(),
|
||||||
|
targetGap,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}).call(typeof window !== 'undefined' ? window : this);
|
|
@ -28,7 +28,7 @@ import {
|
||||||
IPushDataNodeExecuteAfter,
|
IPushDataNodeExecuteAfter,
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
XYPositon,
|
XYPosition,
|
||||||
IRestApiContext,
|
IRestApiContext,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
|
|
||||||
|
@ -584,6 +584,13 @@ export const store = new Vuex.Store({
|
||||||
}
|
}
|
||||||
state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data);
|
state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data);
|
||||||
},
|
},
|
||||||
|
clearNodeExecutionData (state, nodeName: string): void {
|
||||||
|
if (state.workflowExecutionData === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.delete(state.workflowExecutionData.data.resultData.runData, nodeName);
|
||||||
|
},
|
||||||
|
|
||||||
setWorkflowSettings (state, workflowSettings: IWorkflowSettings) {
|
setWorkflowSettings (state, workflowSettings: IWorkflowSettings) {
|
||||||
Vue.set(state.workflow, 'settings', workflowSettings);
|
Vue.set(state.workflow, 'settings', workflowSettings);
|
||||||
|
@ -754,7 +761,7 @@ export const store = new Vuex.Store({
|
||||||
return state.nodeIndex[index];
|
return state.nodeIndex[index];
|
||||||
},
|
},
|
||||||
|
|
||||||
getNodeViewOffsetPosition: (state): XYPositon => {
|
getNodeViewOffsetPosition: (state): XYPosition => {
|
||||||
return state.nodeViewOffsetPosition;
|
return state.nodeViewOffsetPosition;
|
||||||
},
|
},
|
||||||
isNodeViewMoveInProgress: (state): boolean => {
|
isNodeViewMoveInProgress: (state): boolean => {
|
||||||
|
@ -781,9 +788,7 @@ export const store = new Vuex.Store({
|
||||||
allConnections: (state): IConnections => {
|
allConnections: (state): IConnections => {
|
||||||
return state.workflow.connections;
|
return state.workflow.connections;
|
||||||
},
|
},
|
||||||
// connectionsByNodeName: (state) => (nodeName: string): {[key: string]: Connection[][]} | null => {
|
outgoingConnectionsByNodeName: (state) => (nodeName: string): INodeConnections => {
|
||||||
// connectionsByNodeName: (state) => (nodeName: string): { [key: string]: NodeConnections} | null => {
|
|
||||||
connectionsByNodeName: (state) => (nodeName: string): INodeConnections => {
|
|
||||||
if (state.workflow.connections.hasOwnProperty(nodeName)) {
|
if (state.workflow.connections.hasOwnProperty(nodeName)) {
|
||||||
return state.workflow.connections[nodeName];
|
return state.workflow.connections[nodeName];
|
||||||
}
|
}
|
||||||
|
@ -792,15 +797,14 @@ export const store = new Vuex.Store({
|
||||||
allNodes: (state): INodeUi[] => {
|
allNodes: (state): INodeUi[] => {
|
||||||
return state.workflow.nodes;
|
return state.workflow.nodes;
|
||||||
},
|
},
|
||||||
nodeByName: (state) => (nodeName: string): INodeUi | null => {
|
nodesByName: (state: IRootState): {[name: string]: INodeUi} => {
|
||||||
const foundNode = state.workflow.nodes.find(node => {
|
return state.workflow.nodes.reduce((accu: {[name: string]: INodeUi}, node) => {
|
||||||
return node.name === nodeName;
|
accu[node.name] = node;
|
||||||
});
|
return accu;
|
||||||
|
}, {});
|
||||||
if (foundNode === undefined) {
|
},
|
||||||
return null;
|
getNodeByName: (state, getters) => (nodeName: string): INodeUi | null => {
|
||||||
}
|
return getters.nodesByName[nodeName] || null;
|
||||||
return foundNode;
|
|
||||||
},
|
},
|
||||||
nodesIssuesExist: (state): boolean => {
|
nodesIssuesExist: (state): boolean => {
|
||||||
for (const node of state.workflow.nodes) {
|
for (const node of state.workflow.nodes) {
|
||||||
|
@ -825,10 +829,10 @@ export const store = new Vuex.Store({
|
||||||
return foundType;
|
return foundType;
|
||||||
},
|
},
|
||||||
activeNode: (state, getters): INodeUi | null => {
|
activeNode: (state, getters): INodeUi | null => {
|
||||||
return getters.nodeByName(state.activeNode);
|
return getters.getNodeByName(state.activeNode);
|
||||||
},
|
},
|
||||||
lastSelectedNode: (state, getters): INodeUi | null => {
|
lastSelectedNode: (state, getters): INodeUi | null => {
|
||||||
return getters.nodeByName(state.lastSelectedNode);
|
return getters.getNodeByName(state.lastSelectedNode);
|
||||||
},
|
},
|
||||||
lastSelectedNodeOutputIndex: (state, getters): number | null => {
|
lastSelectedNodeOutputIndex: (state, getters): number | null => {
|
||||||
return state.lastSelectedNodeOutputIndex;
|
return state.lastSelectedNodeOutputIndex;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
725
packages/editor-ui/src/views/canvasHelpers.ts
Normal file
725
packages/editor-ui/src/views/canvasHelpers.ts
Normal file
|
@ -0,0 +1,725 @@
|
||||||
|
import { getStyleTokenValue } from "@/components/helpers";
|
||||||
|
import { START_NODE_TYPE } from "@/constants";
|
||||||
|
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
||||||
|
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
||||||
|
import {
|
||||||
|
IConnection,
|
||||||
|
ITaskData,
|
||||||
|
INodeExecutionData,
|
||||||
|
NodeInputConnections,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const OVERLAY_DROP_NODE_ID = 'drop-add-node';
|
||||||
|
export const OVERLAY_MIDPOINT_ARROW_ID = 'midpoint-arrow';
|
||||||
|
export const OVERLAY_ENDPOINT_ARROW_ID = 'endpoint-arrow';
|
||||||
|
export const OVERLAY_RUN_ITEMS_ID = 'run-items-label';
|
||||||
|
export const OVERLAY_CONNECTION_ACTIONS_ID = 'connection-actions';
|
||||||
|
export const JSPLUMB_FLOWCHART_STUB = 26;
|
||||||
|
export const OVERLAY_INPUT_NAME_LABEL = 'input-name-label';
|
||||||
|
export const OVERLAY_INPUT_NAME_LABEL_POSITION = [-3, .5];
|
||||||
|
export const OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED = [-4.5, .5];
|
||||||
|
export const OVERLAY_OUTPUT_NAME_LABEL = 'output-name-label';
|
||||||
|
export const GRID_SIZE = 20;
|
||||||
|
|
||||||
|
const MIN_X_TO_SHOW_OUTPUT_LABEL = 90;
|
||||||
|
const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100;
|
||||||
|
|
||||||
|
export const NODE_SIZE = 100;
|
||||||
|
export const DEFAULT_START_POSITION_X = 240;
|
||||||
|
export const DEFAULT_START_POSITION_Y = 300;
|
||||||
|
export const HEADER_HEIGHT = 65;
|
||||||
|
export const SIDEBAR_WIDTH = 65;
|
||||||
|
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = 300;
|
||||||
|
export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
|
||||||
|
const LOOPBACK_MINIMUM = 140;
|
||||||
|
export const INPUT_UUID_KEY = '-input';
|
||||||
|
export const OUTPUT_UUID_KEY = '-output';
|
||||||
|
|
||||||
|
export const DEFAULT_START_NODE = {
|
||||||
|
name: 'Start',
|
||||||
|
type: START_NODE_TYPE,
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [
|
||||||
|
DEFAULT_START_POSITION_X,
|
||||||
|
DEFAULT_START_POSITION_Y,
|
||||||
|
] as XYPosition,
|
||||||
|
parameters: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
|
||||||
|
cornerRadius: 12,
|
||||||
|
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
||||||
|
targetGap: 4,
|
||||||
|
alwaysRespectStubs: false,
|
||||||
|
loopbackVerticalLength: NODE_SIZE, // height of vertical segment when looping
|
||||||
|
loopbackMinimum: LOOPBACK_MINIMUM, // minimum length before flowchart loops around
|
||||||
|
getEndpointOffset(endpoint: Endpoint) {
|
||||||
|
const indexOffset = 10; // stub offset between different endpoints of same node
|
||||||
|
const index = endpoint && endpoint.__meta ? endpoint.__meta.index : 0;
|
||||||
|
|
||||||
|
const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL);
|
||||||
|
const labelOffset = outputOverlay && outputOverlay.label && outputOverlay.label.length > 1 ? 10 : 0;
|
||||||
|
|
||||||
|
return index * indexOffset + labelOffset;
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
export const CONNECTOR_PAINT_STYLE_DEFAULT: PaintStyle = {
|
||||||
|
stroke: getStyleTokenValue('--color-foreground-dark'),
|
||||||
|
strokeWidth: 2,
|
||||||
|
outlineWidth: 12,
|
||||||
|
outlineStroke: 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONNECTOR_PAINT_STYLE_PULL: PaintStyle = {
|
||||||
|
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||||
|
stroke: getStyleTokenValue('--color-foreground-xdark'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONNECTOR_PAINT_STYLE_PRIMARY = {
|
||||||
|
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||||
|
stroke: getStyleTokenValue('--color-primary'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONNECTOR_PAINT_STYLE_SUCCESS = {
|
||||||
|
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||||
|
stroke: getStyleTokenValue('--color-success-light'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
||||||
|
[
|
||||||
|
'Arrow',
|
||||||
|
{
|
||||||
|
id: OVERLAY_ENDPOINT_ARROW_ID,
|
||||||
|
location: 1,
|
||||||
|
width: 12,
|
||||||
|
foldback: 1,
|
||||||
|
length: 10,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Arrow',
|
||||||
|
{
|
||||||
|
id: OVERLAY_MIDPOINT_ARROW_ID,
|
||||||
|
location: 0.5,
|
||||||
|
width: 12,
|
||||||
|
foldback: 1,
|
||||||
|
length: 10,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CONNECTOR_DROP_NODE_OVERLAY: OverlaySpec[] = [
|
||||||
|
[
|
||||||
|
'Label',
|
||||||
|
{
|
||||||
|
id: OVERLAY_DROP_NODE_ID,
|
||||||
|
label: 'Drop connection<br />to add node',
|
||||||
|
cssClass: 'drop-add-node-label',
|
||||||
|
location: 0.5,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export const ANCHOR_POSITIONS: {
|
||||||
|
[key: string]: {
|
||||||
|
[key: number]: string[] | number[][];
|
||||||
|
}
|
||||||
|
} = {
|
||||||
|
input: {
|
||||||
|
1: [
|
||||||
|
[0.01, 0.5, -1, 0],
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
[0.01, 0.3, -1, 0],
|
||||||
|
[0.01, 0.7, -1, 0],
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
[0.01, 0.25, -1, 0],
|
||||||
|
[0.01, 0.5, -1, 0],
|
||||||
|
[0.01, 0.75, -1, 0],
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
[0.01, 0.2, -1, 0],
|
||||||
|
[0.01, 0.4, -1, 0],
|
||||||
|
[0.01, 0.6, -1, 0],
|
||||||
|
[0.01, 0.8, -1, 0],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
1: [
|
||||||
|
[.99, 0.5, 1, 0],
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
[.99, 0.3, 1, 0],
|
||||||
|
[.99, 0.7, 1, 0],
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
[.99, 0.25, 1, 0],
|
||||||
|
[.99, 0.5, 1, 0],
|
||||||
|
[.99, 0.75, 1, 0],
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
[.99, 0.2, 1, 0],
|
||||||
|
[.99, 0.4, 1, 0],
|
||||||
|
[.99, 0.6, 1, 0],
|
||||||
|
[.99, 0.8, 1, 0],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getInputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({
|
||||||
|
width: 8,
|
||||||
|
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20,
|
||||||
|
fill: getStyleTokenValue(color),
|
||||||
|
stroke: getStyleTokenValue(color),
|
||||||
|
lineWidth: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getInputNameOverlay = (label: string) => ([
|
||||||
|
'Label',
|
||||||
|
{
|
||||||
|
id: OVERLAY_INPUT_NAME_LABEL,
|
||||||
|
location: OVERLAY_INPUT_NAME_LABEL_POSITION,
|
||||||
|
label,
|
||||||
|
cssClass: 'node-input-endpoint-label',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const getOutputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({
|
||||||
|
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||||
|
fill: getStyleTokenValue(color),
|
||||||
|
outlineStroke: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getOutputNameOverlay = (label: string) => ([
|
||||||
|
'Label',
|
||||||
|
{
|
||||||
|
id: OVERLAY_OUTPUT_NAME_LABEL,
|
||||||
|
location: [1.9, 0.5],
|
||||||
|
label,
|
||||||
|
cssClass: 'node-output-endpoint-label',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const addOverlays = (connection: Connection, overlays: OverlaySpec[]) => {
|
||||||
|
overlays.forEach((overlay: OverlaySpec) => {
|
||||||
|
connection.addOverlay(overlay);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||||
|
return nodes.reduce((leftmostTop, node) => {
|
||||||
|
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
||||||
|
return leftmostTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
|
||||||
|
return nodes.reduce((accu: IBounds, node: INodeUi) => {
|
||||||
|
if (node.position[0] < accu.minX) {
|
||||||
|
accu.minX = node.position[0];
|
||||||
|
}
|
||||||
|
if (node.position[1] < accu.minY) {
|
||||||
|
accu.minY = node.position[1];
|
||||||
|
}
|
||||||
|
if (node.position[0] > accu.maxX) {
|
||||||
|
accu.maxX = node.position[0];
|
||||||
|
}
|
||||||
|
if (node.position[1] > accu.maxY) {
|
||||||
|
accu.maxY = node.position[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return accu;
|
||||||
|
}, {
|
||||||
|
minX: nodes[0].position[0],
|
||||||
|
minY: nodes[0].position[1],
|
||||||
|
maxX: nodes[0].position[0],
|
||||||
|
maxY: nodes[0].position[1],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||||
|
scale /= 1.25;
|
||||||
|
xOffset /= 1.25;
|
||||||
|
yOffset /= 1.25;
|
||||||
|
xOffset += window.innerWidth / 10;
|
||||||
|
yOffset += window.innerHeight / 10;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scale,
|
||||||
|
offset: [xOffset, yOffset],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scaleBigger = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||||
|
scale *= 1.25;
|
||||||
|
xOffset -= window.innerWidth / 10;
|
||||||
|
yOffset -= window.innerHeight / 10;
|
||||||
|
xOffset *= 1.25;
|
||||||
|
yOffset *= 1.25;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scale,
|
||||||
|
offset: [xOffset, yOffset],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
||||||
|
if (config.scale > 1) { // zoomed in
|
||||||
|
while (config.scale > 1) {
|
||||||
|
config = scaleSmaller(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
while (config.scale < 1) {
|
||||||
|
config = scaleBigger(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.scale = 1;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||||
|
try {
|
||||||
|
return item.getOverlay(overlayId); // handle when _jsPlumb element is deleted
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||||
|
const overlay = getOverlay(item, overlayId);
|
||||||
|
if (overlay) {
|
||||||
|
overlay.setVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hideOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||||
|
const overlay = getOverlay(item, overlayId);
|
||||||
|
if (overlay) {
|
||||||
|
overlay.setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showOrHideMidpointArrow = (connection: Connection) => {
|
||||||
|
if (!connection || !connection.endpoints || connection.endpoints.length !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasItemsLabel = !!getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||||
|
|
||||||
|
const sourceEndpoint = connection.endpoints[0];
|
||||||
|
const targetEndpoint = connection.endpoints[1];
|
||||||
|
|
||||||
|
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||||
|
const targetPosition = targetEndpoint.anchor.lastReturnValue ? targetEndpoint.anchor.lastReturnValue[0] : sourcePosition + 1; // lastReturnValue is null when moving connections from node to another
|
||||||
|
|
||||||
|
const minimum = hasItemsLabel ? 150 : 0;
|
||||||
|
const isBackwards = sourcePosition >= targetPosition;
|
||||||
|
const isTooLong = Math.abs(sourcePosition - targetPosition) >= minimum;
|
||||||
|
|
||||||
|
const arrow = getOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||||
|
if (arrow) {
|
||||||
|
arrow.setVisible(isBackwards && isTooLong);
|
||||||
|
arrow.setLocation(hasItemsLabel ? .6: .5);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getConnectorLengths = (connection: Connection): [number, number] => {
|
||||||
|
if (!connection.connector) {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
const bounds = connection.connector.bounds;
|
||||||
|
const diffX = Math.abs(bounds.maxX - bounds.minX);
|
||||||
|
const diffY = Math.abs(bounds.maxY - bounds.minY);
|
||||||
|
|
||||||
|
return [diffX, diffY];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoopingBackwards = (connection: Connection) => {
|
||||||
|
const sourceEndpoint = connection.endpoints[0];
|
||||||
|
const targetEndpoint = connection.endpoints[1];
|
||||||
|
|
||||||
|
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||||
|
const targetPosition = targetEndpoint.anchor.lastReturnValue[0];
|
||||||
|
|
||||||
|
return targetPosition - sourcePosition < (-1 * LOOPBACK_MINIMUM);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showOrHideItemsLabel = (connection: Connection) => {
|
||||||
|
if (!connection || !connection.connector) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlay = getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||||
|
if (!overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionsOverlay = getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||||
|
if (actionsOverlay && actionsOverlay.visible) {
|
||||||
|
overlay.setVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [diffX, diffY] = getConnectorLengths(connection);
|
||||||
|
|
||||||
|
if (diffX < MIN_X_TO_SHOW_OUTPUT_LABEL && diffY < MIN_Y_TO_SHOW_OUTPUT_LABEL) {
|
||||||
|
overlay.setVisible(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlay.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerElement = overlay.canvas && overlay.canvas.querySelector('span');
|
||||||
|
if (innerElement) {
|
||||||
|
if (diffY === 0 || isLoopingBackwards(connection)) {
|
||||||
|
innerElement.classList.add('floating');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
innerElement.classList.remove('floating');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIcon = (name: string): string => {
|
||||||
|
if (name === 'trash') {
|
||||||
|
return `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="trash" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-trash fa-w-14 Icon__medium_ctPPJ"><path data-v-66d5c7e2="" fill="currentColor" d="M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z" class=""></path></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'plus') {
|
||||||
|
return `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-plus fa-w-14 Icon__medium_ctPPJ"><path data-v-301ed208="" fill="currentColor" d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z" class=""></path></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const canUsePosition = (position1: XYPosition, position2: XYPosition) => {
|
||||||
|
if (Math.abs(position1[0] - position2[0]) <= 100) {
|
||||||
|
if (Math.abs(position1[1] - position2[1]) <= 50) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNewNodePosition = (nodes: INodeUi[], newPosition: XYPosition, movePosition?: XYPosition): XYPosition => {
|
||||||
|
const targetPosition: XYPosition = [...newPosition];
|
||||||
|
|
||||||
|
targetPosition[0] = targetPosition[0] - (targetPosition[0] % GRID_SIZE);
|
||||||
|
targetPosition[1] = targetPosition[1] - (targetPosition[1] % GRID_SIZE);
|
||||||
|
|
||||||
|
if (!movePosition) {
|
||||||
|
movePosition = [40, 40];
|
||||||
|
}
|
||||||
|
|
||||||
|
let conflictFound = false;
|
||||||
|
let i, node;
|
||||||
|
do {
|
||||||
|
conflictFound = false;
|
||||||
|
for (i = 0; i < nodes.length; i++) {
|
||||||
|
node = nodes[i];
|
||||||
|
if (!canUsePosition(node.position, targetPosition)) {
|
||||||
|
conflictFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conflictFound === true) {
|
||||||
|
targetPosition[0] += movePosition[0];
|
||||||
|
targetPosition[1] += movePosition[1];
|
||||||
|
}
|
||||||
|
} while (conflictFound === true);
|
||||||
|
|
||||||
|
return targetPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => {
|
||||||
|
// @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];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRelativePosition = (x: number, y: number, scale: number, offset: XYPosition): XYPosition => {
|
||||||
|
return [
|
||||||
|
(x - offset[0]) / scale,
|
||||||
|
(y - offset[1]) / scale,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBackgroundStyles = (scale: number, offsetPosition: XYPosition) => {
|
||||||
|
const squareSize = GRID_SIZE * scale;
|
||||||
|
const dotSize = 1 * scale;
|
||||||
|
const dotPosition = (GRID_SIZE / 2) * scale;
|
||||||
|
const styles: object = {
|
||||||
|
'background-size': `${squareSize}px ${squareSize}px`,
|
||||||
|
'background-position': `left ${offsetPosition[0]}px top ${offsetPosition[1]}px`,
|
||||||
|
};
|
||||||
|
if (squareSize > 10.5) {
|
||||||
|
const dotColor = getStyleTokenValue('--color-canvas-dot');
|
||||||
|
return {
|
||||||
|
...styles,
|
||||||
|
'background-image': `radial-gradient(circle at ${dotPosition}px ${dotPosition}px, ${dotColor} ${dotSize}px, transparent 0)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hideConnectionActions = (connection: Connection | null) => {
|
||||||
|
if (connection && connection.connector) {
|
||||||
|
hideOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||||
|
showOrHideItemsLabel(connection);
|
||||||
|
showOrHideMidpointArrow(connection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showConectionActions = (connection: Connection | null) => {
|
||||||
|
if (connection && connection.connector) {
|
||||||
|
showOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||||
|
hideOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||||
|
if (!getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||||
|
hideOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputConnections) => {
|
||||||
|
const outputMap: {[sourceOutputIndex: string]: {[targetNodeName: string]: {[targetInputIndex: string]: {total: number, iterations: number}}}} = {};
|
||||||
|
|
||||||
|
data.forEach((run: ITaskData) => {
|
||||||
|
if (!run.data || !run.data.main) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
run.data.main.forEach((output: INodeExecutionData[] | null, i: number) => {
|
||||||
|
if (!nodeConnections[i]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeConnections[i]
|
||||||
|
.map((connection: IConnection) => {
|
||||||
|
const sourceOutputIndex = i;
|
||||||
|
const targetNodeName = connection.node;
|
||||||
|
const targetInputIndex = connection.index;
|
||||||
|
|
||||||
|
if (!outputMap[sourceOutputIndex]) {
|
||||||
|
outputMap[sourceOutputIndex] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputMap[sourceOutputIndex][targetNodeName]) {
|
||||||
|
outputMap[sourceOutputIndex][targetNodeName] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputMap[sourceOutputIndex][targetNodeName][targetInputIndex]) {
|
||||||
|
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex] = {
|
||||||
|
total: 0,
|
||||||
|
iterations: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].total += output ? output.length : 0;
|
||||||
|
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].iterations += output ? 1 : 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetConnection = (connection: Connection) => {
|
||||||
|
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||||
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||||
|
showOrHideMidpointArrow(connection);
|
||||||
|
if (connection.canvas) {
|
||||||
|
connection.canvas.classList.remove('success');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addConnectionOutputSuccess = (connection: Connection, output: {total: number, iterations: number}) => {
|
||||||
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_SUCCESS);
|
||||||
|
if (connection.canvas) {
|
||||||
|
connection.canvas.classList.add('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||||
|
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = `${output.total}`;
|
||||||
|
label = output.total > 1 ? `${label} items` : `${label} item`;
|
||||||
|
label = output.iterations > 1 ? `${label} total` : label;
|
||||||
|
|
||||||
|
connection.addOverlay([
|
||||||
|
'Label',
|
||||||
|
{
|
||||||
|
id: OVERLAY_RUN_ITEMS_ID,
|
||||||
|
label: `<span>${label}</span>`,
|
||||||
|
cssClass: 'connection-run-items-label',
|
||||||
|
location: .5,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
showOrHideItemsLabel(connection);
|
||||||
|
showOrHideMidpointArrow(connection);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getZoomToFit = (nodes: INodeUi[]): {offset: XYPosition, zoomLevel: number} => {
|
||||||
|
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
||||||
|
|
||||||
|
const PADDING = NODE_SIZE * 4;
|
||||||
|
|
||||||
|
const editorWidth = window.innerWidth;
|
||||||
|
const diffX = maxX - minX + SIDEBAR_WIDTH + PADDING;
|
||||||
|
const scaleX = editorWidth / diffX;
|
||||||
|
|
||||||
|
const editorHeight = window.innerHeight;
|
||||||
|
const diffY = maxY - minY + HEADER_HEIGHT + PADDING;
|
||||||
|
const scaleY = editorHeight / diffY;
|
||||||
|
|
||||||
|
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
||||||
|
let xOffset = (minX * -1) * zoomLevel + SIDEBAR_WIDTH; // find top right corner
|
||||||
|
xOffset += (editorWidth - SIDEBAR_WIDTH - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
|
let yOffset = (minY * -1) * zoomLevel + HEADER_HEIGHT; // find top right corner
|
||||||
|
yOffset += (editorHeight - HEADER_HEIGHT - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
|
return {
|
||||||
|
zoomLevel,
|
||||||
|
offset: [xOffset, yOffset],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUniqueNodeName = (nodes: INodeUi[], originalName: string, additinalUsedNames?: string[]) => {
|
||||||
|
// Check if node-name is unique else find one that is
|
||||||
|
additinalUsedNames = additinalUsedNames || [];
|
||||||
|
|
||||||
|
// Get all the names of the current nodes
|
||||||
|
const nodeNames = nodes.map((node: INodeUi) => {
|
||||||
|
return node.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check first if the current name is already unique
|
||||||
|
if (!nodeNames.includes(originalName) && !additinalUsedNames.includes(originalName)) {
|
||||||
|
return originalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameMatch = originalName.match(/(.*\D+)(\d*)/);
|
||||||
|
let ignore, baseName, nameIndex, uniqueName;
|
||||||
|
let index = 1;
|
||||||
|
|
||||||
|
if (nameMatch === null) {
|
||||||
|
// Name is only a number
|
||||||
|
index = parseInt(originalName, 10);
|
||||||
|
baseName = '';
|
||||||
|
uniqueName = baseName + index;
|
||||||
|
} else {
|
||||||
|
// Name is string or string/number combination
|
||||||
|
[ignore, baseName, nameIndex] = nameMatch;
|
||||||
|
if (nameIndex !== '') {
|
||||||
|
index = parseInt(nameIndex, 10);
|
||||||
|
}
|
||||||
|
uniqueName = baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
nodeNames.includes(uniqueName) ||
|
||||||
|
additinalUsedNames.includes(uniqueName)
|
||||||
|
) {
|
||||||
|
uniqueName = baseName + (index++);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
||||||
|
if (connection && connection.connector) {
|
||||||
|
if (targetEndpoint) {
|
||||||
|
connection.connector.setTargetEndpoint(targetEndpoint);
|
||||||
|
}
|
||||||
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
||||||
|
hideOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showPullConnectionState = (connection: Connection) => {
|
||||||
|
if (connection && connection.connector) {
|
||||||
|
connection.connector.resetTargetEndpoint();
|
||||||
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PULL);
|
||||||
|
showOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetConnectionAfterPull = (connection: Connection) => {
|
||||||
|
if (connection && connection.connector) {
|
||||||
|
connection.connector.resetTargetEndpoint();
|
||||||
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||||
|
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||||
|
if (inputNameOverlay) {
|
||||||
|
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveBackInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||||
|
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||||
|
if (inputNameOverlay) {
|
||||||
|
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addConnectionActionsOverlay = (connection: Connection, onDelete: Function, onAdd: Function) => {
|
||||||
|
if (getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID)) {
|
||||||
|
return; // avoid free floating actions when moving connection from one node to another
|
||||||
|
}
|
||||||
|
connection.addOverlay([
|
||||||
|
'Label',
|
||||||
|
{
|
||||||
|
id: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||||
|
label: `<div class="add">${getIcon('plus')}</div> <div class="delete">${getIcon('trash')}</div>`,
|
||||||
|
cssClass: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||||
|
visible: false,
|
||||||
|
events: {
|
||||||
|
mousedown: (overlay: Overlay, event: MouseEvent) => {
|
||||||
|
const element = event.target as HTMLElement;
|
||||||
|
if (element.classList.contains('delete') || (element.parentElement && element.parentElement.classList.contains('delete'))) {
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
else if (element.classList.contains('add') || (element.parentElement && element.parentElement.classList.contains('add'))) {
|
||||||
|
onAdd();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOutputEndpointUUID = (nodeIndex: string, outputIndex: number) => {
|
||||||
|
return `${nodeIndex}${OUTPUT_UUID_KEY}${outputIndex}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInputEndpointUUID = (nodeIndex: string, inputIndex: number) => {
|
||||||
|
return `${nodeIndex}${INPUT_UUID_KEY}${inputIndex}`;
|
||||||
|
};
|
|
@ -1,85 +0,0 @@
|
||||||
import { INodeUi, IZoomConfig } from "@/Interface";
|
|
||||||
|
|
||||||
interface ICorners {
|
|
||||||
minX: number;
|
|
||||||
minY: number;
|
|
||||||
maxX: number;
|
|
||||||
maxY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
|
||||||
return nodes.reduce((leftmostTop, node) => {
|
|
||||||
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
|
||||||
return leftmostTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWorkflowCorners = (nodes: INodeUi[]): ICorners => {
|
|
||||||
return nodes.reduce((accu: ICorners, node: INodeUi) => {
|
|
||||||
if (node.position[0] < accu.minX) {
|
|
||||||
accu.minX = node.position[0];
|
|
||||||
}
|
|
||||||
if (node.position[1] < accu.minY) {
|
|
||||||
accu.minY = node.position[1];
|
|
||||||
}
|
|
||||||
if (node.position[0] > accu.maxX) {
|
|
||||||
accu.maxX = node.position[0];
|
|
||||||
}
|
|
||||||
if (node.position[1] > accu.maxY) {
|
|
||||||
accu.maxY = node.position[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return accu;
|
|
||||||
}, {
|
|
||||||
minX: nodes[0].position[0],
|
|
||||||
minY: nodes[0].position[1],
|
|
||||||
maxX: nodes[0].position[0],
|
|
||||||
maxY: nodes[0].position[1],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
|
||||||
scale /= 1.25;
|
|
||||||
xOffset /= 1.25;
|
|
||||||
yOffset /= 1.25;
|
|
||||||
xOffset += window.innerWidth / 10;
|
|
||||||
yOffset += window.innerHeight / 10;
|
|
||||||
|
|
||||||
return {
|
|
||||||
scale,
|
|
||||||
offset: [xOffset, yOffset],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const scaleBigger = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
|
||||||
scale *= 1.25;
|
|
||||||
xOffset -= window.innerWidth / 10;
|
|
||||||
yOffset -= window.innerHeight / 10;
|
|
||||||
xOffset *= 1.25;
|
|
||||||
yOffset *= 1.25;
|
|
||||||
|
|
||||||
return {
|
|
||||||
scale,
|
|
||||||
offset: [xOffset, yOffset],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
|
||||||
if (config.scale > 1) { // zoomed in
|
|
||||||
while (config.scale > 1) {
|
|
||||||
config = scaleSmaller(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
while (config.scale < 1) {
|
|
||||||
config = scaleBigger(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.scale = 1;
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
|
@ -864,8 +864,10 @@ export class HttpRequest implements INodeType {
|
||||||
if (requestOptions.headers === undefined) {
|
if (requestOptions.headers === undefined) {
|
||||||
requestOptions.headers = {};
|
requestOptions.headers = {};
|
||||||
}
|
}
|
||||||
|
if (['POST', 'PUT', 'PATCH'].includes(requestMethod)) {
|
||||||
requestOptions.headers['Content-Type'] = 'application/json';
|
requestOptions.headers['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (options.bodyContentType === 'raw') {
|
} else if (options.bodyContentType === 'raw') {
|
||||||
requestOptions.json = false;
|
requestOptions.json = false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -918,7 +920,7 @@ export class HttpRequest implements INodeType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.sendMessageToUI(sendRequest);
|
this.sendMessageToUI(sendRequest);
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
// Now that the options are all set make the actual http request
|
// Now that the options are all set make the actual http request
|
||||||
if (oAuth1Api !== undefined) {
|
if (oAuth1Api !== undefined) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"resources": {
|
"resources": {
|
||||||
"credentialDocumentation": [
|
"credentialDocumentation": [
|
||||||
{
|
{
|
||||||
"url": "https://docs.n8n.io/credentials/OneSimpleAPI"
|
"url": "https://docs.n8n.io/credentials/oneSimpleApi/"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryDocumentation": [
|
"primaryDocumentation": [
|
||||||
|
|
Loading…
Reference in a new issue