mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
✨ 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:
parent
0c6af9fd95
commit
d8598b0126
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -191,6 +191,7 @@ export const workflowRun = mixins(
|
|||
},
|
||||
};
|
||||
this.$store.commit('setWorkflowExecutionData', executionData);
|
||||
this.updateNodesExecutionIssues();
|
||||
|
||||
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
|
||||
|
||||
|
|
|
@ -116,3 +116,4 @@ export const COMPANY_SIZE_PERSONAL_USE = 'personalUser';
|
|||
|
||||
export const CODING_SKILL_KEY = 'codingSkill';
|
||||
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
779
packages/editor-ui/src/plugins/N8nCustomConnectorType.js
Normal file
779
packages/editor-ui/src/plugins/N8nCustomConnectorType.js
Normal file
|
@ -0,0 +1,779 @@
|
|||
/**
|
||||
* Custom connector type
|
||||
* Based on jsplumb Flowchart and Bezier types
|
||||
*
|
||||
* Source GitHub repository:
|
||||
* https://github.com/jsplumb/jsplumb
|
||||
*
|
||||
* Source files:
|
||||
* https://github.com/jsplumb/jsplumb/blob/fb5fce52794fa52306825bdaa62bf3855cdfd7e0/src/connectors-flowchart.js
|
||||
* https://github.com/jsplumb/jsplumb/blob/fb5fce52794fa52306825bdaa62bf3855cdfd7e0/src/connectors-bezier.js
|
||||
*
|
||||
*
|
||||
* All 1.x.x and 2.x.x versions of jsPlumb Community edition, and so also the
|
||||
* content of this file, are dual-licensed under both MIT and GPLv2.
|
||||
*
|
||||
* MIT LICENSE
|
||||
*
|
||||
* Copyright (c) 2010 - 2014 jsPlumb, http://jsplumbtoolkit.com/
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* ===============================================================================
|
||||
* GNU GENERAL PUBLIC LICENSE
|
||||
* Version 2, June 1991
|
||||
*
|
||||
* Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
* Everyone is permitted to copy and distribute verbatim copies
|
||||
* of this license document, but changing it is not allowed.
|
||||
*
|
||||
* Preamble
|
||||
*
|
||||
* The licenses for most software are designed to take away your
|
||||
* freedom to share and change it. By contrast, the GNU General Public
|
||||
* License is intended to guarantee your freedom to share and change free
|
||||
* software--to make sure the software is free for all its users. This
|
||||
* General Public License applies to most of the Free Software
|
||||
* Foundation's software and to any other program whose authors commit to
|
||||
* using it. (Some other Free Software Foundation software is covered by
|
||||
* the GNU Lesser General Public License instead.) You can apply it to
|
||||
* your programs, too.
|
||||
*
|
||||
* When we speak of free software, we are referring to freedom, not
|
||||
* price. Our General Public Licenses are designed to make sure that you
|
||||
* have the freedom to distribute copies of free software (and charge for
|
||||
* this service if you wish), that you receive source code or can get it
|
||||
* if you want it, that you can change the software or use pieces of it
|
||||
* in new free programs; and that you know you can do these things.
|
||||
*
|
||||
* To protect your rights, we need to make restrictions that forbid
|
||||
* anyone to deny you these rights or to ask you to surrender the rights.
|
||||
* These restrictions translate to certain responsibilities for you if you
|
||||
* distribute copies of the software, or if you modify it.
|
||||
*
|
||||
* For example, if you distribute copies of such a program, whether
|
||||
* gratis or for a fee, you must give the recipients all the rights that
|
||||
* you have. You must make sure that they, too, receive or can get the
|
||||
* source code. And you must show them these terms so they know their
|
||||
* rights.
|
||||
*
|
||||
* We protect your rights with two steps: (1) copyright the software, and
|
||||
* (2) offer you this license which gives you legal permission to copy,
|
||||
* distribute and/or modify the software.
|
||||
*
|
||||
* Also, for each author's protection and ours, we want to make certain
|
||||
* that everyone understands that there is no warranty for this free
|
||||
* software. If the software is modified by someone else and passed on, we
|
||||
* want its recipients to know that what they have is not the original, so
|
||||
* that any problems introduced by others will not reflect on the original
|
||||
* authors' reputations.
|
||||
*
|
||||
* Finally, any free program is threatened constantly by software
|
||||
* patents. We wish to avoid the danger that redistributors of a free
|
||||
* program will individually obtain patent licenses, in effect making the
|
||||
* program proprietary. To prevent this, we have made it clear that any
|
||||
* patent must be licensed for everyone's free use or not licensed at all.
|
||||
*
|
||||
* The precise terms and conditions for copying, distribution and
|
||||
* modification follow.
|
||||
*
|
||||
* GNU GENERAL PUBLIC LICENSE
|
||||
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
*
|
||||
* 0. This License applies to any program or other work which contains
|
||||
* a notice placed by the copyright holder saying it may be distributed
|
||||
* under the terms of this General Public License. The "Program", below,
|
||||
* refers to any such program or work, and a "work based on the Program"
|
||||
* means either the Program or any derivative work under copyright law:
|
||||
* that is to say, a work containing the Program or a portion of it,
|
||||
* either verbatim or with modifications and/or translated into another
|
||||
* language. (Hereinafter, translation is included without limitation in
|
||||
* the term "modification".) Each licensee is addressed as "you".
|
||||
*
|
||||
* Activities other than copying, distribution and modification are not
|
||||
* covered by this License; they are outside its scope. The act of
|
||||
* running the Program is not restricted, and the output from the Program
|
||||
* is covered only if its contents constitute a work based on the
|
||||
* Program (independent of having been made by running the Program).
|
||||
* Whether that is true depends on what the Program does.
|
||||
*
|
||||
* 1. You may copy and distribute verbatim copies of the Program's
|
||||
* source code as you receive it, in any medium, provided that you
|
||||
* conspicuously and appropriately publish on each copy an appropriate
|
||||
* copyright notice and disclaimer of warranty; keep intact all the
|
||||
* notices that refer to this License and to the absence of any warranty;
|
||||
* and give any other recipients of the Program a copy of this License
|
||||
* along with the Program.
|
||||
*
|
||||
* You may charge a fee for the physical act of transferring a copy, and
|
||||
* you may at your option offer warranty protection in exchange for a fee.
|
||||
*
|
||||
* 2. You may modify your copy or copies of the Program or any portion
|
||||
* of it, thus forming a work based on the Program, and copy and
|
||||
* distribute such modifications or work under the terms of Section 1
|
||||
* above, provided that you also meet all of these conditions:
|
||||
*
|
||||
* a) You must cause the modified files to carry prominent notices
|
||||
* stating that you changed the files and the date of any change.
|
||||
*
|
||||
* b) You must cause any work that you distribute or publish, that in
|
||||
* whole or in part contains or is derived from the Program or any
|
||||
* part thereof, to be licensed as a whole at no charge to all third
|
||||
* parties under the terms of this License.
|
||||
*
|
||||
* c) If the modified program normally reads commands interactively
|
||||
* when run, you must cause it, when started running for such
|
||||
* interactive use in the most ordinary way, to print or display an
|
||||
* announcement including an appropriate copyright notice and a
|
||||
* notice that there is no warranty (or else, saying that you provide
|
||||
* a warranty) and that users may redistribute the program under
|
||||
* these conditions, and telling the user how to view a copy of this
|
||||
* License. (Exception: if the Program itself is interactive but
|
||||
* does not normally print such an announcement, your work based on
|
||||
* the Program is not required to print an announcement.)
|
||||
*
|
||||
* These requirements apply to the modified work as a whole. If
|
||||
* identifiable sections of that work are not derived from the Program,
|
||||
* and can be reasonably considered independent and separate works in
|
||||
* themselves, then this License, and its terms, do not apply to those
|
||||
* sections when you distribute them as separate works. But when you
|
||||
* distribute the same sections as part of a whole which is a work based
|
||||
* on the Program, the distribution of the whole must be on the terms of
|
||||
* this License, whose permissions for other licensees extend to the
|
||||
* entire whole, and thus to each and every part regardless of who wrote it.
|
||||
*
|
||||
* Thus, it is not the intent of this section to claim rights or contest
|
||||
* your rights to work written entirely by you; rather, the intent is to
|
||||
* exercise the right to control the distribution of derivative or
|
||||
* collective works based on the Program.
|
||||
*
|
||||
* In addition, mere aggregation of another work not based on the Program
|
||||
* with the Program (or with a work based on the Program) on a volume of
|
||||
* a storage or distribution medium does not bring the other work under
|
||||
* the scope of this License.
|
||||
*
|
||||
* 3. You may copy and distribute the Program (or a work based on it,
|
||||
* under Section 2) in object code or executable form under the terms of
|
||||
* Sections 1 and 2 above provided that you also do one of the following:
|
||||
*
|
||||
* a) Accompany it with the complete corresponding machine-readable
|
||||
* source code, which must be distributed under the terms of Sections
|
||||
* 1 and 2 above on a medium customarily used for software interchange; or,
|
||||
*
|
||||
* b) Accompany it with a written offer, valid for at least three
|
||||
* years, to give any third party, for a charge no more than your
|
||||
* cost of physically performing source distribution, a complete
|
||||
* machine-readable copy of the corresponding source code, to be
|
||||
* distributed under the terms of Sections 1 and 2 above on a medium
|
||||
* customarily used for software interchange; or,
|
||||
*
|
||||
* c) Accompany it with the information you received as to the offer
|
||||
* to distribute corresponding source code. (This alternative is
|
||||
* allowed only for noncommercial distribution and only if you
|
||||
* received the program in object code or executable form with such
|
||||
* an offer, in accord with Subsection b above.)
|
||||
*
|
||||
* The source code for a work means the preferred form of the work for
|
||||
* making modifications to it. For an executable work, complete source
|
||||
* code means all the source code for all modules it contains, plus any
|
||||
* associated interface definition files, plus the scripts used to
|
||||
* control compilation and installation of the executable. However, as a
|
||||
* special exception, the source code distributed need not include
|
||||
* anything that is normally distributed (in either source or binary
|
||||
* form) with the major components (compiler, kernel, and so on) of the
|
||||
* operating system on which the executable runs, unless that component
|
||||
* itself accompanies the executable.
|
||||
*
|
||||
* If distribution of executable or object code is made by offering
|
||||
* access to copy from a designated place, then offering equivalent
|
||||
* access to copy the source code from the same place counts as
|
||||
* distribution of the source code, even though third parties are not
|
||||
* compelled to copy the source along with the object code.
|
||||
*
|
||||
* 4. You may not copy, modify, sublicense, or distribute the Program
|
||||
* except as expressly provided under this License. Any attempt
|
||||
* otherwise to copy, modify, sublicense or distribute the Program is
|
||||
* void, and will automatically terminate your rights under this License.
|
||||
* However, parties who have received copies, or rights, from you under
|
||||
* this License will not have their licenses terminated so long as such
|
||||
* parties remain in full compliance.
|
||||
*
|
||||
* 5. You are not required to accept this License, since you have not
|
||||
* signed it. However, nothing else grants you permission to modify or
|
||||
* distribute the Program or its derivative works. These actions are
|
||||
* prohibited by law if you do not accept this License. Therefore, by
|
||||
* modifying or distributing the Program (or any work based on the
|
||||
* Program), you indicate your acceptance of this License to do so, and
|
||||
* all its terms and conditions for copying, distributing or modifying
|
||||
* the Program or works based on it.
|
||||
*
|
||||
* 6. Each time you redistribute the Program (or any work based on the
|
||||
* Program), the recipient automatically receives a license from the
|
||||
* original licensor to copy, distribute or modify the Program subject to
|
||||
* these terms and conditions. You may not impose any further
|
||||
* restrictions on the recipients' exercise of the rights granted herein.
|
||||
* You are not responsible for enforcing compliance by third parties to
|
||||
* this License.
|
||||
*
|
||||
* 7. If, as a consequence of a court judgment or allegation of patent
|
||||
* infringement or for any other reason (not limited to patent issues),
|
||||
* conditions are imposed on you (whether by court order, agreement or
|
||||
* otherwise) that contradict the conditions of this License, they do not
|
||||
* excuse you from the conditions of this License. If you cannot
|
||||
* distribute so as to satisfy simultaneously your obligations under this
|
||||
* License and any other pertinent obligations, then as a consequence you
|
||||
* may not distribute the Program at all. For example, if a patent
|
||||
* license would not permit royalty-free redistribution of the Program by
|
||||
* all those who receive copies directly or indirectly through you, then
|
||||
* the only way you could satisfy both it and this License would be to
|
||||
* refrain entirely from distribution of the Program.
|
||||
*
|
||||
* If any portion of this section is held invalid or unenforceable under
|
||||
* any particular circumstance, the balance of the section is intended to
|
||||
* apply and the section as a whole is intended to apply in other
|
||||
* circumstances.
|
||||
*
|
||||
* It is not the purpose of this section to induce you to infringe any
|
||||
* patents or other property right claims or to contest validity of any
|
||||
* such claims; this section has the sole purpose of protecting the
|
||||
* integrity of the free software distribution system, which is
|
||||
* implemented by public license practices. Many people have made
|
||||
* generous contributions to the wide range of software distributed
|
||||
* through that system in reliance on consistent application of that
|
||||
* system; it is up to the author/donor to decide if he or she is willing
|
||||
* to distribute software through any other system and a licensee cannot
|
||||
* impose that choice.
|
||||
*
|
||||
* This section is intended to make thoroughly clear what is believed to
|
||||
* be a consequence of the rest of this License.
|
||||
*
|
||||
* 8. If the distribution and/or use of the Program is restricted in
|
||||
* certain countries either by patents or by copyrighted interfaces, the
|
||||
* original copyright holder who places the Program under this License
|
||||
* may add an explicit geographical distribution limitation excluding
|
||||
* those countries, so that distribution is permitted only in or among
|
||||
* countries not thus excluded. In such case, this License incorporates
|
||||
* the limitation as if written in the body of this License.
|
||||
*
|
||||
* 9. The Free Software Foundation may publish revised and/or new versions
|
||||
* of the General Public License from time to time. Such new versions will
|
||||
* be similar in spirit to the present version, but may differ in detail to
|
||||
* address new problems or concerns.
|
||||
*
|
||||
* Each version is given a distinguishing version number. If the Program
|
||||
* specifies a version number of this License which applies to it and "any
|
||||
* later version", you have the option of following the terms and conditions
|
||||
* either of that version or of any later version published by the Free
|
||||
* Software Foundation. If the Program does not specify a version number of
|
||||
* this License, you may choose any version ever published by the Free Software
|
||||
* Foundation.
|
||||
*
|
||||
* 10. If you wish to incorporate parts of the Program into other free
|
||||
* programs whose distribution conditions are different, write to the author
|
||||
* to ask for permission. For software which is copyrighted by the Free
|
||||
* Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
* make exceptions for this. Our decision will be guided by the two goals
|
||||
* of preserving the free status of all derivatives of our free software and
|
||||
* of promoting the sharing and reuse of software generally.
|
||||
*
|
||||
* NO WARRANTY
|
||||
*
|
||||
* 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
* FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
* OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
* PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
* OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
* TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
* PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
* REPAIR OR CORRECTION.
|
||||
*
|
||||
* 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
* WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
* REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
* INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
* OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
* TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
* YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
* PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGES.
|
||||
*
|
||||
*/
|
||||
(function () {
|
||||
|
||||
"use strict";
|
||||
var root = this, _jp = root.jsPlumb, _ju = root.jsPlumbUtil, _jg = root.Biltong;
|
||||
var STRAIGHT = "Straight";
|
||||
var ARC = "Arc";
|
||||
|
||||
/**
|
||||
* Custom connector type
|
||||
*
|
||||
* @param stub {number} length of stub segments in flowchart
|
||||
* @param getEndpointOffset {Function} callback to offset stub length based on endpoint in flowchart
|
||||
* @param midpoint {number} float percent of halfway point of segments in flowchart
|
||||
* @param loopbackVerticalLength {number} height of vertical segment when looping in flowchart
|
||||
* @param cornerRadius {number} radius of flowchart connectors
|
||||
* @param loopbackMinimum {number} minimum threshold before looping behavior takes effect in flowchart
|
||||
* @param targetGap {number} gap between connector and target endpoint in both flowchart and bezier
|
||||
*/
|
||||
const N8nCustom = function (params) {
|
||||
params = params || {};
|
||||
this.type = "N8nCustom";
|
||||
|
||||
params.stub = params.stub == null ? 30 : params.stub;
|
||||
|
||||
var _super = _jp.Connectors.AbstractConnector.apply(this, arguments),
|
||||
minorAnchor = 0, // seems to be angle at which connector leaves endpoint
|
||||
majorAnchor = 0, // translates to curviness of bezier curve
|
||||
segments,
|
||||
midpoint = params.midpoint == null ? 0.5 : params.midpoint,
|
||||
alwaysRespectStubs = params.alwaysRespectStubs === true,
|
||||
loopbackVerticalLength = params.loopbackVerticalLength || 0,
|
||||
lastx = null, lasty = null,
|
||||
cornerRadius = params.cornerRadius != null ? params.cornerRadius : 0,
|
||||
loopbackMinimum = params.loopbackMinimum || 100,
|
||||
curvinessCoeffient = 0.4,
|
||||
zBezierOffset = 40,
|
||||
targetGap = params.targetGap || 0,
|
||||
stub = params.stub || 0;
|
||||
|
||||
/**
|
||||
* Set target endpoint
|
||||
* (to override default behavior tracking mouse when dragging mouse)
|
||||
* @param {Endpoint} endpoint
|
||||
*/
|
||||
this.setTargetEndpoint = function (endpoint) {
|
||||
this.overrideTargetEndpoint = endpoint;
|
||||
};
|
||||
|
||||
/**
|
||||
* reset target endpoint overriding default behavior
|
||||
*/
|
||||
this.resetTargetEndpoint = function () {
|
||||
this.overrideTargetEndpoint = null;
|
||||
};
|
||||
|
||||
this._compute = function (originalPaintInfo, connParams) {
|
||||
const paintInfo = _getPaintInfo(connParams, { targetGap, stub, overrideTargetEndpoint: this.overrideTargetEndpoint, getEndpointOffset: params.getEndpointOffset });
|
||||
Object.keys(paintInfo).forEach((key) => {
|
||||
// override so that bounding box is calculated correctly wheen target override is set
|
||||
originalPaintInfo[key] = paintInfo[key];
|
||||
});
|
||||
|
||||
if (paintInfo.tx < 0) {
|
||||
this._computeFlowchart(paintInfo);
|
||||
}
|
||||
else {
|
||||
this._computeBezier(paintInfo);
|
||||
}
|
||||
};
|
||||
|
||||
this._computeBezier = function (paintInfo) {
|
||||
var sp = paintInfo.sourcePos,
|
||||
tp = paintInfo.targetPos,
|
||||
_w = Math.abs(sp[0] - tp[0]) - paintInfo.targetGap,
|
||||
_h = Math.abs(sp[1] - tp[1]);
|
||||
|
||||
var _CP, _CP2,
|
||||
_sx = sp[0] < tp[0] ? _w : 0,
|
||||
_sy = sp[1] < tp[1] ? _h : 0,
|
||||
_tx = sp[0] < tp[0] ? 0 : _w,
|
||||
_ty = sp[1] < tp[1] ? 0 : _h;
|
||||
|
||||
if (paintInfo.ySpan <= 20 || (paintInfo.ySpan <= 100 && paintInfo.xSpan <= 100)) {
|
||||
majorAnchor = 0.1;
|
||||
}
|
||||
else {
|
||||
majorAnchor = paintInfo.xSpan * curvinessCoeffient + zBezierOffset;
|
||||
}
|
||||
|
||||
_CP = _findControlPoint([_sx, _sy], sp, tp, paintInfo.sourceEndpoint, paintInfo.targetEndpoint, paintInfo.so, paintInfo.to, majorAnchor, minorAnchor);
|
||||
_CP2 = _findControlPoint([_tx, _ty], tp, sp, paintInfo.targetEndpoint, paintInfo.sourceEndpoint, paintInfo.to, paintInfo.so, majorAnchor, minorAnchor);
|
||||
|
||||
_super.addSegment(this, "Bezier", {
|
||||
x1: _sx, y1: _sy, x2: _tx, y2: _ty,
|
||||
cp1x: _CP[0], cp1y: _CP[1], cp2x: _CP2[0], cp2y: _CP2[1],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* helper method to add a segment.
|
||||
*/
|
||||
const addFlowchartSegment = function (segments, x, y, paintInfo) {
|
||||
if (lastx === x && lasty === y) {
|
||||
return;
|
||||
}
|
||||
var lx = lastx == null ? paintInfo.sx : lastx,
|
||||
ly = lasty == null ? paintInfo.sy : lasty,
|
||||
o = lx === x ? "v" : "h";
|
||||
|
||||
lastx = x;
|
||||
lasty = y;
|
||||
segments.push([ lx, ly, x, y, o ]);
|
||||
};
|
||||
|
||||
this._computeFlowchart = function (paintInfo) {
|
||||
|
||||
segments = [];
|
||||
lastx = null;
|
||||
lasty = null;
|
||||
|
||||
// calculate Stubs.
|
||||
var stubs = calcualteStubSegment(paintInfo, {alwaysRespectStubs});
|
||||
|
||||
// add the start stub segment. use stubs for loopback as it will look better, with the loop spaced
|
||||
// away from the element.
|
||||
addFlowchartSegment(segments, stubs[0], stubs[1], paintInfo);
|
||||
|
||||
// compute the rest of the line
|
||||
var p = calculateLineSegment(paintInfo, stubs, {midpoint, loopbackMinimum, loopbackVerticalLength});
|
||||
if (p) {
|
||||
for (var i = 0; i < p.length; i++) {
|
||||
addFlowchartSegment(segments, p[i][0], p[i][1], paintInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// line to end stub
|
||||
addFlowchartSegment(segments, stubs[2], stubs[3], paintInfo);
|
||||
|
||||
// end stub to end (common)
|
||||
addFlowchartSegment(segments, paintInfo.tx, paintInfo.ty, paintInfo);
|
||||
|
||||
// write out the segments.
|
||||
writeFlowchartSegments(_super, this, segments, paintInfo, cornerRadius);
|
||||
};
|
||||
};
|
||||
|
||||
_jp.Connectors.N8nCustom = N8nCustom;
|
||||
_ju.extend(_jp.Connectors.N8nCustom, _jp.Connectors.AbstractConnector);
|
||||
|
||||
|
||||
function _findControlPoint(point, sourceAnchorPosition, targetAnchorPosition, sourceEndpoint, targetEndpoint, soo, too, majorAnchor, minorAnchor) {
|
||||
// determine if the two anchors are perpendicular to each other in their orientation. we swap the control
|
||||
// points around if so (code could be tightened up)
|
||||
var perpendicular = soo[0] !== too[0] || soo[1] === too[1],
|
||||
p = [];
|
||||
|
||||
if (!perpendicular) {
|
||||
if (soo[0] === 0) {
|
||||
p.push(sourceAnchorPosition[0] < targetAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[0] - (majorAnchor * soo[0]));
|
||||
}
|
||||
|
||||
if (soo[1] === 0) {
|
||||
p.push(sourceAnchorPosition[1] < targetAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[1] + (majorAnchor * too[1]));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (too[0] === 0) {
|
||||
p.push(targetAnchorPosition[0] < sourceAnchorPosition[0] ? point[0] + minorAnchor : point[0] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[0] + (majorAnchor * too[0]));
|
||||
}
|
||||
|
||||
if (too[1] === 0) {
|
||||
p.push(targetAnchorPosition[1] < sourceAnchorPosition[1] ? point[1] + minorAnchor : point[1] - minorAnchor);
|
||||
}
|
||||
else {
|
||||
p.push(point[1] + (majorAnchor * soo[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
|
||||
function sgn(n) {
|
||||
return n < 0 ? -1 : n === 0 ? 0 : 1;
|
||||
};
|
||||
|
||||
function getFlowchartSegmentDirections(segment) {
|
||||
return [
|
||||
sgn( segment[2] - segment[0] ),
|
||||
sgn( segment[3] - segment[1] ),
|
||||
];
|
||||
};
|
||||
|
||||
function getSegmentLength(s) {
|
||||
return Math.sqrt(Math.pow(s[0] - s[2], 2) + Math.pow(s[1] - s[3], 2));
|
||||
};
|
||||
|
||||
function _cloneArray(a) {
|
||||
var _a = [];
|
||||
_a.push.apply(_a, a);
|
||||
return _a;
|
||||
};
|
||||
|
||||
function writeFlowchartSegments(_super, conn, segments, paintInfo, cornerRadius) {
|
||||
var current = null, next, currentDirection, nextDirection;
|
||||
for (var i = 0; i < segments.length - 1; i++) {
|
||||
|
||||
current = current || _cloneArray(segments[i]);
|
||||
next = _cloneArray(segments[i + 1]);
|
||||
|
||||
currentDirection = getFlowchartSegmentDirections(current);
|
||||
nextDirection = getFlowchartSegmentDirections(next);
|
||||
|
||||
if (cornerRadius > 0 && current[4] !== next[4]) {
|
||||
|
||||
var minSegLength = Math.min(getSegmentLength(current), getSegmentLength(next));
|
||||
var radiusToUse = Math.min(cornerRadius, minSegLength / 2);
|
||||
|
||||
current[2] -= currentDirection[0] * radiusToUse;
|
||||
current[3] -= currentDirection[1] * radiusToUse;
|
||||
next[0] += nextDirection[0] * radiusToUse;
|
||||
next[1] += nextDirection[1] * radiusToUse;
|
||||
|
||||
var ac = (currentDirection[1] === nextDirection[0] && nextDirection[0] === 1) ||
|
||||
((currentDirection[1] === nextDirection[0] && nextDirection[0] === 0) && currentDirection[0] !== nextDirection[1]) ||
|
||||
(currentDirection[1] === nextDirection[0] && nextDirection[0] === -1),
|
||||
sgny = next[1] > current[3] ? 1 : -1,
|
||||
sgnx = next[0] > current[2] ? 1 : -1,
|
||||
sgnEqual = sgny === sgnx,
|
||||
cx = (sgnEqual && ac || (!sgnEqual && !ac)) ? next[0] : current[2],
|
||||
cy = (sgnEqual && ac || (!sgnEqual && !ac)) ? current[3] : next[1];
|
||||
|
||||
_super.addSegment(conn, STRAIGHT, {
|
||||
x1: current[0], y1: current[1], x2: current[2], y2: current[3],
|
||||
});
|
||||
|
||||
_super.addSegment(conn, ARC, {
|
||||
r: radiusToUse,
|
||||
x1: current[2],
|
||||
y1: current[3],
|
||||
x2: next[0],
|
||||
y2: next[1],
|
||||
cx: cx,
|
||||
cy: cy,
|
||||
ac: ac,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// dx + dy are used to adjust for line width.
|
||||
var dx = (current[2] === current[0]) ? 0 : (current[2] > current[0]) ? (paintInfo.lw / 2) : -(paintInfo.lw / 2),
|
||||
dy = (current[3] === current[1]) ? 0 : (current[3] > current[1]) ? (paintInfo.lw / 2) : -(paintInfo.lw / 2);
|
||||
|
||||
_super.addSegment(conn, STRAIGHT, {
|
||||
x1: current[0] - dx, y1: current[1] - dy, x2: current[2] + dx, y2: current[3] + dy,
|
||||
});
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
if (next != null) {
|
||||
// last segment
|
||||
_super.addSegment(conn, STRAIGHT, {
|
||||
x1: next[0], y1: next[1], x2: next[2], y2: next[3],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const lineCalculators = {
|
||||
opposite: function (paintInfo, {axis, startStub, endStub, idx, midx, midy}) {
|
||||
var pi = paintInfo,
|
||||
comparator = pi["is" + axis.toUpperCase() + "GreaterThanStubTimes2"];
|
||||
|
||||
if (!comparator || (pi.so[idx] === 1 && startStub > endStub) || (pi.so[idx] === -1 && startStub < endStub)) {
|
||||
return {
|
||||
"x": [
|
||||
[startStub, midy],
|
||||
[endStub, midy],
|
||||
],
|
||||
"y": [
|
||||
[midx, startStub],
|
||||
[midx, endStub],
|
||||
],
|
||||
}[axis];
|
||||
}
|
||||
else if ((pi.so[idx] === 1 && startStub < endStub) || (pi.so[idx] === -1 && startStub > endStub)) {
|
||||
return {
|
||||
"x": [
|
||||
[midx, pi.sy],
|
||||
[midx, pi.ty],
|
||||
],
|
||||
"y": [
|
||||
[pi.sx, midy],
|
||||
[pi.tx, midy],
|
||||
],
|
||||
}[axis];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const stubCalculators = {
|
||||
opposite: function (paintInfo, {axis, alwaysRespectStubs}) {
|
||||
var pi = paintInfo,
|
||||
idx = axis === "x" ? 0 : 1,
|
||||
areInProximity = {
|
||||
"x": function () {
|
||||
return ( (pi.so[idx] === 1 && (
|
||||
( (pi.startStubX > pi.endStubX) && (pi.tx > pi.startStubX) ) ||
|
||||
( (pi.sx > pi.endStubX) && (pi.tx > pi.sx))))) ||
|
||||
|
||||
( (pi.so[idx] === -1 && (
|
||||
( (pi.startStubX < pi.endStubX) && (pi.tx < pi.startStubX) ) ||
|
||||
( (pi.sx < pi.endStubX) && (pi.tx < pi.sx)))));
|
||||
},
|
||||
"y": function () {
|
||||
return ( (pi.so[idx] === 1 && (
|
||||
( (pi.startStubY > pi.endStubY) && (pi.ty > pi.startStubY) ) ||
|
||||
( (pi.sy > pi.endStubY) && (pi.ty > pi.sy))))) ||
|
||||
|
||||
( (pi.so[idx] === -1 && (
|
||||
( (pi.startStubY < pi.endStubY) && (pi.ty < pi.startStubY) ) ||
|
||||
( (pi.sy < pi.endStubY) && (pi.ty < pi.sy)))));
|
||||
},
|
||||
};
|
||||
|
||||
if (!alwaysRespectStubs && areInProximity[axis]()) {
|
||||
return {
|
||||
"x": [(paintInfo.sx + paintInfo.tx) / 2, paintInfo.startStubY, (paintInfo.sx + paintInfo.tx) / 2, paintInfo.endStubY],
|
||||
"y": [paintInfo.startStubX, (paintInfo.sy + paintInfo.ty) / 2, paintInfo.endStubX, (paintInfo.sy + paintInfo.ty) / 2],
|
||||
}[axis];
|
||||
}
|
||||
else {
|
||||
return [paintInfo.startStubX, paintInfo.startStubY, paintInfo.endStubX, paintInfo.endStubY];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function calcualteStubSegment(paintInfo, {alwaysRespectStubs}) {
|
||||
return stubCalculators['opposite'](paintInfo, {axis: paintInfo.sourceAxis, alwaysRespectStubs});
|
||||
}
|
||||
|
||||
function calculateLineSegment(paintInfo, stubs, { midpoint, loopbackVerticalLength, loopbackMinimum }) {
|
||||
const axis = paintInfo.sourceAxis,
|
||||
idx = paintInfo.sourceAxis === "x" ? 0 : 1,
|
||||
oidx = paintInfo.sourceAxis === "x" ? 1 : 0,
|
||||
startStub = stubs[idx],
|
||||
otherStartStub = stubs[oidx],
|
||||
endStub = stubs[idx + 2],
|
||||
otherEndStub = stubs[oidx + 2];
|
||||
|
||||
const diffX = paintInfo.endStubX - paintInfo.startStubX;
|
||||
const diffY = paintInfo.endStubY - paintInfo.startStubY;
|
||||
const direction = diffY >= 0 ? 1 : -1; // vertical direction of loop, above or below source
|
||||
|
||||
var midx = paintInfo.startStubX + ((paintInfo.endStubX - paintInfo.startStubX) * midpoint),
|
||||
midy;
|
||||
|
||||
if (diffX < (-1 * loopbackMinimum)) {
|
||||
// loop backward behavior
|
||||
midy = paintInfo.startStubY - (diffX < 0 ? direction * loopbackVerticalLength : 0);
|
||||
} else {
|
||||
// original flowchart behavior
|
||||
midy = paintInfo.startStubY + ((paintInfo.endStubY - paintInfo.startStubY) * midpoint);
|
||||
}
|
||||
|
||||
return lineCalculators['opposite'](paintInfo, {axis, startStub, otherStartStub, endStub, otherEndStub, idx, oidx, midx, midy});
|
||||
}
|
||||
|
||||
function _getPaintInfo(params, { targetGap, stub, overrideTargetEndpoint, getEndpointOffset }) {
|
||||
let { targetPos, targetEndpoint } = params;
|
||||
|
||||
if (
|
||||
overrideTargetEndpoint
|
||||
) {
|
||||
targetPos = overrideTargetEndpoint.anchor.getCurrentLocation();
|
||||
targetEndpoint = overrideTargetEndpoint;
|
||||
}
|
||||
|
||||
const sourceGap = 0;
|
||||
|
||||
stub = stub || 0;
|
||||
const sourceStub = _ju.isArray(stub) ? stub[0] : stub;
|
||||
const targetStub = _ju.isArray(stub) ? stub[1] : stub;
|
||||
|
||||
var segment = _jg.quadrant(params.sourcePos, targetPos),
|
||||
swapX = targetPos[0] < params.sourcePos[0],
|
||||
swapY = targetPos[1] < params.sourcePos[1],
|
||||
lw = params.strokeWidth || 1,
|
||||
so = params.sourceEndpoint.anchor.getOrientation(params.sourceEndpoint), // source orientation
|
||||
to = targetEndpoint.anchor.getOrientation(targetEndpoint), // target orientation
|
||||
x = swapX ? targetPos[0] : params.sourcePos[0],
|
||||
y = swapY ? targetPos[1] : params.sourcePos[1],
|
||||
w = Math.abs(targetPos[0] - params.sourcePos[0]),
|
||||
h = Math.abs(targetPos[1] - params.sourcePos[1]);
|
||||
|
||||
// if either anchor does not have an orientation set, we derive one from their relative
|
||||
// positions. we fix the axis to be the one in which the two elements are further apart, and
|
||||
// point each anchor at the other element. this is also used when dragging a new connection.
|
||||
if (so[0] === 0 && so[1] === 0 || to[0] === 0 && to[1] === 0) {
|
||||
var index = w > h ? 0 : 1, oIndex = [1, 0][index];
|
||||
so = [];
|
||||
to = [];
|
||||
so[index] = params.sourcePos[index] > targetPos[index] ? -1 : 1;
|
||||
to[index] = params.sourcePos[index] > targetPos[index] ? 1 : -1;
|
||||
so[oIndex] = 0;
|
||||
to[oIndex] = 0;
|
||||
}
|
||||
|
||||
const sx = swapX ? w + (sourceGap * so[0]) : sourceGap * so[0],
|
||||
sy = swapY ? h + (sourceGap * so[1]) : sourceGap * so[1],
|
||||
tx = swapX ? targetGap * to[0] : w + (targetGap * to[0]),
|
||||
ty = swapY ? targetGap * to[1] : h + (targetGap * to[1]),
|
||||
oProduct = ((so[0] * to[0]) + (so[1] * to[1]));
|
||||
|
||||
const sourceStubWithOffset = sourceStub + (getEndpointOffset && params.sourceEndpoint ? getEndpointOffset(params.sourceEndpoint) : 0);
|
||||
const targetStubWithOffset = targetStub + (getEndpointOffset && targetEndpoint ? getEndpointOffset(targetEndpoint) : 0);
|
||||
|
||||
// same as paintinfo generated by jsplumb AbstractConnector type
|
||||
var result = {
|
||||
sx: sx, sy: sy, tx: tx, ty: ty, lw: lw,
|
||||
xSpan: Math.abs(tx - sx),
|
||||
ySpan: Math.abs(ty - sy),
|
||||
mx: (sx + tx) / 2,
|
||||
my: (sy + ty) / 2,
|
||||
so: so, to: to, x: x, y: y, w: w, h: h,
|
||||
segment: segment,
|
||||
startStubX: sx + (so[0] * sourceStubWithOffset),
|
||||
startStubY: sy + (so[1] * sourceStubWithOffset),
|
||||
endStubX: tx + (to[0] * targetStubWithOffset),
|
||||
endStubY: ty + (to[1] * targetStubWithOffset),
|
||||
isXGreaterThanStubTimes2: Math.abs(sx - tx) > (sourceStubWithOffset + targetStubWithOffset),
|
||||
isYGreaterThanStubTimes2: Math.abs(sy - ty) > (sourceStubWithOffset + targetStubWithOffset),
|
||||
opposite: oProduct === -1,
|
||||
perpendicular: oProduct === 0,
|
||||
orthogonal: oProduct === 1,
|
||||
sourceAxis: so[0] === 0 ? "y" : "x",
|
||||
points: [x, y, w, h, sx, sy, tx, ty ],
|
||||
stubs:[sourceStubWithOffset, targetStubWithOffset],
|
||||
anchorOrientation: "opposite", // always opposite since our endpoints are always opposite (source orientation is left (1) and target orientaiton is right (-1))
|
||||
|
||||
/** custom keys added */
|
||||
sourceEndpoint: params.sourceEndpoint,
|
||||
targetEndpoint: targetEndpoint,
|
||||
sourcePos: params.sourcePos,
|
||||
targetPos: targetEndpoint.anchor.getCurrentLocation(),
|
||||
targetGap,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
}).call(typeof window !== 'undefined' ? window : this);
|
|
@ -28,7 +28,7 @@ import {
|
|||
IPushDataNodeExecuteAfter,
|
||||
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
725
packages/editor-ui/src/views/canvasHelpers.ts
Normal file
725
packages/editor-ui/src/views/canvasHelpers.ts
Normal file
|
@ -0,0 +1,725 @@
|
|||
import { getStyleTokenValue } from "@/components/helpers";
|
||||
import { START_NODE_TYPE } from "@/constants";
|
||||
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
||||
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
||||
import {
|
||||
IConnection,
|
||||
ITaskData,
|
||||
INodeExecutionData,
|
||||
NodeInputConnections,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const OVERLAY_DROP_NODE_ID = 'drop-add-node';
|
||||
export const OVERLAY_MIDPOINT_ARROW_ID = 'midpoint-arrow';
|
||||
export const OVERLAY_ENDPOINT_ARROW_ID = 'endpoint-arrow';
|
||||
export const OVERLAY_RUN_ITEMS_ID = 'run-items-label';
|
||||
export const OVERLAY_CONNECTION_ACTIONS_ID = 'connection-actions';
|
||||
export const JSPLUMB_FLOWCHART_STUB = 26;
|
||||
export const OVERLAY_INPUT_NAME_LABEL = 'input-name-label';
|
||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION = [-3, .5];
|
||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED = [-4.5, .5];
|
||||
export const OVERLAY_OUTPUT_NAME_LABEL = 'output-name-label';
|
||||
export const GRID_SIZE = 20;
|
||||
|
||||
const MIN_X_TO_SHOW_OUTPUT_LABEL = 90;
|
||||
const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100;
|
||||
|
||||
export const NODE_SIZE = 100;
|
||||
export const DEFAULT_START_POSITION_X = 240;
|
||||
export const DEFAULT_START_POSITION_Y = 300;
|
||||
export const HEADER_HEIGHT = 65;
|
||||
export const SIDEBAR_WIDTH = 65;
|
||||
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = 300;
|
||||
export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
|
||||
const LOOPBACK_MINIMUM = 140;
|
||||
export const INPUT_UUID_KEY = '-input';
|
||||
export const OUTPUT_UUID_KEY = '-output';
|
||||
|
||||
export const DEFAULT_START_NODE = {
|
||||
name: 'Start',
|
||||
type: START_NODE_TYPE,
|
||||
typeVersion: 1,
|
||||
position: [
|
||||
DEFAULT_START_POSITION_X,
|
||||
DEFAULT_START_POSITION_Y,
|
||||
] as XYPosition,
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
|
||||
cornerRadius: 12,
|
||||
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
||||
targetGap: 4,
|
||||
alwaysRespectStubs: false,
|
||||
loopbackVerticalLength: NODE_SIZE, // height of vertical segment when looping
|
||||
loopbackMinimum: LOOPBACK_MINIMUM, // minimum length before flowchart loops around
|
||||
getEndpointOffset(endpoint: Endpoint) {
|
||||
const indexOffset = 10; // stub offset between different endpoints of same node
|
||||
const index = endpoint && endpoint.__meta ? endpoint.__meta.index : 0;
|
||||
|
||||
const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL);
|
||||
const labelOffset = outputOverlay && outputOverlay.label && outputOverlay.label.length > 1 ? 10 : 0;
|
||||
|
||||
return index * indexOffset + labelOffset;
|
||||
},
|
||||
}];
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_DEFAULT: PaintStyle = {
|
||||
stroke: getStyleTokenValue('--color-foreground-dark'),
|
||||
strokeWidth: 2,
|
||||
outlineWidth: 12,
|
||||
outlineStroke: 'transparent',
|
||||
};
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_PULL: PaintStyle = {
|
||||
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
stroke: getStyleTokenValue('--color-foreground-xdark'),
|
||||
};
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_PRIMARY = {
|
||||
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
stroke: getStyleTokenValue('--color-primary'),
|
||||
};
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_SUCCESS = {
|
||||
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
stroke: getStyleTokenValue('--color-success-light'),
|
||||
};
|
||||
|
||||
export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
||||
[
|
||||
'Arrow',
|
||||
{
|
||||
id: OVERLAY_ENDPOINT_ARROW_ID,
|
||||
location: 1,
|
||||
width: 12,
|
||||
foldback: 1,
|
||||
length: 10,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'Arrow',
|
||||
{
|
||||
id: OVERLAY_MIDPOINT_ARROW_ID,
|
||||
location: 0.5,
|
||||
width: 12,
|
||||
foldback: 1,
|
||||
length: 10,
|
||||
visible: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
export const CONNECTOR_DROP_NODE_OVERLAY: OverlaySpec[] = [
|
||||
[
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_DROP_NODE_ID,
|
||||
label: 'Drop connection<br />to add node',
|
||||
cssClass: 'drop-add-node-label',
|
||||
location: 0.5,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
export const ANCHOR_POSITIONS: {
|
||||
[key: string]: {
|
||||
[key: number]: string[] | number[][];
|
||||
}
|
||||
} = {
|
||||
input: {
|
||||
1: [
|
||||
[0.01, 0.5, -1, 0],
|
||||
],
|
||||
2: [
|
||||
[0.01, 0.3, -1, 0],
|
||||
[0.01, 0.7, -1, 0],
|
||||
],
|
||||
3: [
|
||||
[0.01, 0.25, -1, 0],
|
||||
[0.01, 0.5, -1, 0],
|
||||
[0.01, 0.75, -1, 0],
|
||||
],
|
||||
4: [
|
||||
[0.01, 0.2, -1, 0],
|
||||
[0.01, 0.4, -1, 0],
|
||||
[0.01, 0.6, -1, 0],
|
||||
[0.01, 0.8, -1, 0],
|
||||
],
|
||||
},
|
||||
output: {
|
||||
1: [
|
||||
[.99, 0.5, 1, 0],
|
||||
],
|
||||
2: [
|
||||
[.99, 0.3, 1, 0],
|
||||
[.99, 0.7, 1, 0],
|
||||
],
|
||||
3: [
|
||||
[.99, 0.25, 1, 0],
|
||||
[.99, 0.5, 1, 0],
|
||||
[.99, 0.75, 1, 0],
|
||||
],
|
||||
4: [
|
||||
[.99, 0.2, 1, 0],
|
||||
[.99, 0.4, 1, 0],
|
||||
[.99, 0.6, 1, 0],
|
||||
[.99, 0.8, 1, 0],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const getInputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({
|
||||
width: 8,
|
||||
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20,
|
||||
fill: getStyleTokenValue(color),
|
||||
stroke: getStyleTokenValue(color),
|
||||
lineWidth: 0,
|
||||
});
|
||||
|
||||
export const getInputNameOverlay = (label: string) => ([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_INPUT_NAME_LABEL,
|
||||
location: OVERLAY_INPUT_NAME_LABEL_POSITION,
|
||||
label,
|
||||
cssClass: 'node-input-endpoint-label',
|
||||
visible: true,
|
||||
},
|
||||
]);
|
||||
|
||||
export const getOutputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({
|
||||
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||
fill: getStyleTokenValue(color),
|
||||
outlineStroke: 'none',
|
||||
});
|
||||
|
||||
export const getOutputNameOverlay = (label: string) => ([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_OUTPUT_NAME_LABEL,
|
||||
location: [1.9, 0.5],
|
||||
label,
|
||||
cssClass: 'node-output-endpoint-label',
|
||||
visible: true,
|
||||
},
|
||||
]);
|
||||
|
||||
export const addOverlays = (connection: Connection, overlays: OverlaySpec[]) => {
|
||||
overlays.forEach((overlay: OverlaySpec) => {
|
||||
connection.addOverlay(overlay);
|
||||
});
|
||||
};
|
||||
|
||||
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||
return nodes.reduce((leftmostTop, node) => {
|
||||
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
||||
return leftmostTop;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
|
||||
return nodes.reduce((accu: IBounds, node: INodeUi) => {
|
||||
if (node.position[0] < accu.minX) {
|
||||
accu.minX = node.position[0];
|
||||
}
|
||||
if (node.position[1] < accu.minY) {
|
||||
accu.minY = node.position[1];
|
||||
}
|
||||
if (node.position[0] > accu.maxX) {
|
||||
accu.maxX = node.position[0];
|
||||
}
|
||||
if (node.position[1] > accu.maxY) {
|
||||
accu.maxY = node.position[1];
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, {
|
||||
minX: nodes[0].position[0],
|
||||
minY: nodes[0].position[1],
|
||||
maxX: nodes[0].position[0],
|
||||
maxY: nodes[0].position[1],
|
||||
});
|
||||
};
|
||||
|
||||
export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale /= 1.25;
|
||||
xOffset /= 1.25;
|
||||
yOffset /= 1.25;
|
||||
xOffset += window.innerWidth / 10;
|
||||
yOffset += window.innerHeight / 10;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleBigger = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale *= 1.25;
|
||||
xOffset -= window.innerWidth / 10;
|
||||
yOffset -= window.innerHeight / 10;
|
||||
xOffset *= 1.25;
|
||||
yOffset *= 1.25;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
||||
if (config.scale > 1) { // zoomed in
|
||||
while (config.scale > 1) {
|
||||
config = scaleSmaller(config);
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (config.scale < 1) {
|
||||
config = scaleBigger(config);
|
||||
}
|
||||
}
|
||||
|
||||
config.scale = 1;
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const getOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||
try {
|
||||
return item.getOverlay(overlayId); // handle when _jsPlumb element is deleted
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const showOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||
const overlay = getOverlay(item, overlayId);
|
||||
if (overlay) {
|
||||
overlay.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
export const hideOverlay = (item: Connection | Endpoint, overlayId: string) => {
|
||||
const overlay = getOverlay(item, overlayId);
|
||||
if (overlay) {
|
||||
overlay.setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
export const showOrHideMidpointArrow = (connection: Connection) => {
|
||||
if (!connection || !connection.endpoints || connection.endpoints.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasItemsLabel = !!getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
|
||||
const sourceEndpoint = connection.endpoints[0];
|
||||
const targetEndpoint = connection.endpoints[1];
|
||||
|
||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||
const targetPosition = targetEndpoint.anchor.lastReturnValue ? targetEndpoint.anchor.lastReturnValue[0] : sourcePosition + 1; // lastReturnValue is null when moving connections from node to another
|
||||
|
||||
const minimum = hasItemsLabel ? 150 : 0;
|
||||
const isBackwards = sourcePosition >= targetPosition;
|
||||
const isTooLong = Math.abs(sourcePosition - targetPosition) >= minimum;
|
||||
|
||||
const arrow = getOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||
if (arrow) {
|
||||
arrow.setVisible(isBackwards && isTooLong);
|
||||
arrow.setLocation(hasItemsLabel ? .6: .5);
|
||||
}
|
||||
};
|
||||
|
||||
export const getConnectorLengths = (connection: Connection): [number, number] => {
|
||||
if (!connection.connector) {
|
||||
return [0, 0];
|
||||
}
|
||||
const bounds = connection.connector.bounds;
|
||||
const diffX = Math.abs(bounds.maxX - bounds.minX);
|
||||
const diffY = Math.abs(bounds.maxY - bounds.minY);
|
||||
|
||||
return [diffX, diffY];
|
||||
};
|
||||
|
||||
const isLoopingBackwards = (connection: Connection) => {
|
||||
const sourceEndpoint = connection.endpoints[0];
|
||||
const targetEndpoint = connection.endpoints[1];
|
||||
|
||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||
const targetPosition = targetEndpoint.anchor.lastReturnValue[0];
|
||||
|
||||
return targetPosition - sourcePosition < (-1 * LOOPBACK_MINIMUM);
|
||||
};
|
||||
|
||||
export const showOrHideItemsLabel = (connection: Connection) => {
|
||||
if (!connection || !connection.connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionsOverlay = getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
if (actionsOverlay && actionsOverlay.visible) {
|
||||
overlay.setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const [diffX, diffY] = getConnectorLengths(connection);
|
||||
|
||||
if (diffX < MIN_X_TO_SHOW_OUTPUT_LABEL && diffY < MIN_Y_TO_SHOW_OUTPUT_LABEL) {
|
||||
overlay.setVisible(false);
|
||||
}
|
||||
else {
|
||||
overlay.setVisible(true);
|
||||
}
|
||||
|
||||
const innerElement = overlay.canvas && overlay.canvas.querySelector('span');
|
||||
if (innerElement) {
|
||||
if (diffY === 0 || isLoopingBackwards(connection)) {
|
||||
innerElement.classList.add('floating');
|
||||
}
|
||||
else {
|
||||
innerElement.classList.remove('floating');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getIcon = (name: string): string => {
|
||||
if (name === 'trash') {
|
||||
return `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="trash" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-trash fa-w-14 Icon__medium_ctPPJ"><path data-v-66d5c7e2="" fill="currentColor" d="M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z" class=""></path></svg>`;
|
||||
}
|
||||
|
||||
if (name === 'plus') {
|
||||
return `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-plus fa-w-14 Icon__medium_ctPPJ"><path data-v-301ed208="" fill="currentColor" d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z" class=""></path></svg>`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
|
||||
const canUsePosition = (position1: XYPosition, position2: XYPosition) => {
|
||||
if (Math.abs(position1[0] - position2[0]) <= 100) {
|
||||
if (Math.abs(position1[1] - position2[1]) <= 50) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getNewNodePosition = (nodes: INodeUi[], newPosition: XYPosition, movePosition?: XYPosition): XYPosition => {
|
||||
const targetPosition: XYPosition = [...newPosition];
|
||||
|
||||
targetPosition[0] = targetPosition[0] - (targetPosition[0] % GRID_SIZE);
|
||||
targetPosition[1] = targetPosition[1] - (targetPosition[1] % GRID_SIZE);
|
||||
|
||||
if (!movePosition) {
|
||||
movePosition = [40, 40];
|
||||
}
|
||||
|
||||
let conflictFound = false;
|
||||
let i, node;
|
||||
do {
|
||||
conflictFound = false;
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
node = nodes[i];
|
||||
if (!canUsePosition(node.position, targetPosition)) {
|
||||
conflictFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (conflictFound === true) {
|
||||
targetPosition[0] += movePosition[0];
|
||||
targetPosition[1] += movePosition[1];
|
||||
}
|
||||
} while (conflictFound === true);
|
||||
|
||||
return targetPosition;
|
||||
};
|
||||
|
||||
export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => {
|
||||
// @ts-ignore
|
||||
const x = e.pageX !== undefined ? e.pageX : (e.touches && e.touches[0] && e.touches[0].pageX ? e.touches[0].pageX : 0);
|
||||
// @ts-ignore
|
||||
const y = e.pageY !== undefined ? e.pageY : (e.touches && e.touches[0] && e.touches[0].pageY ? e.touches[0].pageY : 0);
|
||||
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
export const getRelativePosition = (x: number, y: number, scale: number, offset: XYPosition): XYPosition => {
|
||||
return [
|
||||
(x - offset[0]) / scale,
|
||||
(y - offset[1]) / scale,
|
||||
];
|
||||
};
|
||||
|
||||
export const getBackgroundStyles = (scale: number, offsetPosition: XYPosition) => {
|
||||
const squareSize = GRID_SIZE * scale;
|
||||
const dotSize = 1 * scale;
|
||||
const dotPosition = (GRID_SIZE / 2) * scale;
|
||||
const styles: object = {
|
||||
'background-size': `${squareSize}px ${squareSize}px`,
|
||||
'background-position': `left ${offsetPosition[0]}px top ${offsetPosition[1]}px`,
|
||||
};
|
||||
if (squareSize > 10.5) {
|
||||
const dotColor = getStyleTokenValue('--color-canvas-dot');
|
||||
return {
|
||||
...styles,
|
||||
'background-image': `radial-gradient(circle at ${dotPosition}px ${dotPosition}px, ${dotColor} ${dotSize}px, transparent 0)`,
|
||||
};
|
||||
}
|
||||
return styles;
|
||||
};
|
||||
|
||||
export const hideConnectionActions = (connection: Connection | null) => {
|
||||
if (connection && connection.connector) {
|
||||
hideOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
showOrHideItemsLabel(connection);
|
||||
showOrHideMidpointArrow(connection);
|
||||
}
|
||||
};
|
||||
|
||||
export const showConectionActions = (connection: Connection | null) => {
|
||||
if (connection && connection.connector) {
|
||||
showOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
hideOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
if (!getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||
hideOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputConnections) => {
|
||||
const outputMap: {[sourceOutputIndex: string]: {[targetNodeName: string]: {[targetInputIndex: string]: {total: number, iterations: number}}}} = {};
|
||||
|
||||
data.forEach((run: ITaskData) => {
|
||||
if (!run.data || !run.data.main) {
|
||||
return;
|
||||
}
|
||||
|
||||
run.data.main.forEach((output: INodeExecutionData[] | null, i: number) => {
|
||||
if (!nodeConnections[i]) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodeConnections[i]
|
||||
.map((connection: IConnection) => {
|
||||
const sourceOutputIndex = i;
|
||||
const targetNodeName = connection.node;
|
||||
const targetInputIndex = connection.index;
|
||||
|
||||
if (!outputMap[sourceOutputIndex]) {
|
||||
outputMap[sourceOutputIndex] = {};
|
||||
}
|
||||
|
||||
if (!outputMap[sourceOutputIndex][targetNodeName]) {
|
||||
outputMap[sourceOutputIndex][targetNodeName] = {};
|
||||
}
|
||||
|
||||
if (!outputMap[sourceOutputIndex][targetNodeName][targetInputIndex]) {
|
||||
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex] = {
|
||||
total: 0,
|
||||
iterations: 0,
|
||||
};
|
||||
}
|
||||
|
||||
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].total += output ? output.length : 0;
|
||||
outputMap[sourceOutputIndex][targetNodeName][targetInputIndex].iterations += output ? 1 : 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return outputMap;
|
||||
};
|
||||
|
||||
export const resetConnection = (connection: Connection) => {
|
||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||
showOrHideMidpointArrow(connection);
|
||||
if (connection.canvas) {
|
||||
connection.canvas.classList.remove('success');
|
||||
}
|
||||
};
|
||||
|
||||
export const addConnectionOutputSuccess = (connection: Connection, output: {total: number, iterations: number}) => {
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_SUCCESS);
|
||||
if (connection.canvas) {
|
||||
connection.canvas.classList.add('success');
|
||||
}
|
||||
|
||||
if (getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||
}
|
||||
|
||||
let label = `${output.total}`;
|
||||
label = output.total > 1 ? `${label} items` : `${label} item`;
|
||||
label = output.iterations > 1 ? `${label} total` : label;
|
||||
|
||||
connection.addOverlay([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_RUN_ITEMS_ID,
|
||||
label: `<span>${label}</span>`,
|
||||
cssClass: 'connection-run-items-label',
|
||||
location: .5,
|
||||
},
|
||||
]);
|
||||
|
||||
showOrHideItemsLabel(connection);
|
||||
showOrHideMidpointArrow(connection);
|
||||
};
|
||||
|
||||
|
||||
export const getZoomToFit = (nodes: INodeUi[]): {offset: XYPosition, zoomLevel: number} => {
|
||||
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
||||
|
||||
const PADDING = NODE_SIZE * 4;
|
||||
|
||||
const editorWidth = window.innerWidth;
|
||||
const diffX = maxX - minX + SIDEBAR_WIDTH + PADDING;
|
||||
const scaleX = editorWidth / diffX;
|
||||
|
||||
const editorHeight = window.innerHeight;
|
||||
const diffY = maxY - minY + HEADER_HEIGHT + PADDING;
|
||||
const scaleY = editorHeight / diffY;
|
||||
|
||||
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
||||
let xOffset = (minX * -1) * zoomLevel + SIDEBAR_WIDTH; // find top right corner
|
||||
xOffset += (editorWidth - SIDEBAR_WIDTH - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
|
||||
|
||||
let yOffset = (minY * -1) * zoomLevel + HEADER_HEIGHT; // find top right corner
|
||||
yOffset += (editorHeight - HEADER_HEIGHT - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
|
||||
|
||||
return {
|
||||
zoomLevel,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const getUniqueNodeName = (nodes: INodeUi[], originalName: string, additinalUsedNames?: string[]) => {
|
||||
// Check if node-name is unique else find one that is
|
||||
additinalUsedNames = additinalUsedNames || [];
|
||||
|
||||
// Get all the names of the current nodes
|
||||
const nodeNames = nodes.map((node: INodeUi) => {
|
||||
return node.name;
|
||||
});
|
||||
|
||||
// Check first if the current name is already unique
|
||||
if (!nodeNames.includes(originalName) && !additinalUsedNames.includes(originalName)) {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
const nameMatch = originalName.match(/(.*\D+)(\d*)/);
|
||||
let ignore, baseName, nameIndex, uniqueName;
|
||||
let index = 1;
|
||||
|
||||
if (nameMatch === null) {
|
||||
// Name is only a number
|
||||
index = parseInt(originalName, 10);
|
||||
baseName = '';
|
||||
uniqueName = baseName + index;
|
||||
} else {
|
||||
// Name is string or string/number combination
|
||||
[ignore, baseName, nameIndex] = nameMatch;
|
||||
if (nameIndex !== '') {
|
||||
index = parseInt(nameIndex, 10);
|
||||
}
|
||||
uniqueName = baseName;
|
||||
}
|
||||
|
||||
while (
|
||||
nodeNames.includes(uniqueName) ||
|
||||
additinalUsedNames.includes(uniqueName)
|
||||
) {
|
||||
uniqueName = baseName + (index++);
|
||||
}
|
||||
|
||||
return uniqueName;
|
||||
};
|
||||
|
||||
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
||||
if (connection && connection.connector) {
|
||||
if (targetEndpoint) {
|
||||
connection.connector.setTargetEndpoint(targetEndpoint);
|
||||
}
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
||||
hideOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||
}
|
||||
};
|
||||
|
||||
export const showPullConnectionState = (connection: Connection) => {
|
||||
if (connection && connection.connector) {
|
||||
connection.connector.resetTargetEndpoint();
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PULL);
|
||||
showOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||
}
|
||||
};
|
||||
|
||||
export const resetConnectionAfterPull = (connection: Connection) => {
|
||||
if (connection && connection.connector) {
|
||||
connection.connector.resetTargetEndpoint();
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||
}
|
||||
};
|
||||
|
||||
export const resetInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||
if (inputNameOverlay) {
|
||||
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION);
|
||||
}
|
||||
};
|
||||
|
||||
export const moveBackInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||
if (inputNameOverlay) {
|
||||
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED);
|
||||
}
|
||||
};
|
||||
|
||||
export const addConnectionActionsOverlay = (connection: Connection, onDelete: Function, onAdd: Function) => {
|
||||
if (getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID)) {
|
||||
return; // avoid free floating actions when moving connection from one node to another
|
||||
}
|
||||
connection.addOverlay([
|
||||
'Label',
|
||||
{
|
||||
id: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||
label: `<div class="add">${getIcon('plus')}</div> <div class="delete">${getIcon('trash')}</div>`,
|
||||
cssClass: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||
visible: false,
|
||||
events: {
|
||||
mousedown: (overlay: Overlay, event: MouseEvent) => {
|
||||
const element = event.target as HTMLElement;
|
||||
if (element.classList.contains('delete') || (element.parentElement && element.parentElement.classList.contains('delete'))) {
|
||||
onDelete();
|
||||
}
|
||||
else if (element.classList.contains('add') || (element.parentElement && element.parentElement.classList.contains('add'))) {
|
||||
onAdd();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
export const getOutputEndpointUUID = (nodeIndex: string, outputIndex: number) => {
|
||||
return `${nodeIndex}${OUTPUT_UUID_KEY}${outputIndex}`;
|
||||
};
|
||||
|
||||
export const getInputEndpointUUID = (nodeIndex: string, inputIndex: number) => {
|
||||
return `${nodeIndex}${INPUT_UUID_KEY}${inputIndex}`;
|
||||
};
|
|
@ -1,85 +0,0 @@
|
|||
import { INodeUi, IZoomConfig } from "@/Interface";
|
||||
|
||||
interface ICorners {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||
return nodes.reduce((leftmostTop, node) => {
|
||||
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
||||
return leftmostTop;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkflowCorners = (nodes: INodeUi[]): ICorners => {
|
||||
return nodes.reduce((accu: ICorners, node: INodeUi) => {
|
||||
if (node.position[0] < accu.minX) {
|
||||
accu.minX = node.position[0];
|
||||
}
|
||||
if (node.position[1] < accu.minY) {
|
||||
accu.minY = node.position[1];
|
||||
}
|
||||
if (node.position[0] > accu.maxX) {
|
||||
accu.maxX = node.position[0];
|
||||
}
|
||||
if (node.position[1] > accu.maxY) {
|
||||
accu.maxY = node.position[1];
|
||||
}
|
||||
|
||||
return accu;
|
||||
}, {
|
||||
minX: nodes[0].position[0],
|
||||
minY: nodes[0].position[1],
|
||||
maxX: nodes[0].position[0],
|
||||
maxY: nodes[0].position[1],
|
||||
});
|
||||
};
|
||||
|
||||
export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale /= 1.25;
|
||||
xOffset /= 1.25;
|
||||
yOffset /= 1.25;
|
||||
xOffset += window.innerWidth / 10;
|
||||
yOffset += window.innerHeight / 10;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleBigger = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
|
||||
scale *= 1.25;
|
||||
xOffset -= window.innerWidth / 10;
|
||||
yOffset -= window.innerHeight / 10;
|
||||
xOffset *= 1.25;
|
||||
yOffset *= 1.25;
|
||||
|
||||
return {
|
||||
scale,
|
||||
offset: [xOffset, yOffset],
|
||||
};
|
||||
};
|
||||
|
||||
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
|
||||
if (config.scale > 1) { // zoomed in
|
||||
while (config.scale > 1) {
|
||||
config = scaleSmaller(config);
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (config.scale < 1) {
|
||||
config = scaleBigger(config);
|
||||
}
|
||||
}
|
||||
|
||||
config.scale = 1;
|
||||
|
||||
return config;
|
||||
};
|
Loading…
Reference in a new issue