Workflow canvas revamp (#2388)

* bring back overrides

* fix input output label positions

* simply update label positions

* refactor a bunch

* update min x to show items

* hide overlay on connection

* only delete target connection, add maximum to push nodes out

* rename const

* rename const

* set new insert position

* fix insert behavior

* update position handling

* show arrow along with label

* update connector

* set endpoint styles

* update pattern

* push nodes up / down in case of if node

* set position in switch

* only one action at a time

* add custom flow chart type

* select start node by default when opening new workflow

* add enter delay

* fix delete bug

* change connection type

* add offset for if/switch/merge

* fix gap

* fix drag issue

* implement new states

* update disabled state

* add selected state

* make selects faster

* update positioning

* truncate when selected

* remove offset for actions

* fix icon scaling

* refactor js plumb

* fix looping behavior at close distance

* lock version

* change background to dots

* update endpoints styling

* increase spacing

* udpate node z-index

* fix output label positions

* fix output label positions

* reset location

* add label offset

* update border radius

* fix height issue

* fix parallaxing issue

* fix zoomout issue

* add success z-index

* clean up js file

* add package lock

* fix z-index bug

* update dot grid

* update zoom level

* set values, increase grid size

* fix drop position

* prevent duplicate connections

* fix stub

* use localstorage overrides for colors

* add colors to system

* revert no longer needed changes

* revert no longer needed changes

* add canvas colors

* add canvas colors

* use variable for id

* force type

* refactor helpers

* add label constants

* refactor func

* refactor

* fix

* refactor

* clean up css

* refactor setzoom level

* refactor

* refactor

* refactor func

* remove scope

* remove localstorage caching

* clean up imports

* update zero case

* add delete connection

* update selected state

* add base type, remove straight line

* add stub offset back

* rename param

* add label offset

* update font size of items

* move up label

* fix error state while executing

* disrespect stubs

* check for errors

* refactor position

* clean up extra space

* make entire node connectable

* Revert "make entire node connectable"

e304f7c5b8

* always show border

* add border to zoom buttons

* update spacing

* update colors

* allow connecting to entire node

* fix pull conn active

* two line names

* apply select to all lines

* increase input margin

* override target pos

* reset conn after pull

* fix types

* update orientation

* fix up connectors snapping

* hide arrow on pull

* update overrides for connectors

* change text

* update pull colors

* set to 1 line when selected

* fix executions bug

* build

* refactor node component

* remove comment

* refactor more

* remove prop

* fix build issue

* fix input drag bug in executions

* reset offset

* update select background

* handle issue when endpoints are not set

* fix connection aborted issue

* add try catch to help show errors

* wrap bind with try/catch

* set default styles

* reset pos despite zoom

* add more checks

* clean up impl

* update icon

* handle unknown types

* hide items on init

* fix importing unknown types with credentials

* change opacity

* push up item label

* update color

* update label class and colors

* add to drop distance

* fix z-index to match node

* disable eslint

* fix lasso tool selection

* update background color

* update waiting state

* update tooltip positions

* update wait node border

* fix selection bug mostly

* if selected, move above other nodes

* add line through disabled nodes

* remove node color option

* move label above connection

* success color for line through

* update options index

* hide waiting icon when disabled

* fix gmail icon

* refactor icons

* clear execution data on disable/delete

* fix selected node

* fix executing behavior

* optional __meta

* set grid size

* remove default color

* remove node color

* add comments

* comments

* add comments

* remove empty space

* update comment

* refactor uuids

* fix type issue

* Revert "fix type issue"

9523b34f96

* Revert "fix type issue"

9523b34f96

* Revert "refactor uuids"

07f6848065

* fix build issues

* refactor

* update uuid

* child nodes

* skip nodes behind when pushing in loop

* shift output icon for switch node

* don't show output if waiting

* waiting on init

* build

* change to bezier

* revert connector change

* add bezier type

* fix snapping

* clean up impl

* refactor func

* make const

* rename type

* refactor to simplify

* Revert "refactor to simplify"

2db0ed504c

* enable flowchart mode

* clean up flowchart type

* refactor type

* merge types

* configure curviness

* set in localstorage

* fix straight line arrow bug

* show arrow when pulling

* refactor / simplify

* fix target gap in bezier

* refactor target gap

* add comments

* add comment

* fix dragging connections

* fix bug when moving connection

* update comment

* rename file

* update values

* update minor

* update straight line box

* clean up conn types

* clean up z-indexes

* move color filters to node icon

* update background color

* update to use grid size value

* fix endpoint offsets

* set yspan range lower

* remove overlays when moving conn

* prevent unwanted connections

* fix messed up connections

* remove console log

* clear execution issues on workflow run

* update corner radius

* fix drag/delete bug

* increase offset

* update disabled state

* address comments

* refactor

* refactor func

*  Add full license text to N8nCustomConnectorType.js

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Mutasem Aldmour 2021-11-19 10:17:13 +01:00 committed by GitHub
parent 0c6af9fd95
commit d8598b0126
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 2720 additions and 1062 deletions

View file

@ -16,7 +16,7 @@ import VariableTable from './VariableTable.vue';
<Canvas>
<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: {
VariableTable,
},

View file

@ -44,7 +44,7 @@ import ColorCircles from './ColorCircles.vue';
<Canvas>
<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: {
ColorCircles,
},
@ -109,7 +109,7 @@ import ColorCircles from './ColorCircles.vue';
<Canvas>
<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: {
ColorCircles,
},
@ -129,3 +129,16 @@ import ColorCircles from './ColorCircles.vue';
}}
</Story>
</Canvas>
## Canvas
<Canvas>
<Story name="canvas">
{{
template: `<color-circles :colors="['--color-canvas-background', '--color-canvas-dot']" />`,
components: {
ColorCircles,
},
}}
</Story>
</Canvas>

View file

@ -75,6 +75,15 @@
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-s: 77%;
--color-warning-l: 57%;
@ -187,6 +196,24 @@
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-s: 20%;
--color-foreground-base-l: 88.2%;
@ -259,6 +286,25 @@
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-base: 4px;
--border-radius-small: 2px;

View file

@ -22,32 +22,61 @@ import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
PaintStyle,
} from 'jsplumb';
declare module 'jsplumb' {
interface PaintStyle {
stroke?: string;
fill?: string;
strokeWidth?: number;
outlineStroke?: string;
outlineWidth?: number;
}
interface Anchor {
lastReturnValue: number[];
}
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, callback: Function): void; // tslint:disable-line:no-any
bind(event: string, callback: Function): void;
removeOverlay(name: string): void;
removeOverlays(): void;
setParameter(name: string, value: any): void; // tslint:disable-line:no-any
setPaintStyle(arg0: PaintStyle): void;
addOverlay(arg0: any[]): void; // tslint:disable-line:no-any
setConnector(arg0: any[]): void; // tslint:disable-line:no-any
getUuids(): [string, string];
}
interface Endpoint {
__meta?: {
nodeName: string,
index: number,
};
getOverlay(name: string): any; // tslint:disable-line:no-any
}
interface Overlay {
setVisible(visible: boolean): void;
setLocation(location: number): void;
canvas?: HTMLElement;
}
interface OnConnectionBindInfo {
@ -66,18 +95,14 @@ export interface IEndpointOptions {
dragProxy?: any; // tslint:disable-line:no-any
endpoint?: string;
endpointStyle?: object;
endpointHoverStyle?: object;
isSource?: boolean;
isTarget?: boolean;
maxConnections?: number;
overlays?: any; // tslint:disable-line:no-any
parameters?: any; // tslint:disable-line:no-any
uuid?: string;
}
export interface IConnectionsUi {
[key: string]: {
[key: string]: IEndpointOptions;
};
enabled?: boolean;
}
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 interface INodeUi extends INode {
position: XYPositon;
position: XYPosition;
color?: string;
notes?: string;
issues?: INodeIssues;
_jsPlumb?: {
endpoints?: {
[key: string]: IEndpointOptions[];
};
};
name: string;
}
export interface INodeTypesMaxCount {
@ -604,7 +625,7 @@ export interface IRootState {
lastSelectedNodeOutputIndex: number | null;
nodeIndex: Array<string | null>;
nodeTypes: INodeTypeDescription[];
nodeViewOffsetPosition: XYPositon;
nodeViewOffsetPosition: XYPosition;
nodeViewMoveInProgress: boolean;
selectedNodes: INodeUi[];
sessionId: string;
@ -670,5 +691,13 @@ export interface IRestApiContext {
export interface IZoomConfig {
scale: number;
offset: XYPositon;
offset: XYPosition;
}
export interface IBounds {
minX: number;
minY: number;
maxX: number;
maxY: number;
}

View file

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

View file

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

View file

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

View file

@ -122,13 +122,6 @@ export default mixins(
return this.nodeType.properties;
},
isColorDefaultValue (): boolean {
if (this.nodeType === null) {
return false;
}
return this.node.color === this.nodeType.defaults.color;
},
workflowRunning (): boolean {
return this.$store.getters.isActionActive('workflowRunning');
},
@ -170,14 +163,6 @@ export default mixins(
noDataExpression: true,
description: 'If active, the note above will display in the flow as a subtitle.',
},
{
displayName: 'Node Color',
name: 'color',
type: 'color',
default: '#ff0000',
noDataExpression: true,
description: 'The color of the node in the flow.',
},
{
displayName: 'Always Output Data',
name: 'alwaysOutputData',
@ -317,7 +302,7 @@ export default mixins(
// Update the values on the node
this.$store.commit('updateNodeProperties', updateInformation);
const node = this.$store.getters.nodeByName(updateInformation.name);
const node = this.$store.getters.getNodeByName(updateInformation.name);
// Update the issues
this.updateNodeCredentialIssues(node);
@ -337,7 +322,7 @@ export default mixins(
// Save the node name before we commit the change because
// we need the old name to rename the node properly
const nodeNameBefore = parameterData.node || this.node.name;
const node = this.$store.getters.nodeByName(nodeNameBefore);
const node = this.$store.getters.getNodeByName(nodeNameBefore);
if (parameterData.name === 'name') {
// Name of node changed so we have to set also the new node name as active
@ -353,7 +338,10 @@ export default mixins(
} else if (parameterData.name.startsWith('parameters.')) {
// A node parameter changed
const nodeType = this.$store.getters.nodeType(node.type);
const nodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
if (!nodeType) {
return;
}
// Get only the parameters which are different to the defaults
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
@ -491,10 +479,6 @@ export default mixins(
// Set default value
Vue.set(this.nodeValues, nodeSetting.name, nodeSetting.default);
}
if (nodeSetting.name === 'color') {
// For color also apply the default node color to the node settings
nodeSetting.default = this.nodeType.defaults.color;
}
}
Vue.set(this.nodeValues, 'parameters', JSON.parse(JSON.stringify(this.node.parameters)));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -116,3 +116,4 @@ export const COMPANY_SIZE_PERSONAL_USE = 'personalUser';
export const CODING_SKILL_KEY = 'codingSkill';
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';

View file

@ -28,8 +28,6 @@ $--badge-warning-color: #6b5900;
// Warning tooltip
$--warning-tooltip-color: #ff8080;
$--custom-node-view-background : #faf9fe;
// Table
$--custom-table-background-main: $--custom-header-background;
$--custom-table-background-stripe-color: #f6f6f6;

View file

@ -2,8 +2,9 @@
@import "~n8n-design-system/theme/dist/index.css";
body {
background-color: $--custom-node-view-background;
background-color: var(--color-canvas-background);
}
.clickable {

View 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);

View file

@ -28,7 +28,7 @@ import {
IPushDataNodeExecuteAfter,
IUpdateInformation,
IWorkflowDb,
XYPositon,
XYPosition,
IRestApiContext,
} from './Interface';
@ -576,6 +576,13 @@ export const store = new Vuex.Store({
}
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) {
Vue.set(state.workflow, 'settings', workflowSettings);
@ -739,7 +746,7 @@ export const store = new Vuex.Store({
return state.nodeIndex[index];
},
getNodeViewOffsetPosition: (state): XYPositon => {
getNodeViewOffsetPosition: (state): XYPosition => {
return state.nodeViewOffsetPosition;
},
isNodeViewMoveInProgress: (state): boolean => {
@ -766,9 +773,7 @@ export const store = new Vuex.Store({
allConnections: (state): IConnections => {
return state.workflow.connections;
},
// connectionsByNodeName: (state) => (nodeName: string): {[key: string]: Connection[][]} | null => {
// connectionsByNodeName: (state) => (nodeName: string): { [key: string]: NodeConnections} | null => {
connectionsByNodeName: (state) => (nodeName: string): INodeConnections => {
outgoingConnectionsByNodeName: (state) => (nodeName: string): INodeConnections => {
if (state.workflow.connections.hasOwnProperty(nodeName)) {
return state.workflow.connections[nodeName];
}
@ -777,15 +782,14 @@ export const store = new Vuex.Store({
allNodes: (state): INodeUi[] => {
return state.workflow.nodes;
},
nodeByName: (state) => (nodeName: string): INodeUi | null => {
const foundNode = state.workflow.nodes.find(node => {
return node.name === nodeName;
});
if (foundNode === undefined) {
return null;
}
return foundNode;
nodesByName: (state: IRootState): {[name: string]: INodeUi} => {
return state.workflow.nodes.reduce((accu: {[name: string]: INodeUi}, node) => {
accu[node.name] = node;
return accu;
}, {});
},
getNodeByName: (state, getters) => (nodeName: string): INodeUi | null => {
return getters.nodesByName[nodeName] || null;
},
nodesIssuesExist: (state): boolean => {
for (const node of state.workflow.nodes) {
@ -810,10 +814,10 @@ export const store = new Vuex.Store({
return foundType;
},
activeNode: (state, getters): INodeUi | null => {
return getters.nodeByName(state.activeNode);
return getters.getNodeByName(state.activeNode);
},
lastSelectedNode: (state, getters): INodeUi | null => {
return getters.nodeByName(state.lastSelectedNode);
return getters.getNodeByName(state.lastSelectedNode);
},
lastSelectedNodeOutputIndex: (state, getters): number | null => {
return state.lastSelectedNodeOutputIndex;

File diff suppressed because it is too large Load diff

View 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}`;
};

View file

@ -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;
};