mirror of
https://github.com/n8n-io/n8n.git
synced 2024-09-19 22:37:31 -07:00
refactor(editor): Upgrade to jsPlumb 5 (#4989)
* WIP: Nodeview * Replace types * Finish N8nPlus endpoint type * Working on connector * Apply prettier * Fixed prettier issues * Debugging rendering * Fixed connectorrs position recalc * Fix snapping and output labels, WIP dragging * Fix N8nPlus endpoint rendering issues * Cleanup * Fix undo/redo and canvas add button position, cleanup * Cleanup * Revert accidental CLI changes * Fix pnpm-lock * Address bugs that came up during review * Reset CLI package from master * Various fixes * Fix run items label toggling * Linter fixes * Fix stalk size for larger run items label * Remove comment * Correctly reset workspace after renaming the node * Fix canvas e2e tests * Fix undo/redo tests * Fix stalk positioning and triggering of endpoint overlays * Repaint connections on pin removal * Limit repaintings * Unbind jsPlumb events on deactivation * Fix jsPlumb managment of Sticky and minor memort managment improvments * Address rest of PR points * Lint fix * Copy patches folder to docker * Fix e2e tests * set allowNonAppliedPatches to allow build * fix(editor): Handling router errors when navigation is canceled by user (#5271) * 🔨 Handling router errors in main sidebar, removing unused code * 🔨 Handling router errors in modals * ci(core): Fix docker nightly/custom image build (no-changelog) (#5284) * ci(core): Copy patches dir to Docker (no-changelog) * Update patch * Update package-lock * reapply the patch * skip patchedDependencies after the frontend is built --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> * Fix connector hover state on success * Remove allowNonAppliedPatches from package.json --------- Co-authored-by: Milorad FIlipović <milorad@n8n.io> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
5cb7e5007d
commit
766501723b
|
@ -39,11 +39,9 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
WorkflowPage.getters.nodeConnections().realHover();
|
||||||
cy.get('.connection-actions .add').invoke('show');
|
cy.get('.connection-actions .add').filter(':visible').click();
|
||||||
cy.get('.connection-actions .add').should('be.visible');
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME, false);
|
||||||
cy.get('.connection-actions .add').click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 3);
|
WorkflowPage.getters.canvasNodes().should('have.have.length', 3);
|
||||||
|
@ -141,8 +139,8 @@ describe('Undo/Redo', () => {
|
||||||
it('should undo/redo deleting a connection by pressing delete button', () => {
|
it('should undo/redo deleting a connection by pressing delete button', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
WorkflowPage.getters.nodeConnections().realHover();
|
||||||
cy.get('.connection-actions .delete').click();
|
cy.get('.connection-actions .delete').filter(':visible').should('be.visible').click();
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
|
|
|
@ -61,11 +61,9 @@ describe('Canvas Actions', () => {
|
||||||
it('should add note between two connected nodes', () => {
|
it('should add note between two connected nodes', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||||
cy.get('.connection-actions .add').as('AddNodeConnectionButton');
|
cy.get('.connection-actions .add').filter(':visible').should('be.visible').click()
|
||||||
cy.get('@AddNodeConnectionButton').invoke('show');
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, false);
|
||||||
cy.get('@AddNodeConnectionButton').should('be.visible').click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
||||||
// And last node should be pushed to the right
|
// And last node should be pushed to the right
|
||||||
|
@ -219,8 +217,8 @@ describe('Canvas Actions', () => {
|
||||||
it('should delete connections by pressing the delete button', () => {
|
it('should delete connections by pressing the delete button', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||||
cy.get('.connection-actions .delete').click();
|
cy.get('.connection-actions .delete').filter(':visible').should('be.visible').click();
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ describe('Credentials', () => {
|
||||||
it('should correctly render required and optional credentials', () => {
|
it('should correctly render required and optional credentials', () => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true);
|
workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true, true);
|
||||||
cy.get('body').type('{downArrow}');
|
cy.get('body').type('{downArrow}');
|
||||||
cy.get('body').type('{enter}');
|
cy.get('body').type('{enter}');
|
||||||
// Select incoming authentication
|
// Select incoming authentication
|
||||||
|
|
|
@ -53,7 +53,7 @@ describe('NDV', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show correct validation state for resource locator params', () => {
|
it('should show correct validation state for resource locator params', () => {
|
||||||
workflowPage.actions.addNodeToCanvas('Typeform', true);
|
workflowPage.actions.addNodeToCanvas('Typeform', true, false);
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
cy.get('.has-issues').should('have.length', 0);
|
cy.get('.has-issues').should('have.length', 0);
|
||||||
cy.get('[class*=hasIssues]').should('have.length', 0);
|
cy.get('[class*=hasIssues]').should('have.length', 0);
|
||||||
|
@ -66,7 +66,7 @@ describe('NDV', () => {
|
||||||
|
|
||||||
it('should show validation errors only after blur or re-opening of NDV', () => {
|
it('should show validation errors only after blur or re-opening of NDV', () => {
|
||||||
workflowPage.actions.addNodeToCanvas('Manual Trigger');
|
workflowPage.actions.addNodeToCanvas('Manual Trigger');
|
||||||
workflowPage.actions.addNodeToCanvas('Airtable', true);
|
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
cy.get('.has-issues').should('have.length', 0);
|
cy.get('.has-issues').should('have.length', 0);
|
||||||
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
|
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
|
||||||
|
|
|
@ -92,8 +92,11 @@ export class WorkflowPage extends BasePage {
|
||||||
this.getters.nodeCreatorSearchBar().type('{enter}');
|
this.getters.nodeCreatorSearchBar().type('{enter}');
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
},
|
},
|
||||||
addNodeToCanvas: (nodeDisplayName: string, preventNdvClose?: boolean) => {
|
addNodeToCanvas: (nodeDisplayName: string, plusButtonClick = true, preventNdvClose?: boolean) => {
|
||||||
|
if (plusButtonClick) {
|
||||||
this.getters.nodeCreatorPlusButton().click();
|
this.getters.nodeCreatorPlusButton().click();
|
||||||
|
}
|
||||||
|
|
||||||
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
|
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
|
||||||
this.getters.nodeCreatorSearchBar().type('{enter}');
|
this.getters.nodeCreatorSearchBar().type('{enter}');
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
//
|
//
|
||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
import "cypress-real-events";
|
||||||
import { WorkflowsPage, SigninPage, SignupPage } from '../pages';
|
import { WorkflowsPage, SigninPage, SignupPage } from '../pages';
|
||||||
import { N8N_AUTH_COOKIE } from '../constants';
|
import { N8N_AUTH_COOKIE } from '../constants';
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"@types/node": "^16.11.22",
|
"@types/node": "^16.11.22",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^10.0.3",
|
"cypress": "^10.0.3",
|
||||||
|
"cypress-real-events": "^1.7.6",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jest-environment-jsdom": "^29.3.1",
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
"jest-mock": "^29.3.1",
|
"jest-mock": "^29.3.1",
|
||||||
|
|
|
@ -39,6 +39,11 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||||
|
"@jsplumb/browser-ui": "^5.13.2",
|
||||||
|
"@jsplumb/common": "^5.13.2",
|
||||||
|
"@jsplumb/connector-bezier": "^5.13.2",
|
||||||
|
"@jsplumb/core": "^5.13.2",
|
||||||
|
"@jsplumb/util": "^5.13.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"codemirror-lang-html-n8n": "^1.0.0",
|
"codemirror-lang-html-n8n": "^1.0.0",
|
||||||
"codemirror-lang-n8n-expression": "^0.1.0",
|
"codemirror-lang-n8n-expression": "^0.1.0",
|
||||||
|
@ -50,7 +55,6 @@
|
||||||
"humanize-duration": "^3.27.2",
|
"humanize-duration": "^3.27.2",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
"jsonpath": "^1.1.1",
|
"jsonpath": "^1.1.1",
|
||||||
"jsplumb": "2.15.4",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
|
|
@ -1,17 +1,6 @@
|
||||||
import { CREDENTIAL_EDIT_MODAL_KEY } from './constants';
|
import { CREDENTIAL_EDIT_MODAL_KEY } from './constants';
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { IMenuItem } from 'n8n-design-system';
|
import { IMenuItem } from 'n8n-design-system';
|
||||||
import {
|
|
||||||
jsPlumbInstance,
|
|
||||||
DragOptions,
|
|
||||||
DropOptions,
|
|
||||||
ElementGroupRef,
|
|
||||||
Endpoint,
|
|
||||||
EndpointOptions,
|
|
||||||
EndpointRectangle,
|
|
||||||
EndpointRectangleOptions,
|
|
||||||
EndpointSpec,
|
|
||||||
} from 'jsplumb';
|
|
||||||
import {
|
import {
|
||||||
GenericValue,
|
GenericValue,
|
||||||
IConnections,
|
IConnections,
|
||||||
|
@ -47,98 +36,6 @@ import { BulkCommand, Undoable } from '@/models/history';
|
||||||
|
|
||||||
export * from 'n8n-design-system/types';
|
export * from 'n8n-design-system/types';
|
||||||
|
|
||||||
declare module 'jsplumb' {
|
|
||||||
interface PaintStyle {
|
|
||||||
stroke?: string;
|
|
||||||
fill?: string;
|
|
||||||
strokeWidth?: number;
|
|
||||||
outlineStroke?: string;
|
|
||||||
outlineWidth?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend jsPlumb Anchor interface
|
|
||||||
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;
|
|
||||||
bind(event: string, callback: Function): void;
|
|
||||||
removeOverlay(name: string): void;
|
|
||||||
removeOverlays(): void;
|
|
||||||
setParameter(name: string, value: any): void;
|
|
||||||
setPaintStyle(arg0: PaintStyle): void;
|
|
||||||
addOverlay(arg0: any[]): void;
|
|
||||||
setConnector(arg0: any[]): void;
|
|
||||||
getUuids(): [string, string];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Endpoint {
|
|
||||||
endpoint: any;
|
|
||||||
elementId: string;
|
|
||||||
__meta?: {
|
|
||||||
nodeName: string;
|
|
||||||
nodeId: string;
|
|
||||||
index: number;
|
|
||||||
totalEndpoints: number;
|
|
||||||
};
|
|
||||||
getUuid(): string;
|
|
||||||
getOverlay(name: string): any;
|
|
||||||
repaint(params?: object): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface N8nPlusEndpoint extends Endpoint {
|
|
||||||
setSuccessOutput(message: string): void;
|
|
||||||
clearSuccessOutput(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Overlay {
|
|
||||||
setVisible(visible: boolean): void;
|
|
||||||
setLocation(location: number): void;
|
|
||||||
canvas?: HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnConnectionBindInfo {
|
|
||||||
originalSourceEndpoint: Endpoint;
|
|
||||||
originalTargetEndpoint: Endpoint;
|
|
||||||
getParameters(): { index: number };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndpointOptions from jsplumb seems incomplete and wrong so we define an own one
|
|
||||||
export type IEndpointOptions = Omit<EndpointOptions, 'endpoint' | 'dragProxy'> & {
|
|
||||||
endpointStyle: EndpointStyle;
|
|
||||||
endpointHoverStyle: EndpointStyle;
|
|
||||||
endpoint?: EndpointSpec | string;
|
|
||||||
dragAllowedWhenFull?: boolean;
|
|
||||||
dropOptions?: DropOptions & {
|
|
||||||
tolerance: string;
|
|
||||||
};
|
|
||||||
dragProxy?:
|
|
||||||
| string
|
|
||||||
| string[]
|
|
||||||
| EndpointSpec
|
|
||||||
| [EndpointRectangle, EndpointRectangleOptions & { strokeWidth: number }];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EndpointStyle = {
|
export type EndpointStyle = {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
@ -152,21 +49,6 @@ export type EndpointStyle = {
|
||||||
hoverMessage?: string;
|
hoverMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IDragOptions = DragOptions & {
|
|
||||||
grid: [number, number];
|
|
||||||
filter: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type IJsPlumbInstance = Omit<jsPlumbInstance, 'addEndpoint' | 'draggable'> & {
|
|
||||||
clearDragSelection: () => void;
|
|
||||||
addEndpoint(
|
|
||||||
el: ElementGroupRef,
|
|
||||||
params?: IEndpointOptions,
|
|
||||||
referenceParams?: IEndpointOptions,
|
|
||||||
): Endpoint | Endpoint[];
|
|
||||||
draggable(el: {}, options?: IDragOptions): IJsPlumbInstance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IUpdateInformation {
|
export interface IUpdateInformation {
|
||||||
name: string;
|
name: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="node-wrapper" :style="nodePosition" :id="nodeId" data-test-id="canvas-node">
|
<div
|
||||||
|
class="node-wrapper"
|
||||||
|
:style="nodePosition"
|
||||||
|
:id="nodeId"
|
||||||
|
data-test-id="canvas-node"
|
||||||
|
:ref="data.name"
|
||||||
|
:data-name="data.name"
|
||||||
|
>
|
||||||
<div class="select-background" v-show="isSelected"></div>
|
<div class="select-background" v-show="isSelected"></div>
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
|
@ -7,8 +14,6 @@
|
||||||
'touch-active': isTouchActive,
|
'touch-active': isTouchActive,
|
||||||
'is-touch-device': isTouchDevice,
|
'is-touch-device': isTouchDevice,
|
||||||
}"
|
}"
|
||||||
:data-name="data.name"
|
|
||||||
:ref="data.name"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="nodeClass"
|
:class="nodeClass"
|
||||||
|
@ -515,7 +520,6 @@ export default mixins(
|
||||||
this.data.name,
|
this.data.name,
|
||||||
!this.data.disabled,
|
!this.data.disabled,
|
||||||
this.data.disabled === true,
|
this.data.disabled === true,
|
||||||
this,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
this.$telemetry.track('User clicked node hover button', {
|
this.$telemetry.track('User clicked node hover button', {
|
||||||
|
@ -698,6 +702,7 @@ export default mixins(
|
||||||
|
|
||||||
.items-count {
|
.items-count {
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -807,20 +812,33 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dot-output-endpoint:hover circle {
|
||||||
|
fill: var(--color-primary);
|
||||||
|
}
|
||||||
/** connector */
|
/** connector */
|
||||||
.jtk-connector {
|
.jtk-connector {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jtk-floating-endpoint {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.jtk-connector path {
|
.jtk-connector path {
|
||||||
transition: stroke 0.1s ease-in-out;
|
transition: stroke 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jtk-connector.success {
|
.jtk-overlay {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.jtk-connector {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
.node-input-endpoint-label,
|
||||||
|
.node-output-endpoint-label,
|
||||||
|
.connection-run-items-label {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
.jtk-connector.jtk-hover {
|
.jtk-connector.jtk-hover {
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
}
|
}
|
||||||
|
@ -828,30 +846,25 @@ export default mixins(
|
||||||
.jtk-endpoint.plus-endpoint {
|
.jtk-endpoint.plus-endpoint {
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jtk-endpoint.dot-output-endpoint {
|
.jtk-endpoint.dot-output-endpoint {
|
||||||
z-index: 7;
|
z-index: 7;
|
||||||
}
|
overflow: auto;
|
||||||
|
|
||||||
.jtk-overlay {
|
|
||||||
z-index: 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled-linethrough {
|
.disabled-linethrough {
|
||||||
z-index: 8;
|
z-index: 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jtk-connector.jtk-dragging {
|
|
||||||
z-index: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jtk-drag-active.dot-output-endpoint,
|
.jtk-drag-active.dot-output-endpoint,
|
||||||
.jtk-drag-active.rect-input-endpoint {
|
.jtk-drag-active.rect-input-endpoint {
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
.rect-input-endpoint > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.connection-actions {
|
.connection-actions {
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-options {
|
.node-options {
|
||||||
|
@ -861,88 +874,207 @@ export default mixins(
|
||||||
.drop-add-node-label {
|
.drop-add-node-label {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jtk-connector.success:not(.jtk-hover) {
|
||||||
|
path:not(.jtk-connector-outline) {
|
||||||
|
stroke: var(--color-success-light);
|
||||||
|
}
|
||||||
|
path[jtk-overlay-id='endpoint-arrow'],
|
||||||
|
path[jtk-overlay-id='midpoint-arrow'] {
|
||||||
|
fill: var(--color-success-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
$--stalklength: 40px;
|
:root {
|
||||||
$--box-size-medium: 24px;
|
--endpoint-size-small: 14px;
|
||||||
$--box-size-small: 18px;
|
--endpoint-size-medium: 18px;
|
||||||
|
--stalk-size: 40px;
|
||||||
|
--stalk-success-size: 87px;
|
||||||
|
--stalk-long-size: 127px;
|
||||||
|
--plus-endpoint-box-size: 24px;
|
||||||
|
--plus-endpoint-box-size-small: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-svg-circle {
|
||||||
|
z-index: 111;
|
||||||
|
circle {
|
||||||
|
stroke: var(--color-foreground-xdark);
|
||||||
|
stroke-width: 2px;
|
||||||
|
fill: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
circle {
|
||||||
|
stroke: var(--color-primary);
|
||||||
|
fill: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.plus-stalk {
|
||||||
|
width: calc(var(--stalk-size) + 2px);
|
||||||
|
border: 1px solid var(--color-foreground-dark);
|
||||||
|
margin-left: calc(var(--stalk-size) / 2);
|
||||||
|
z-index: 3;
|
||||||
|
&.ep-success {
|
||||||
|
border-color: var(--color-success-light);
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: attr(data-label);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
line-height: 1.3em;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.connection-run-items-label {
|
||||||
|
// Disable points events so that the label does not block the connection
|
||||||
|
// mouse over event.
|
||||||
|
pointer-events: none;
|
||||||
|
span {
|
||||||
|
border-radius: 7px;
|
||||||
|
background-color: hsla(
|
||||||
|
var(--color-canvas-background-h),
|
||||||
|
var(--color-canvas-background-s),
|
||||||
|
var(--color-canvas-background-l),
|
||||||
|
0.85
|
||||||
|
);
|
||||||
|
line-height: 1.3em;
|
||||||
|
padding: 0px 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
color: var(--color-success);
|
||||||
|
margin-top: -15px;
|
||||||
|
|
||||||
|
&.floating {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-input-name-label {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.plus-endpoint {
|
.plus-endpoint {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
.plus-stalk {
|
margin-left: calc((var(--stalk-size) + var(--plus-endpoint-box-size) / 2) - 1px);
|
||||||
border-top: 2px solid var(--color-foreground-dark);
|
g {
|
||||||
position: absolute;
|
fill: var(--color-background-xlight);
|
||||||
width: $--stalklength;
|
|
||||||
height: 0;
|
|
||||||
right: 100%;
|
|
||||||
top: calc(50% - 1px);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
.connection-run-items-label {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: none;
|
|
||||||
left: calc(50% + 4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plus-container {
|
&:hover {
|
||||||
color: var(--color-foreground-xdark);
|
path {
|
||||||
border: 2px solid var(--color-foreground-xdark);
|
fill: var(--color-primary);
|
||||||
background-color: var(--color-background-xlight);
|
}
|
||||||
border-radius: var(--border-radius-base);
|
rect {
|
||||||
height: $--box-size-medium;
|
stroke: var(--color-primary);
|
||||||
width: $--box-size-medium;
|
}
|
||||||
|
}
|
||||||
display: inline-flex;
|
path {
|
||||||
align-items: center;
|
fill: var(--color-foreground-xdark);
|
||||||
justify-content: center;
|
}
|
||||||
font-size: var(--font-size-2xs);
|
rect {
|
||||||
position: absolute;
|
stroke: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
height: $--box-size-small;
|
margin-left: calc((var(--stalk-size) + var(--plus-endpoint-box-size-small) / 2));
|
||||||
width: $--box-size-small;
|
g {
|
||||||
font-size: 8px;
|
transform: scale(0.75);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
rect {
|
||||||
|
stroke-width: 2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover .plus-container {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
&:hover .drop-hover-message {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-plus {
|
&.hidden {
|
||||||
width: 1em;
|
display: none;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.drop-hover-message {
|
.node-input-endpoint-label,
|
||||||
|
.node-output-endpoint-label {
|
||||||
|
background-color: hsla(
|
||||||
|
var(--color-canvas-background-h),
|
||||||
|
var(--color-canvas-background-s),
|
||||||
|
var(--color-canvas-background-l),
|
||||||
|
0.85
|
||||||
|
);
|
||||||
|
border-radius: 7px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
padding: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-output-endpoint-label {
|
||||||
|
margin-left: calc(var(--endpoint-size-small) + var(--spacing-3xs));
|
||||||
|
}
|
||||||
|
.node-input-endpoint-label {
|
||||||
|
text-align: right;
|
||||||
|
margin-left: -25px;
|
||||||
|
|
||||||
|
&--moved {
|
||||||
|
margin-left: -40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover-message.jtk-overlay {
|
||||||
|
--hover-message-width: 110px;
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
line-height: var(--font-line-height-regular);
|
line-height: var(--font-line-height-regular);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
|
width: var(--hover-message-width);
|
||||||
position: absolute;
|
margin-left: calc(
|
||||||
top: -6px;
|
(var(--hover-message-width) / 2) + var(--stalk-size) + var(--plus-endpoint-box-size) +
|
||||||
left: calc(100% + 8px);
|
var(--spacing-2xs)
|
||||||
width: 200px;
|
);
|
||||||
display: none;
|
opacity: 0;
|
||||||
}
|
pointer-events: none;
|
||||||
|
&.small {
|
||||||
&.hidden > * {
|
margin-left: calc(
|
||||||
display: none;
|
(var(--hover-message-width) / 2) + var(--stalk-size) + var(--plus-endpoint-box-size-small) +
|
||||||
}
|
var(--spacing-2xs)
|
||||||
|
);
|
||||||
&.success .plus-stalk {
|
|
||||||
border-color: var(--color-success-light);
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline;
|
|
||||||
}
|
}
|
||||||
|
&.visible {
|
||||||
|
pointer-events: all;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ep-success {
|
||||||
|
--stalk-size: var(--stalk-success-size);
|
||||||
|
}
|
||||||
|
.long-stalk {
|
||||||
|
--stalk-size: var(--stalk-long-size);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -549,7 +549,7 @@ export default mixins(externalHooks, nodeHelpers).extend({
|
||||||
},
|
},
|
||||||
nameChanged(name: string) {
|
nameChanged(name: string) {
|
||||||
if (this.node) {
|
if (this.node) {
|
||||||
this.historyStore.pushCommandToUndo(new RenameNodeCommand(this.node.name, name, this));
|
this.historyStore.pushCommandToUndo(new RenameNodeCommand(this.node.name, name));
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.valueChanged({
|
this.valueChanged({
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="sticky-wrapper" :style="stickyPosition" :id="nodeId" ref="sticky">
|
<div
|
||||||
|
class="sticky-wrapper"
|
||||||
|
:id="nodeId"
|
||||||
|
:ref="data.name"
|
||||||
|
:style="stickyPosition"
|
||||||
|
:data-name="data.name"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
'sticky-default': true,
|
'sticky-default': true,
|
||||||
|
@ -11,8 +17,6 @@
|
||||||
<div class="select-sticky-background" v-show="isSelected" />
|
<div class="select-sticky-background" v-show="isSelected" />
|
||||||
<div
|
<div
|
||||||
class="sticky-box"
|
class="sticky-box"
|
||||||
:data-name="data.name"
|
|
||||||
:ref="data.name"
|
|
||||||
@click.left="mouseLeftClick"
|
@click.left="mouseLeftClick"
|
||||||
v-touch:start="touchStart"
|
v-touch:start="touchStart"
|
||||||
v-touch:end="touchEnd"
|
v-touch:end="touchEnd"
|
||||||
|
@ -186,9 +190,6 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
if (!this.isSelected && this.node) {
|
if (!this.isSelected && this.node) {
|
||||||
this.$emit('nodeSelected', this.node.name, false, true);
|
this.$emit('nodeSelected', this.node.name, false, true);
|
||||||
}
|
}
|
||||||
if (this.node) {
|
|
||||||
this.instance.destroyDraggable(this.node.id); // todo avoid destroying if possible
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onResize({ height, width, dX, dY }: { width: number; height: number; dX: number; dY: number }) {
|
onResize({ height, width, dX, dY }: { width: number; height: number; dX: number; dY: number }) {
|
||||||
if (!this.node) {
|
if (!this.node) {
|
||||||
|
@ -202,7 +203,6 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
},
|
},
|
||||||
onResizeEnd() {
|
onResizeEnd() {
|
||||||
this.isResizing = false;
|
this.isResizing = false;
|
||||||
this.__makeInstanceDraggable(this.data);
|
|
||||||
},
|
},
|
||||||
setParameters(params: { content?: string; height?: number; width?: number }) {
|
setParameters(params: { content?: string; height?: number; width?: number }) {
|
||||||
if (this.node) {
|
if (this.node) {
|
||||||
|
@ -252,8 +252,6 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
.sticky-default {
|
.sticky-default {
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
.sticky-box {
|
.sticky-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'prismjs';
|
||||||
import 'prismjs/themes/prism.css';
|
import 'prismjs/themes/prism.css';
|
||||||
import 'vue-prism-editor/dist/VuePrismEditor.css';
|
import 'vue-prism-editor/dist/VuePrismEditor.css';
|
||||||
import 'vue-json-pretty/lib/styles.css';
|
import 'vue-json-pretty/lib/styles.css';
|
||||||
|
import '@jsplumb/browser-ui/css/jsplumbtoolkit.css';
|
||||||
import 'n8n-design-system/css/index.scss';
|
import 'n8n-design-system/css/index.scss';
|
||||||
import './n8n-theme.scss';
|
import './n8n-theme.scss';
|
||||||
|
|
||||||
|
|
|
@ -203,12 +203,12 @@ export const mouseSelect = mixins(deviceSupportHelpers).extend({
|
||||||
nodeDeselected(node: INodeUi) {
|
nodeDeselected(node: INodeUi) {
|
||||||
this.uiStore.removeNodeFromSelection(node);
|
this.uiStore.removeNodeFromSelection(node);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.instance.removeFromDragSelection(node.id);
|
this.instance.removeFromDragSelection(this.$refs[`node-${node.id}`][0].$el);
|
||||||
},
|
},
|
||||||
nodeSelected(node: INodeUi) {
|
nodeSelected(node: INodeUi) {
|
||||||
this.uiStore.addSelectedNode(node);
|
this.uiStore.addSelectedNode(node);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.instance.addToDragSelection(node.id);
|
this.instance.addToDragSelection(this.$refs[`node-${node.id}`][0].$el);
|
||||||
},
|
},
|
||||||
deselectAllNodes() {
|
deselectAllNodes() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { IJsPlumbInstance, IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
|
import { INodeUi } from '@/Interface';
|
||||||
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
|
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
|
||||||
import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
import { NO_OP_NODE_TYPE } from '@/constants';
|
||||||
|
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useUIStore } from '@/stores/ui';
|
import { useUIStore } from '@/stores/ui';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows';
|
import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||||
|
import { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||||
|
import { EndpointOptions } from '@jsplumb/core';
|
||||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||||
import { getStyleTokenValue } from '@/utils';
|
|
||||||
import { useHistoryStore } from '@/stores/history';
|
import { useHistoryStore } from '@/stores/history';
|
||||||
import { MoveNodeCommand } from '@/models/history';
|
import { useCanvasStore } from '@/stores/canvas';
|
||||||
|
|
||||||
export const nodeBase = mixins(deviceSupportHelpers).extend({
|
export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -27,7 +28,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useNodeTypesStore, useUIStore, useWorkflowsStore, useHistoryStore),
|
...mapStores(useNodeTypesStore, useUIStore, useCanvasStore, useWorkflowsStore, useHistoryStore),
|
||||||
data(): INodeUi | null {
|
data(): INodeUi | null {
|
||||||
return this.workflowsStore.getNodeByName(this.name);
|
return this.workflowsStore.getNodeByName(this.name);
|
||||||
},
|
},
|
||||||
|
@ -40,7 +41,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: Object as PropType<IJsPlumbInstance>,
|
type: Object as PropType<BrowserJsPlumbInstance>,
|
||||||
},
|
},
|
||||||
isReadOnly: {
|
isReadOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -79,18 +80,15 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
const anchorPosition =
|
const anchorPosition =
|
||||||
NodeViewUtils.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
|
NodeViewUtils.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
|
||||||
|
|
||||||
const newEndpointData: IEndpointOptions = {
|
const newEndpointData: EndpointOptions = {
|
||||||
uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, index),
|
uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, index),
|
||||||
anchor: anchorPosition,
|
anchor: anchorPosition,
|
||||||
maxConnections: -1,
|
maxConnections: -1,
|
||||||
endpoint: 'Rectangle',
|
endpoint: 'Rectangle',
|
||||||
endpointStyle: NodeViewUtils.getInputEndpointStyle(
|
paintStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
|
||||||
nodeTypeData,
|
hoverPaintStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||||
'--color-foreground-xdark',
|
source: false,
|
||||||
),
|
target: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
|
||||||
endpointHoverStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-primary'),
|
|
||||||
isSource: false,
|
|
||||||
isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
|
|
||||||
parameters: {
|
parameters: {
|
||||||
nodeId: this.nodeId,
|
nodeId: this.nodeId,
|
||||||
type: inputName,
|
type: inputName,
|
||||||
|
@ -99,20 +97,17 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
enabled: !this.isReadOnly, // enabled in default case to allow dragging
|
enabled: !this.isReadOnly, // enabled in default case to allow dragging
|
||||||
cssClass: 'rect-input-endpoint',
|
cssClass: 'rect-input-endpoint',
|
||||||
dragAllowedWhenFull: true,
|
dragAllowedWhenFull: true,
|
||||||
dropOptions: {
|
|
||||||
tolerance: 'touch',
|
|
||||||
hoverClass: 'dropHover',
|
hoverClass: 'dropHover',
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const endpoint = this.instance?.addEndpoint(
|
||||||
|
this.$refs[this.data.name] as Element,
|
||||||
|
newEndpointData,
|
||||||
|
);
|
||||||
if (nodeTypeData.inputNames) {
|
if (nodeTypeData.inputNames) {
|
||||||
// Apply input names if they got set
|
// Apply input names if they got set
|
||||||
newEndpointData.overlays = [
|
endpoint.addOverlay(NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]));
|
||||||
NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
|
|
||||||
if (!Array.isArray(endpoint)) {
|
if (!Array.isArray(endpoint)) {
|
||||||
endpoint.__meta = {
|
endpoint.__meta = {
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
|
@ -133,6 +128,9 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
// this.instance.makeTarget(this.nodeId, newEndpointData);
|
// this.instance.makeTarget(this.nodeId, newEndpointData);
|
||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
if (nodeTypeData.inputs.length === 0) {
|
||||||
|
this.instance.manage(this.$refs[this.data.name] as Element);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
__addOutputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
__addOutputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
|
||||||
let index;
|
let index;
|
||||||
|
@ -153,37 +151,46 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
const anchorPosition =
|
const anchorPosition =
|
||||||
NodeViewUtils.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
|
NodeViewUtils.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
|
||||||
|
|
||||||
const newEndpointData: IEndpointOptions = {
|
const newEndpointData: EndpointOptions = {
|
||||||
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
||||||
anchor: anchorPosition,
|
anchor: anchorPosition,
|
||||||
maxConnections: -1,
|
maxConnections: -1,
|
||||||
endpoint: 'Dot',
|
|
||||||
endpointStyle: NodeViewUtils.getOutputEndpointStyle(
|
endpoint: {
|
||||||
|
type: 'Dot',
|
||||||
|
options: {
|
||||||
|
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paintStyle: NodeViewUtils.getOutputEndpointStyle(
|
||||||
nodeTypeData,
|
nodeTypeData,
|
||||||
'--color-foreground-xdark',
|
'--color-foreground-xdark',
|
||||||
),
|
),
|
||||||
endpointHoverStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
|
hoverPaintStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||||
isSource: true,
|
source: true,
|
||||||
isTarget: false,
|
target: false,
|
||||||
enabled: !this.isReadOnly,
|
enabled: !this.isReadOnly,
|
||||||
parameters: {
|
parameters: {
|
||||||
nodeId: this.nodeId,
|
nodeId: this.nodeId,
|
||||||
type: inputName,
|
type: inputName,
|
||||||
index,
|
index,
|
||||||
},
|
},
|
||||||
|
hoverClass: 'dot-output-endpoint-hover',
|
||||||
|
connectionsDirected: true,
|
||||||
cssClass: 'dot-output-endpoint',
|
cssClass: 'dot-output-endpoint',
|
||||||
dragAllowedWhenFull: false,
|
dragAllowedWhenFull: false,
|
||||||
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const endpoint = this.instance.addEndpoint(
|
||||||
|
this.$refs[this.data.name] as Element,
|
||||||
|
newEndpointData,
|
||||||
|
);
|
||||||
if (nodeTypeData.outputNames) {
|
if (nodeTypeData.outputNames) {
|
||||||
// Apply output names if they got set
|
// Apply output names if they got set
|
||||||
newEndpointData.overlays = [
|
const overlaySpec = NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]);
|
||||||
NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]),
|
const overlay = endpoint.addOverlay(overlaySpec);
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = this.instance.addEndpoint(this.nodeId, { ...newEndpointData });
|
|
||||||
if (!Array.isArray(endpoint)) {
|
if (!Array.isArray(endpoint)) {
|
||||||
endpoint.__meta = {
|
endpoint.__meta = {
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
|
@ -194,26 +201,28 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isReadOnly) {
|
if (!this.isReadOnly) {
|
||||||
const plusEndpointData: IEndpointOptions = {
|
const plusEndpointData: EndpointOptions = {
|
||||||
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
||||||
anchor: anchorPosition,
|
anchor: anchorPosition,
|
||||||
maxConnections: -1,
|
maxConnections: -1,
|
||||||
endpoint: 'N8nPlus',
|
endpoint: {
|
||||||
isSource: true,
|
type: 'N8nPlus',
|
||||||
isTarget: false,
|
options: {
|
||||||
enabled: !this.isReadOnly,
|
dimensions: 24,
|
||||||
endpointStyle: {
|
connectedEndpoint: endpoint,
|
||||||
fill: getStyleTokenValue('--color-xdark'),
|
|
||||||
outlineStroke: 'none',
|
|
||||||
hover: false,
|
|
||||||
showOutputLabel: nodeTypeData.outputs.length === 1,
|
showOutputLabel: nodeTypeData.outputs.length === 1,
|
||||||
size: nodeTypeData.outputs.length >= 3 ? 'small' : 'medium',
|
size: nodeTypeData.outputs.length >= 3 ? 'small' : 'medium',
|
||||||
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
|
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
|
||||||
},
|
},
|
||||||
endpointHoverStyle: {
|
},
|
||||||
fill: getStyleTokenValue('--color-primary'),
|
source: true,
|
||||||
|
target: false,
|
||||||
|
enabled: !this.isReadOnly,
|
||||||
|
paintStyle: {
|
||||||
|
outlineStroke: 'none',
|
||||||
|
},
|
||||||
|
hoverPaintStyle: {
|
||||||
outlineStroke: 'none',
|
outlineStroke: 'none',
|
||||||
hover: true, // hack to distinguish hover state
|
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
nodeId: this.nodeId,
|
nodeId: this.nodeId,
|
||||||
|
@ -222,10 +231,12 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
},
|
},
|
||||||
cssClass: 'plus-draggable-endpoint',
|
cssClass: 'plus-draggable-endpoint',
|
||||||
dragAllowedWhenFull: false,
|
dragAllowedWhenFull: false,
|
||||||
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
|
|
||||||
};
|
};
|
||||||
|
const plusEndpoint = this.instance.addEndpoint(
|
||||||
|
this.$refs[this.data.name] as Element,
|
||||||
|
plusEndpointData,
|
||||||
|
);
|
||||||
|
|
||||||
const plusEndpoint = this.instance.addEndpoint(this.nodeId, plusEndpointData);
|
|
||||||
if (!Array.isArray(plusEndpoint)) {
|
if (!Array.isArray(plusEndpoint)) {
|
||||||
plusEndpoint.__meta = {
|
plusEndpoint.__meta = {
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
|
@ -237,105 +248,12 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
__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.nodeId, {
|
|
||||||
grid: [NodeViewUtils.GRID_SIZE, NodeViewUtils.GRID_SIZE],
|
|
||||||
start: (params: { e: MouseEvent }) => {
|
|
||||||
if (this.isReadOnly === true) {
|
|
||||||
// Do not allow to move nodes in readOnly mode
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
this.dragging = true;
|
|
||||||
|
|
||||||
const isSelected = this.uiStore.isNodeSelected(this.data.name);
|
|
||||||
const nodeName = this.data.name;
|
|
||||||
if (this.data.type === STICKY_NODE_TYPE && !isSelected) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$emit('nodeSelected', nodeName, false, true);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.e && !isSelected) {
|
|
||||||
// Only the node which gets dragged directly gets an event, for all others it is
|
|
||||||
// undefined. So check if the currently dragged node is selected and if not clear
|
|
||||||
// the drag-selection.
|
|
||||||
this.instance.clearDragSelection();
|
|
||||||
this.uiStore.resetSelectedNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uiStore.addActiveAction('dragActive');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
stop: (params: { e: MouseEvent }) => {
|
|
||||||
// @ts-ignore
|
|
||||||
this.dragging = false;
|
|
||||||
if (this.uiStore.isActionActive('dragActive')) {
|
|
||||||
const moveNodes = this.uiStore.getSelectedNodes.slice();
|
|
||||||
const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name);
|
|
||||||
if (!selectedNodeNames.includes(this.data.name)) {
|
|
||||||
// If the current node is not in selected add it to the nodes which
|
|
||||||
// got moved manually
|
|
||||||
moveNodes.push(this.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moveNodes.length > 1) {
|
|
||||||
this.historyStore.startRecordingUndo();
|
|
||||||
}
|
|
||||||
// This does for some reason just get called once for the node that got clicked
|
|
||||||
// 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 newNodePosition: XYPosition;
|
|
||||||
moveNodes.forEach((node: INodeUi) => {
|
|
||||||
const element = document.getElementById(node.id);
|
|
||||||
if (element === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newNodePosition = [
|
|
||||||
parseInt(element.style.left!.slice(0, -2), 10),
|
|
||||||
parseInt(element.style.top!.slice(0, -2), 10),
|
|
||||||
];
|
|
||||||
|
|
||||||
const updateInformation = {
|
|
||||||
name: node.name,
|
|
||||||
properties: {
|
|
||||||
// @ts-ignore, draggable does not have definitions
|
|
||||||
position: newNodePosition,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const oldPosition = node.position;
|
|
||||||
if (oldPosition[0] !== newNodePosition[0] || oldPosition[1] !== newNodePosition[1]) {
|
|
||||||
this.historyStore.pushCommandToUndo(
|
|
||||||
new MoveNodeCommand(node.name, oldPosition, newNodePosition, this),
|
|
||||||
);
|
|
||||||
this.workflowsStore.updateNodeProperties(updateInformation);
|
|
||||||
this.$emit('moved', node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (moveNodes.length > 1) {
|
|
||||||
this.historyStore.stopRecordingUndo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
__addNode(node: INodeUi) {
|
__addNode(node: INodeUi) {
|
||||||
let nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
const nodeTypeData = (this.nodeTypesStore.getNodeType(node.type, node.typeVersion) ??
|
||||||
if (!nodeTypeData) {
|
this.nodeTypesStore.getNodeType(NO_OP_NODE_TYPE)) as INodeTypeDescription;
|
||||||
// If node type is not know use by default the base.noOp data to display it
|
|
||||||
nodeTypeData = this.nodeTypesStore.getNodeType(NO_OP_NODE_TYPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.__addInputEndpoints(node, nodeTypeData);
|
this.__addInputEndpoints(node, nodeTypeData);
|
||||||
this.__addOutputEndpoints(node, nodeTypeData);
|
this.__addOutputEndpoints(node, nodeTypeData);
|
||||||
this.__makeInstanceDraggable(node);
|
|
||||||
},
|
},
|
||||||
touchEnd(e: MouseEvent) {
|
touchEnd(e: MouseEvent) {
|
||||||
if (this.isTouchDevice) {
|
if (this.isTouchDevice) {
|
||||||
|
|
|
@ -505,7 +505,7 @@ export const nodeHelpers = mixins(restApi).extend({
|
||||||
this.updateNodeCredentialIssues(node);
|
this.updateNodeCredentialIssues(node);
|
||||||
if (trackHistory) {
|
if (trackHistory) {
|
||||||
this.historyStore.pushCommandToUndo(
|
this.historyStore.pushCommandToUndo(
|
||||||
new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true, this),
|
new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,17 +21,16 @@ export enum COMMANDS {
|
||||||
// this timeout in between canvas actions
|
// this timeout in between canvas actions
|
||||||
// (0 is usually enough but leaving this just in case)
|
// (0 is usually enough but leaving this just in case)
|
||||||
const CANVAS_ACTION_TIMEOUT = 10;
|
const CANVAS_ACTION_TIMEOUT = 10;
|
||||||
|
export const historyBus = new Vue();
|
||||||
|
|
||||||
export abstract class Undoable {}
|
export abstract class Undoable {}
|
||||||
|
|
||||||
export abstract class Command extends Undoable {
|
export abstract class Command extends Undoable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
eventBus: Vue;
|
|
||||||
|
|
||||||
constructor(name: string, eventBus: Vue) {
|
constructor(name: string) {
|
||||||
super();
|
super();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.eventBus = eventBus;
|
|
||||||
}
|
}
|
||||||
abstract getReverseCommand(): Command;
|
abstract getReverseCommand(): Command;
|
||||||
abstract isEqualTo(anotherCommand: Command): boolean;
|
abstract isEqualTo(anotherCommand: Command): boolean;
|
||||||
|
@ -52,15 +51,15 @@ export class MoveNodeCommand extends Command {
|
||||||
oldPosition: XYPosition;
|
oldPosition: XYPosition;
|
||||||
newPosition: XYPosition;
|
newPosition: XYPosition;
|
||||||
|
|
||||||
constructor(nodeName: string, oldPosition: XYPosition, newPosition: XYPosition, eventBus: Vue) {
|
constructor(nodeName: string, oldPosition: XYPosition, newPosition: XYPosition) {
|
||||||
super(COMMANDS.MOVE_NODE, eventBus);
|
super(COMMANDS.MOVE_NODE);
|
||||||
this.nodeName = nodeName;
|
this.nodeName = nodeName;
|
||||||
this.newPosition = newPosition;
|
this.newPosition = newPosition;
|
||||||
this.oldPosition = oldPosition;
|
this.oldPosition = oldPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReverseCommand(): Command {
|
getReverseCommand(): Command {
|
||||||
return new MoveNodeCommand(this.nodeName, this.newPosition, this.oldPosition, this.eventBus);
|
return new MoveNodeCommand(this.nodeName, this.newPosition, this.oldPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEqualTo(anotherCommand: Command): boolean {
|
isEqualTo(anotherCommand: Command): boolean {
|
||||||
|
@ -76,7 +75,7 @@ export class MoveNodeCommand extends Command {
|
||||||
|
|
||||||
async revert(): Promise<void> {
|
async revert(): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.eventBus.$root.$emit('nodeMove', {
|
historyBus.$emit('nodeMove', {
|
||||||
nodeName: this.nodeName,
|
nodeName: this.nodeName,
|
||||||
position: this.oldPosition,
|
position: this.oldPosition,
|
||||||
});
|
});
|
||||||
|
@ -88,13 +87,13 @@ export class MoveNodeCommand extends Command {
|
||||||
export class AddNodeCommand extends Command {
|
export class AddNodeCommand extends Command {
|
||||||
node: INodeUi;
|
node: INodeUi;
|
||||||
|
|
||||||
constructor(node: INodeUi, eventBus: Vue) {
|
constructor(node: INodeUi) {
|
||||||
super(COMMANDS.ADD_NODE, eventBus);
|
super(COMMANDS.ADD_NODE);
|
||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReverseCommand(): Command {
|
getReverseCommand(): Command {
|
||||||
return new RemoveNodeCommand(this.node, this.eventBus);
|
return new RemoveNodeCommand(this.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEqualTo(anotherCommand: Command): boolean {
|
isEqualTo(anotherCommand: Command): boolean {
|
||||||
|
@ -103,7 +102,7 @@ export class AddNodeCommand extends Command {
|
||||||
|
|
||||||
async revert(): Promise<void> {
|
async revert(): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.eventBus.$root.$emit('revertAddNode', { node: this.node });
|
historyBus.$emit('revertAddNode', { node: this.node });
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -112,13 +111,13 @@ export class AddNodeCommand extends Command {
|
||||||
export class RemoveNodeCommand extends Command {
|
export class RemoveNodeCommand extends Command {
|
||||||
node: INodeUi;
|
node: INodeUi;
|
||||||
|
|
||||||
constructor(node: INodeUi, eventBus: Vue) {
|
constructor(node: INodeUi) {
|
||||||
super(COMMANDS.REMOVE_NODE, eventBus);
|
super(COMMANDS.REMOVE_NODE);
|
||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReverseCommand(): Command {
|
getReverseCommand(): Command {
|
||||||
return new AddNodeCommand(this.node, this.eventBus);
|
return new AddNodeCommand(this.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEqualTo(anotherCommand: Command): boolean {
|
isEqualTo(anotherCommand: Command): boolean {
|
||||||
|
@ -127,7 +126,7 @@ export class RemoveNodeCommand extends Command {
|
||||||
|
|
||||||
async revert(): Promise<void> {
|
async revert(): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.eventBus.$root.$emit('revertRemoveNode', { node: this.node });
|
historyBus.$emit('revertRemoveNode', { node: this.node });
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -136,13 +135,13 @@ export class RemoveNodeCommand extends Command {
|
||||||
export class AddConnectionCommand extends Command {
|
export class AddConnectionCommand extends Command {
|
||||||
connectionData: [IConnection, IConnection];
|
connectionData: [IConnection, IConnection];
|
||||||
|
|
||||||
constructor(connectionData: [IConnection, IConnection], eventBus: Vue) {
|
constructor(connectionData: [IConnection, IConnection]) {
|
||||||
super(COMMANDS.ADD_CONNECTION, eventBus);
|
super(COMMANDS.ADD_CONNECTION);
|
||||||
this.connectionData = connectionData;
|
this.connectionData = connectionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReverseCommand(): Command {
|
getReverseCommand(): Command {
|
||||||
return new RemoveConnectionCommand(this.connectionData, this.eventBus);
|
return new RemoveConnectionCommand(this.connectionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEqualTo(anotherCommand: Command): boolean {
|
isEqualTo(anotherCommand: Command): boolean {
|
||||||
|
@ -157,7 +156,7 @@ export class AddConnectionCommand extends Command {
|
||||||
|
|
||||||
async revert(): Promise<void> {
|
async revert(): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.eventBus.$root.$emit('revertAddConnection', { connection: this.connectionData });
|
historyBus.$emit('revertAddConnection', { connection: this.connectionData });
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -166,13 +165,13 @@ export class AddConnectionCommand extends Command {
|
||||||
export class RemoveConnectionCommand extends Command {
|
export class RemoveConnectionCommand extends Command {
|
||||||
connectionData: [IConnection, IConnection];
|
connectionData: [IConnection, IConnection];
|
||||||
|
|
||||||
constructor(connectionData: [IConnection, IConnection], eventBus: Vue) {
|
constructor(connectionData: [IConnection, IConnection]) {
|
||||||
super(COMMANDS.REMOVE_CONNECTION, eventBus);
|
super(COMMANDS.REMOVE_CONNECTION);
|
||||||
this.connectionData = connectionData;
|
this.connectionData = connectionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReverseCommand(): Command {
|
getReverseCommand(): Command {
|
||||||
return new AddConnectionCommand(this.connectionData, this.eventBus);
|
return new AddConnectionCommand(this.connectionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEqualTo(anotherCommand: Command): boolean {
|
isEqualTo(anotherCommand: Command): boolean {
|
||||||
|
@ -188,7 +187,7 @@ export class RemoveConnectionCommand extends Command {
|
||||||
async revert(): Promise<void> {
|
async revert(): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.eventBus.$root.$emit('revertRemoveConnection', { connection: this.connectionData });
|
historyBus.$emit('revertRemoveConnection', { connection: this.connectionData });
|
||||||
resolve();
|
resolve();
|
||||||
}, CANVAS_ACTION_TIMEOUT);
|
}, CANVAS_ACTION_TIMEOUT);
|
||||||
});
|
});
|
||||||
|
@ -200,15 +199,15 @@ export class EnableNodeToggleCommand extends Command {
|
||||||
oldState: boolean;
|
oldState: boolean;
|
||||||
newState: boolean;
|
newState: boolean;
|
||||||
|
|
||||||
constructor(nodeName: string, oldState: boolean, newState: boolean, eventBus: Vue) {
|
constructor(nodeName: string, oldState: boolean, newState: boolean) {
|
||||||
super(COMMANDS.ENABLE_NODE_TOGGLE, eventBus);
|
super(COMMANDS.ENABLE_NODE_TOGGLE);
|
||||||
this.nodeName = nodeName;
|
this.nodeName = nodeName;
|
||||||
this.newState = newState;
|
this.newState = newState;
|
||||||
this.oldState = oldState;
|
this.oldState = oldState;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReverseCommand(): Command {
|
getReverseCommand(): Command {
|
||||||
return new EnableNodeToggleCommand(this.nodeName, this.newState, this.oldState, this.eventBus);
|
return new EnableNodeToggleCommand(this.nodeName, this.newState, this.oldState);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEqualTo(anotherCommand: Command): boolean {
|
isEqualTo(anotherCommand: Command): boolean {
|
||||||
|
@ -219,7 +218,7 @@ export class EnableNodeToggleCommand extends Command {
|
||||||
|
|
||||||
async revert(): Promise<void> {
|
async revert(): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.eventBus.$root.$emit('enableNodeToggle', {
|
historyBus.$emit('enableNodeToggle', {
|
||||||
nodeName: this.nodeName,
|
nodeName: this.nodeName,
|
||||||
isDisabled: this.oldState,
|
isDisabled: this.oldState,
|
||||||
});
|
});
|
||||||
|
@ -232,14 +231,14 @@ export class RenameNodeCommand extends Command {
|
||||||
currentName: string;
|
currentName: string;
|
||||||
newName: string;
|
newName: string;
|
||||||
|
|
||||||
constructor(currentName: string, newName: string, eventBus: Vue) {
|
constructor(currentName: string, newName: string) {
|
||||||
super(COMMANDS.RENAME_NODE, eventBus);
|
super(COMMANDS.RENAME_NODE);
|
||||||
this.currentName = currentName;
|
this.currentName = currentName;
|
||||||
this.newName = newName;
|
this.newName = newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReverseCommand(): Command {
|
getReverseCommand(): Command {
|
||||||
return new RenameNodeCommand(this.newName, this.currentName, this.eventBus);
|
return new RenameNodeCommand(this.newName, this.currentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEqualTo(anotherCommand: Command): boolean {
|
isEqualTo(anotherCommand: Command): boolean {
|
||||||
|
@ -252,7 +251,7 @@ export class RenameNodeCommand extends Command {
|
||||||
|
|
||||||
async revert(): Promise<void> {
|
async revert(): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.eventBus.$root.$emit('revertRenameNode', {
|
historyBus.$emit('revertRenameNode', {
|
||||||
currentName: this.currentName,
|
currentName: this.currentName,
|
||||||
newName: this.newName,
|
newName: this.newName,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,896 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 = window,
|
|
||||||
_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 = -1; // vertical direction of loop, always below source
|
|
||||||
|
|
||||||
var midx = paintInfo.startStubX + (paintInfo.endStubX - paintInfo.startStubX) * midpoint,
|
|
||||||
midy;
|
|
||||||
|
|
||||||
if (diffY >= 0 || 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);
|
|
|
@ -1,518 +0,0 @@
|
||||||
/**
|
|
||||||
* Custom Plus Endpoint
|
|
||||||
* Based on jsplumb Blank Endpoint type
|
|
||||||
*
|
|
||||||
* Source GitHub repository:
|
|
||||||
* https://github.com/jsplumb/jsplumb
|
|
||||||
*
|
|
||||||
* Source files:
|
|
||||||
* https://github.com/jsplumb/jsplumb/blob/fb5fce52794fa52306825bdaa62bf3855cdfd7e0/src/defaults.js#L1230
|
|
||||||
*
|
|
||||||
* 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 = window,
|
|
||||||
_jp = root.jsPlumb,
|
|
||||||
_ju = root.jsPlumbUtil;
|
|
||||||
|
|
||||||
var DOMElementEndpoint = function (params) {
|
|
||||||
_jp.jsPlumbUIComponent.apply(this, arguments);
|
|
||||||
this._jsPlumb.displayElements = [];
|
|
||||||
};
|
|
||||||
_ju.extend(DOMElementEndpoint, _jp.jsPlumbUIComponent, {
|
|
||||||
getDisplayElements: function () {
|
|
||||||
return this._jsPlumb.displayElements;
|
|
||||||
},
|
|
||||||
appendDisplayElement: function (el) {
|
|
||||||
this._jsPlumb.displayElements.push(el);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Class: Endpoints.N8nPlus
|
|
||||||
*/
|
|
||||||
_jp.Endpoints.N8nPlus = function (params) {
|
|
||||||
const _super = _jp.Endpoints.AbstractEndpoint.apply(this, arguments);
|
|
||||||
this.type = 'N8nPlus';
|
|
||||||
this.label = '';
|
|
||||||
this.labelOffset = 0;
|
|
||||||
this.size = 'medium';
|
|
||||||
this.showOutputLabel = true;
|
|
||||||
|
|
||||||
const boxSize = {
|
|
||||||
medium: 24,
|
|
||||||
small: 18,
|
|
||||||
};
|
|
||||||
const stalkLength = 40;
|
|
||||||
|
|
||||||
DOMElementEndpoint.apply(this, arguments);
|
|
||||||
|
|
||||||
var clazz = params.cssClass ? ' ' + params.cssClass : '';
|
|
||||||
|
|
||||||
this.canvas = _jp.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
display: 'block',
|
|
||||||
background: 'transparent',
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
this._jsPlumb.instance.endpointClass + clazz + ' plus-endpoint',
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canvas.innerHTML = `
|
|
||||||
<div class="plus-stalk">
|
|
||||||
<div class="connection-run-items-label">
|
|
||||||
<span class="floating"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="plus-container">
|
|
||||||
<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">
|
|
||||||
<path 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>
|
|
||||||
<div class="drop-hover-message">
|
|
||||||
Click to add node</br>
|
|
||||||
or drag to connect
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.canvas.addEventListener('click', (e) => {
|
|
||||||
this._jsPlumb.instance.fire('plusEndpointClick', params.endpoint, e);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._jsPlumb.instance.appendElement(this.canvas);
|
|
||||||
|
|
||||||
const container = this.canvas.querySelector('.plus-container');
|
|
||||||
const message = container.querySelector('.drop-hover-message');
|
|
||||||
const plusStalk = this.canvas.querySelector('.plus-stalk');
|
|
||||||
const successOutput = this.canvas.querySelector('.plus-stalk span');
|
|
||||||
|
|
||||||
this.setSuccessOutput = (label) => {
|
|
||||||
this.canvas.classList.add('success');
|
|
||||||
if (this.showOutputLabel) {
|
|
||||||
successOutput.textContent = label;
|
|
||||||
this.label = label;
|
|
||||||
this.labelOffset = successOutput.offsetWidth;
|
|
||||||
|
|
||||||
plusStalk.style.width = `${stalkLength + this.labelOffset}px`;
|
|
||||||
if (this._jsPlumb && this._jsPlumb.instance && !this._jsPlumb.instance.isSuspendDrawing()) {
|
|
||||||
params.endpoint.repaint(); // force rerender to move plus hoverable/draggable space
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.clearSuccessOutput = () => {
|
|
||||||
this.canvas.classList.remove('success');
|
|
||||||
successOutput.textContent = '';
|
|
||||||
this.label = '';
|
|
||||||
this.labelOffset = 0;
|
|
||||||
plusStalk.style.width = `${stalkLength}px`;
|
|
||||||
params.endpoint.repaint();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDragging = () => {
|
|
||||||
const endpoint = params.endpoint;
|
|
||||||
const plusConnections = endpoint.connections;
|
|
||||||
|
|
||||||
if (plusConnections.length) {
|
|
||||||
return !!plusConnections.find(
|
|
||||||
(conn) => conn && conn.targetId && conn.targetId.startsWith('jsPlumb'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasEndpointConnections = () => {
|
|
||||||
const endpoint = params.endpoint;
|
|
||||||
const plusConnections = endpoint.connections;
|
|
||||||
|
|
||||||
if (plusConnections.length >= 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allConnections = this._jsPlumb.instance.getConnections({
|
|
||||||
source: endpoint.elementId,
|
|
||||||
}); // includes connections from other output endpoints like dot
|
|
||||||
|
|
||||||
return !!allConnections.find((connection) => {
|
|
||||||
if (
|
|
||||||
!connection ||
|
|
||||||
!connection.endpoints ||
|
|
||||||
!connection.endpoints.length ||
|
|
||||||
!connection.endpoints[0]
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceEndpoint = connection.endpoints[0];
|
|
||||||
return sourceEndpoint === endpoint || sourceEndpoint.getUuid() === endpoint.getUuid();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.paint = function (style, anchor) {
|
|
||||||
if (hasEndpointConnections()) {
|
|
||||||
this.canvas.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
this.canvas.classList.remove('hidden');
|
|
||||||
container.style.color = style.fill;
|
|
||||||
container.style['border-color'] = style.fill;
|
|
||||||
message.style.display = style.hover ? 'inline' : 'none';
|
|
||||||
}
|
|
||||||
_ju.sizeElement(this.canvas, this.x, this.y, this.w, this.h);
|
|
||||||
};
|
|
||||||
|
|
||||||
this._compute = (anchorPoint, orientation, endpointStyle, connectorPaintStyle) => {
|
|
||||||
this.size = endpointStyle.size || this.size;
|
|
||||||
this.showOutputLabel = !!endpointStyle.showOutputLabel;
|
|
||||||
|
|
||||||
if (this.hoverMessage !== endpointStyle.hoverMessage) {
|
|
||||||
this.hoverMessage = endpointStyle.hoverMessage;
|
|
||||||
message.innerHTML = endpointStyle.hoverMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.size !== 'medium') {
|
|
||||||
container.classList.add(this.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.label && !this.labelOffset) {
|
|
||||||
// if label is hidden, offset is 0 so recalculate
|
|
||||||
this.setSuccessOutput(this.label);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const defaultPosition = [
|
|
||||||
anchorPoint[0] + stalkLength + this.labelOffset,
|
|
||||||
anchorPoint[1] - boxSize[this.size] / 2,
|
|
||||||
boxSize[this.size],
|
|
||||||
boxSize[this.size],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isDragging()) {
|
|
||||||
return defaultPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasEndpointConnections()) {
|
|
||||||
return [0, 0, 0, 0]; // remove hoverable box from view
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultPosition;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
_ju.extend(_jp.Endpoints.N8nPlus, [_jp.Endpoints.AbstractEndpoint, DOMElementEndpoint], {
|
|
||||||
cleanup: function () {
|
|
||||||
if (this.canvas && this.canvas.parentNode) {
|
|
||||||
this.canvas.parentNode.removeChild(this.canvas);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
_jp.Endpoints.svg.N8nPlus = _jp.Endpoints.N8nPlus;
|
|
||||||
})();
|
|
586
packages/editor-ui/src/plugins/connectors/N8nCustomConnector.ts
Normal file
586
packages/editor-ui/src/plugins/connectors/N8nCustomConnector.ts
Normal file
|
@ -0,0 +1,586 @@
|
||||||
|
import { PointXY, log, extend, quadrant } from '@jsplumb/util';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Connection,
|
||||||
|
ArcSegment,
|
||||||
|
AbstractConnector,
|
||||||
|
ConnectorComputeParams,
|
||||||
|
PaintGeometry,
|
||||||
|
Endpoint,
|
||||||
|
StraightSegment,
|
||||||
|
Orientation,
|
||||||
|
} from '@jsplumb/core';
|
||||||
|
import { AnchorPlacement, ConnectorOptions, Geometry, PaintAxis } from '@jsplumb/common';
|
||||||
|
import { BezierSegment } from '@jsplumb/connector-bezier';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
|
import { deepCopy } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export interface N8nConnectorOptions extends ConnectorOptions {}
|
||||||
|
interface N8nConnectorPaintGeometry extends PaintGeometry {
|
||||||
|
sourceEndpoint: Endpoint;
|
||||||
|
targetEndpoint: Endpoint;
|
||||||
|
sourcePos: AnchorPlacement;
|
||||||
|
targetPos: AnchorPlacement;
|
||||||
|
targetGap: number;
|
||||||
|
lw: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlowchartSegment = [number, number, number, number, string];
|
||||||
|
type StubPositions = [number, number, number, number];
|
||||||
|
|
||||||
|
const lineCalculators = {
|
||||||
|
opposite(
|
||||||
|
paintInfo: PaintGeometry,
|
||||||
|
{
|
||||||
|
axis,
|
||||||
|
startStub,
|
||||||
|
endStub,
|
||||||
|
idx,
|
||||||
|
midx,
|
||||||
|
midy,
|
||||||
|
}: {
|
||||||
|
axis: 'x' | 'y';
|
||||||
|
startStub: number;
|
||||||
|
endStub: number;
|
||||||
|
idx: number;
|
||||||
|
midx: number;
|
||||||
|
midy: number;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const pi = paintInfo,
|
||||||
|
comparator = pi[('is' + axis.toUpperCase() + 'GreaterThanStubTimes2') as keyof PaintGeometry];
|
||||||
|
|
||||||
|
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(
|
||||||
|
paintInfo: PaintGeometry,
|
||||||
|
{ axis, alwaysRespectStubs }: { axis: 'x' | 'y'; alwaysRespectStubs: boolean },
|
||||||
|
): StubPositions {
|
||||||
|
const pi = paintInfo,
|
||||||
|
idx = axis === 'x' ? 0 : 1,
|
||||||
|
areInProximity = {
|
||||||
|
x() {
|
||||||
|
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() {
|
||||||
|
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,
|
||||||
|
] as StubPositions,
|
||||||
|
y: [
|
||||||
|
paintInfo.startStubX,
|
||||||
|
(paintInfo.sy + paintInfo.ty) / 2,
|
||||||
|
paintInfo.endStubX,
|
||||||
|
(paintInfo.sy + paintInfo.ty) / 2,
|
||||||
|
] as StubPositions,
|
||||||
|
}[axis];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
paintInfo.startStubX,
|
||||||
|
paintInfo.startStubY,
|
||||||
|
paintInfo.endStubX,
|
||||||
|
paintInfo.endStubY,
|
||||||
|
] as StubPositions;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class N8nConnector extends AbstractConnector {
|
||||||
|
static type = 'N8nConnector';
|
||||||
|
type = N8nConnector.type;
|
||||||
|
|
||||||
|
majorAnchor: number;
|
||||||
|
minorAnchor: number;
|
||||||
|
midpoint: number;
|
||||||
|
alwaysRespectStubs: boolean;
|
||||||
|
loopbackVerticalLength: number;
|
||||||
|
lastx: number | null;
|
||||||
|
lasty: number | null;
|
||||||
|
cornerRadius: number;
|
||||||
|
loopbackMinimum: number;
|
||||||
|
curvinessCoefficient: number;
|
||||||
|
zBezierOffset: number;
|
||||||
|
targetGap: number;
|
||||||
|
overrideTargetEndpoint: Endpoint | null;
|
||||||
|
getEndpointOffset: Function | null;
|
||||||
|
private internalSegments: FlowchartSegment[] = [];
|
||||||
|
|
||||||
|
constructor(public connection: Connection, params: N8nConnectorOptions) {
|
||||||
|
super(connection, params);
|
||||||
|
params = params || {};
|
||||||
|
this.minorAnchor = 0; // seems to be angle at which connector leaves endpoint
|
||||||
|
this.majorAnchor = 0; // translates to curviness of bezier curve
|
||||||
|
this.stub = params.stub || 0;
|
||||||
|
this.midpoint = 0.5;
|
||||||
|
this.alwaysRespectStubs = params.alwaysRespectStubs === true;
|
||||||
|
this.loopbackVerticalLength = params.loopbackVerticalLength || 0;
|
||||||
|
this.lastx = null;
|
||||||
|
this.lasty = null;
|
||||||
|
this.cornerRadius = params.cornerRadius !== null ? params.cornerRadius : 0;
|
||||||
|
this.loopbackMinimum = params.loopbackMinimum || 100;
|
||||||
|
this.curvinessCoefficient = 0.4;
|
||||||
|
this.zBezierOffset = 40;
|
||||||
|
this.targetGap = params.targetGap || 0;
|
||||||
|
this.stub = params.stub || 0;
|
||||||
|
this.overrideTargetEndpoint = params.overrideTargetEndpoint || null;
|
||||||
|
this.getEndpointOffset = params.getEndpointOffset || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultStubs(): [number, number] {
|
||||||
|
return [30, 30];
|
||||||
|
}
|
||||||
|
|
||||||
|
sgn(n: number) {
|
||||||
|
return n < 0 ? -1 : n === 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFlowchartSegmentDirections(segment: FlowchartSegment): [number, number] {
|
||||||
|
return [this.sgn(segment[2] - segment[0]), this.sgn(segment[3] - segment[1])];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSegmentLength(s: FlowchartSegment) {
|
||||||
|
return Math.sqrt(Math.pow(s[0] - s[2], 2) + Math.pow(s[1] - s[3], 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _findControlPoint(
|
||||||
|
point: PointXY,
|
||||||
|
sourceAnchorPosition: AnchorPlacement,
|
||||||
|
targetAnchorPosition: AnchorPlacement,
|
||||||
|
soo: [number, number],
|
||||||
|
too: [number, number],
|
||||||
|
): PointXY {
|
||||||
|
// 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)
|
||||||
|
const perpendicular = soo[0] !== too[0] || soo[1] === too[1],
|
||||||
|
p: PointXY = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!perpendicular) {
|
||||||
|
if (soo[0] === 0) {
|
||||||
|
p.x =
|
||||||
|
sourceAnchorPosition.curX < targetAnchorPosition.curX
|
||||||
|
? point.x + this.minorAnchor
|
||||||
|
: point.x - this.minorAnchor;
|
||||||
|
} else {
|
||||||
|
p.x = point.x - this.majorAnchor * soo[0];
|
||||||
|
}
|
||||||
|
if (soo[1] === 0) {
|
||||||
|
p.y =
|
||||||
|
sourceAnchorPosition.curY < targetAnchorPosition.curY
|
||||||
|
? point.y + this.minorAnchor
|
||||||
|
: point.y - this.minorAnchor;
|
||||||
|
} else {
|
||||||
|
p.y = point.y + this.majorAnchor * too[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (too[0] === 0) {
|
||||||
|
p.x =
|
||||||
|
targetAnchorPosition.curX < sourceAnchorPosition.curX
|
||||||
|
? point.x + this.minorAnchor
|
||||||
|
: point.x - this.minorAnchor;
|
||||||
|
} else {
|
||||||
|
p.x = point.x + this.majorAnchor * too[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (too[1] === 0) {
|
||||||
|
p.y =
|
||||||
|
targetAnchorPosition.curY < sourceAnchorPosition.curY
|
||||||
|
? point.y + this.minorAnchor
|
||||||
|
: point.y - this.minorAnchor;
|
||||||
|
} else {
|
||||||
|
p.y = point.y + this.majorAnchor * soo[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFlowchartSegments(paintInfo: N8nConnectorPaintGeometry) {
|
||||||
|
let current: FlowchartSegment = null;
|
||||||
|
let next: FlowchartSegment = null;
|
||||||
|
let currentDirection: [number, number];
|
||||||
|
let nextDirection: [number, number];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.internalSegments.length - 1; i++) {
|
||||||
|
current = current || (deepCopy(this.internalSegments[i]) as FlowchartSegment);
|
||||||
|
next = deepCopy(this.internalSegments[i + 1]) as FlowchartSegment;
|
||||||
|
|
||||||
|
currentDirection = this.getFlowchartSegmentDirections(current);
|
||||||
|
nextDirection = this.getFlowchartSegmentDirections(next);
|
||||||
|
|
||||||
|
if (this.cornerRadius > 0 && current[4] !== next[4]) {
|
||||||
|
const minSegLength = Math.min(this.getSegmentLength(current), this.getSegmentLength(next));
|
||||||
|
const radiusToUse = Math.min(this.cornerRadius, minSegLength / 2);
|
||||||
|
|
||||||
|
current[2] -= currentDirection[0] * radiusToUse;
|
||||||
|
current[3] -= currentDirection[1] * radiusToUse;
|
||||||
|
next[0] += nextDirection[0] * radiusToUse;
|
||||||
|
next[1] += nextDirection[1] * radiusToUse;
|
||||||
|
|
||||||
|
const 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];
|
||||||
|
|
||||||
|
this._addSegment(StraightSegment, {
|
||||||
|
x1: current[0],
|
||||||
|
y1: current[1],
|
||||||
|
x2: current[2],
|
||||||
|
y2: current[3],
|
||||||
|
});
|
||||||
|
|
||||||
|
this._addSegment(ArcSegment, {
|
||||||
|
r: radiusToUse,
|
||||||
|
x1: current[2],
|
||||||
|
y1: current[3],
|
||||||
|
x2: next[0],
|
||||||
|
y2: next[1],
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
ac,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// dx + dy are used to adjust for line width.
|
||||||
|
const 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);
|
||||||
|
|
||||||
|
this._addSegment(StraightSegment, {
|
||||||
|
x1: current[0] - dx,
|
||||||
|
y1: current[1] - dy,
|
||||||
|
x2: current[2] + dx,
|
||||||
|
y2: current[3] + dy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
if (next !== null) {
|
||||||
|
// last segment
|
||||||
|
this._addSegment(StraightSegment, {
|
||||||
|
x1: next[0],
|
||||||
|
y1: next[1],
|
||||||
|
x2: next[2],
|
||||||
|
y2: next[3],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateStubSegment(paintInfo: PaintGeometry): StubPositions {
|
||||||
|
return stubCalculators['opposite'](paintInfo, {
|
||||||
|
axis: paintInfo.sourceAxis,
|
||||||
|
alwaysRespectStubs: this.alwaysRespectStubs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateLineSegment(paintInfo: PaintGeometry, stubs: StubPositions) {
|
||||||
|
const axis = paintInfo.sourceAxis;
|
||||||
|
const idx = paintInfo.sourceAxis === 'x' ? 0 : 1;
|
||||||
|
const startStub = stubs[idx];
|
||||||
|
const endStub = stubs[idx + 2];
|
||||||
|
|
||||||
|
const diffX = paintInfo.endStubX - paintInfo.startStubX;
|
||||||
|
const diffY = paintInfo.endStubY - paintInfo.startStubY;
|
||||||
|
const direction = -1; // vertical direction of loop, always below source
|
||||||
|
|
||||||
|
const midx = paintInfo.startStubX + (paintInfo.endStubX - paintInfo.startStubX) * this.midpoint;
|
||||||
|
let midy: number;
|
||||||
|
|
||||||
|
if (diffY >= 0 || diffX < -1 * this.loopbackMinimum) {
|
||||||
|
// loop backward behavior
|
||||||
|
midy = paintInfo.startStubY - (diffX < 0 ? direction * this.loopbackVerticalLength : 0);
|
||||||
|
} else {
|
||||||
|
// original flowchart behavior
|
||||||
|
midy = paintInfo.startStubY + (paintInfo.endStubY - paintInfo.startStubY) * this.midpoint;
|
||||||
|
}
|
||||||
|
return lineCalculators['opposite'](paintInfo, { axis, startStub, endStub, idx, midx, midy });
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPaintInfo(params: ConnectorComputeParams): N8nConnectorPaintGeometry {
|
||||||
|
let targetPos = params.targetPos;
|
||||||
|
let targetEndpoint: Endpoint = params.targetEndpoint;
|
||||||
|
if (this.overrideTargetEndpoint) {
|
||||||
|
targetPos = this.overrideTargetEndpoint._anchor.computedPosition as AnchorPlacement;
|
||||||
|
targetEndpoint = this.overrideTargetEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stub = this.stub || 0;
|
||||||
|
const sourceGap = 0;
|
||||||
|
const sourceStub = isArray(this.stub) ? this.stub[0] : this.stub;
|
||||||
|
const targetStub = isArray(this.stub) ? this.stub[1] : this.stub;
|
||||||
|
const segment = quadrant(params.sourcePos, targetPos);
|
||||||
|
const swapX = targetPos.curX < params.sourcePos.curX;
|
||||||
|
const swapY = targetPos.curY < params.sourcePos.curY;
|
||||||
|
const lw = params.strokeWidth || 1;
|
||||||
|
const x = swapX ? targetPos.curX : params.sourcePos.curX;
|
||||||
|
const y = swapY ? targetPos.curY : params.sourcePos.curY;
|
||||||
|
const w = Math.abs(targetPos.curX - params.sourcePos.curX);
|
||||||
|
const h = Math.abs(targetPos.curY - params.sourcePos.curY);
|
||||||
|
let so: Orientation = [params.sourcePos.ox, params.sourcePos.oy];
|
||||||
|
let to: Orientation = [targetPos.ox, targetPos.oy];
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
const index = w > h ? 'curX' : 'curY';
|
||||||
|
const indexNum = w > h ? 0 : 1;
|
||||||
|
const oIndex = [1, 0][indexNum];
|
||||||
|
so = [];
|
||||||
|
to = [];
|
||||||
|
so[indexNum] = params.sourcePos[index] > targetPos[index] ? -1 : 1;
|
||||||
|
to[indexNum] = 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 ? this.targetGap * to[0] : w + this.targetGap * to[0],
|
||||||
|
ty = swapY ? this.targetGap * to[1] : h + this.targetGap * to[1],
|
||||||
|
oProduct = so[0] * to[0] + so[1] * to[1];
|
||||||
|
|
||||||
|
const sourceStubWithOffset =
|
||||||
|
sourceStub +
|
||||||
|
(this.getEndpointOffset && params.sourceEndpoint
|
||||||
|
? this.getEndpointOffset(params.sourceEndpoint)
|
||||||
|
: 0);
|
||||||
|
|
||||||
|
const targetStubWithOffset =
|
||||||
|
targetStub +
|
||||||
|
(this.getEndpointOffset && targetEndpoint ? this.getEndpointOffset(targetEndpoint) : 0);
|
||||||
|
|
||||||
|
// same as paintinfo generated by jsplumb AbstractConnector type
|
||||||
|
const result = {
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
lw,
|
||||||
|
xSpan: Math.abs(tx - sx),
|
||||||
|
ySpan: Math.abs(ty - sy),
|
||||||
|
mx: (sx + tx) / 2,
|
||||||
|
my: (sy + ty) / 2,
|
||||||
|
so,
|
||||||
|
to,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
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' as PaintAxis),
|
||||||
|
points: [x, y, w, h, sx, sy, tx, ty] as [
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
],
|
||||||
|
stubs: [sourceStubWithOffset, targetStubWithOffset] as [number, number],
|
||||||
|
anchorOrientation: 'opposite', // always opposite since our endpoints are always opposite (source orientation is left (1) and target orientation is right (-1))
|
||||||
|
|
||||||
|
/** custom keys added */
|
||||||
|
sourceEndpoint: params.sourceEndpoint,
|
||||||
|
targetEndpoint,
|
||||||
|
sourcePos: params.sourcePos,
|
||||||
|
targetPos,
|
||||||
|
targetGap: this.targetGap,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
_compute(originalPaintInfo: PaintGeometry, connParams: ConnectorComputeParams) {
|
||||||
|
const paintInfo = this._getPaintInfo(connParams);
|
||||||
|
// Set the type of key as key of paintInfo
|
||||||
|
// TODO: Check if this is the best way to do this
|
||||||
|
// Object.assign(originalPaintInfo, paintInfo);
|
||||||
|
Object.keys(paintInfo).forEach((key) => {
|
||||||
|
if (key === undefined) return;
|
||||||
|
// override so that bounding box is calculated correctly when target override is set
|
||||||
|
originalPaintInfo[key as keyof PaintGeometry] = paintInfo[key as keyof PaintGeometry];
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (paintInfo.tx < 0) {
|
||||||
|
this._computeFlowchart(paintInfo);
|
||||||
|
} else {
|
||||||
|
this._computeBezier(paintInfo);
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set target endpoint
|
||||||
|
* (to override default behavior tracking mouse when dragging mouse)
|
||||||
|
* @param {Endpoint} endpoint
|
||||||
|
*/
|
||||||
|
setTargetEndpoint(endpoint: Endpoint) {
|
||||||
|
this.overrideTargetEndpoint = endpoint;
|
||||||
|
}
|
||||||
|
resetTargetEndpoint() {
|
||||||
|
this.overrideTargetEndpoint = null;
|
||||||
|
}
|
||||||
|
_computeBezier(paintInfo: N8nConnectorPaintGeometry) {
|
||||||
|
const sp = paintInfo.sourcePos;
|
||||||
|
const tp = paintInfo.targetPos;
|
||||||
|
const _w = Math.abs(sp.curX - tp.curX) - this.targetGap;
|
||||||
|
const _h = Math.abs(sp.curY - tp.curY);
|
||||||
|
const _sx = sp.curX < tp.curX ? _w : 0;
|
||||||
|
const _sy = sp.curY < tp.curY ? _h : 0;
|
||||||
|
const _tx = sp.curX < tp.curX ? 0 : _w;
|
||||||
|
const _ty = sp.curY < tp.curY ? 0 : _h;
|
||||||
|
|
||||||
|
if (paintInfo.ySpan <= 20 || (paintInfo.ySpan <= 100 && paintInfo.xSpan <= 100)) {
|
||||||
|
this.majorAnchor = 0.1;
|
||||||
|
} else {
|
||||||
|
this.majorAnchor = paintInfo.xSpan * this.curvinessCoefficient + this.zBezierOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _CP = this._findControlPoint({ x: _sx, y: _sy }, sp, tp, paintInfo.so, paintInfo.to);
|
||||||
|
const _CP2 = this._findControlPoint({ x: _tx, y: _ty }, tp, sp, paintInfo.to, paintInfo.so);
|
||||||
|
|
||||||
|
const bezRes = {
|
||||||
|
x1: _sx,
|
||||||
|
y1: _sy,
|
||||||
|
x2: _tx,
|
||||||
|
y2: _ty,
|
||||||
|
cp1x: _CP.x,
|
||||||
|
cp1y: _CP.y,
|
||||||
|
cp2x: _CP2.x,
|
||||||
|
cp2y: _CP2.y,
|
||||||
|
};
|
||||||
|
this._addSegment(BezierSegment, bezRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFlowchartSegment(x: number, y: number, paintInfo: PaintGeometry) {
|
||||||
|
if (this.lastx === x && this.lasty === y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lx = this.lastx === null ? paintInfo.sx : this.lastx;
|
||||||
|
const ly = this.lasty === null ? paintInfo.sy : this.lasty;
|
||||||
|
const o = lx === x ? 'v' : 'h';
|
||||||
|
|
||||||
|
this.lastx = x;
|
||||||
|
this.lasty = y;
|
||||||
|
this.internalSegments.push([lx, ly, x, y, o]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeFlowchart(paintInfo: N8nConnectorPaintGeometry) {
|
||||||
|
this.segments = [];
|
||||||
|
this.lastx = null;
|
||||||
|
this.lasty = null;
|
||||||
|
|
||||||
|
this.internalSegments = [];
|
||||||
|
|
||||||
|
// calculate Stubs.
|
||||||
|
const stubs = this.calculateStubSegment(paintInfo);
|
||||||
|
|
||||||
|
// add the start stub segment. use stubs for loopback as it will look better, with the loop spaced
|
||||||
|
// away from the element.
|
||||||
|
this.addFlowchartSegment(stubs[0], stubs[1], paintInfo);
|
||||||
|
|
||||||
|
// compute the rest of the line
|
||||||
|
const p = this.calculateLineSegment(paintInfo, stubs);
|
||||||
|
if (p) {
|
||||||
|
for (let i = 0; i < p.length; i++) {
|
||||||
|
this.addFlowchartSegment(p[i][0], p[i][1], paintInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// line to end stub
|
||||||
|
this.addFlowchartSegment(stubs[2], stubs[3], paintInfo);
|
||||||
|
|
||||||
|
// end stub to end (common)
|
||||||
|
this.addFlowchartSegment(paintInfo.tx, paintInfo.ty, paintInfo);
|
||||||
|
|
||||||
|
// write out the segments.
|
||||||
|
this.writeFlowchartSegments(paintInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformGeometry(g: Geometry, dx: number, dy: number): Geometry {
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { registerEndpointRenderer, svg } from '@jsplumb/browser-ui';
|
||||||
|
import { N8nPlusEndpoint } from './N8nPlusEndpointType';
|
||||||
|
|
||||||
|
export const register = () => {
|
||||||
|
registerEndpointRenderer<N8nPlusEndpoint>(N8nPlusEndpoint.type, {
|
||||||
|
makeNode: (ep: N8nPlusEndpoint) => {
|
||||||
|
const group = svg.node('g');
|
||||||
|
const containerBorder = svg.node('rect', {
|
||||||
|
rx: 3,
|
||||||
|
'stroke-width': 2,
|
||||||
|
fillOpacity: 0,
|
||||||
|
height: ep.params.dimensions - 2,
|
||||||
|
width: ep.params.dimensions - 2,
|
||||||
|
y: 1,
|
||||||
|
x: 1,
|
||||||
|
});
|
||||||
|
const plusPath = svg.node('path', {
|
||||||
|
d: 'm16.40655,10.89837l-3.30491,0l0,-3.30491c0,-0.40555 -0.32889,-0.73443 -0.73443,-0.73443l-0.73443,0c-0.40554,0 -0.73442,0.32888 -0.73442,0.73443l0,3.30491l-3.30491,0c-0.40555,0 -0.73443,0.32888 -0.73443,0.73442l0,0.73443c0,0.40554 0.32888,0.73443 0.73443,0.73443l3.30491,0l0,3.30491c0,0.40554 0.32888,0.73442 0.73442,0.73442l0.73443,0c0.40554,0 0.73443,-0.32888 0.73443,-0.73442l0,-3.30491l3.30491,0c0.40554,0 0.73442,-0.32889 0.73442,-0.73443l0,-0.73443c0,-0.40554 -0.32888,-0.73442 -0.73442,-0.73442z',
|
||||||
|
});
|
||||||
|
if (ep.params.size !== 'medium') {
|
||||||
|
ep.addClass(ep.params.size);
|
||||||
|
}
|
||||||
|
group.appendChild(containerBorder);
|
||||||
|
group.appendChild(plusPath);
|
||||||
|
|
||||||
|
ep.setupOverlays();
|
||||||
|
ep.setVisible(false);
|
||||||
|
return group;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNode: (ep: N8nPlusEndpoint) => {
|
||||||
|
const ifNoConnections = ep.getConnections().length === 0;
|
||||||
|
|
||||||
|
ep.setIsVisible(ifNoConnections);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
183
packages/editor-ui/src/plugins/endpoints/N8nPlusEndpointType.ts
Normal file
183
packages/editor-ui/src/plugins/endpoints/N8nPlusEndpointType.ts
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
import { EndpointHandler, Endpoint, EndpointRepresentation, Overlay } from '@jsplumb/core';
|
||||||
|
import { AnchorPlacement, EndpointRepresentationParams } from '@jsplumb/common';
|
||||||
|
import {
|
||||||
|
createElement,
|
||||||
|
EVENT_ENDPOINT_MOUSEOVER,
|
||||||
|
EVENT_ENDPOINT_MOUSEOUT,
|
||||||
|
EVENT_ENDPOINT_CLICK,
|
||||||
|
EVENT_CONNECTION_ABORT,
|
||||||
|
} from '@jsplumb/browser-ui';
|
||||||
|
|
||||||
|
export type ComputedN8nPlusEndpoint = [number, number, number, number, number];
|
||||||
|
interface N8nPlusEndpointParams extends EndpointRepresentationParams {
|
||||||
|
dimensions: number;
|
||||||
|
connectedEndpoint: Endpoint;
|
||||||
|
hoverMessage: string;
|
||||||
|
size: 'small' | 'medium';
|
||||||
|
showOutputLabel: boolean;
|
||||||
|
}
|
||||||
|
export const PlusStalkOverlay = 'plus-stalk';
|
||||||
|
export const HoverMessageOverlay = 'hover-message';
|
||||||
|
|
||||||
|
export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpoint> {
|
||||||
|
params: N8nPlusEndpointParams;
|
||||||
|
label: string;
|
||||||
|
stalkOverlay: Overlay | null;
|
||||||
|
messageOverlay: Overlay | null;
|
||||||
|
|
||||||
|
constructor(endpoint: Endpoint, params: N8nPlusEndpointParams) {
|
||||||
|
super(endpoint, params);
|
||||||
|
|
||||||
|
this.params = params;
|
||||||
|
|
||||||
|
this.label = '';
|
||||||
|
this.stalkOverlay = null;
|
||||||
|
this.messageOverlay = null;
|
||||||
|
|
||||||
|
this.unbindEvents();
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
static type = 'N8nPlus';
|
||||||
|
type = N8nPlusEndpoint.type;
|
||||||
|
|
||||||
|
setupOverlays() {
|
||||||
|
this.clearOverlays();
|
||||||
|
this.endpoint.instance.setSuspendDrawing(true);
|
||||||
|
this.stalkOverlay = this.endpoint.addOverlay({
|
||||||
|
type: 'Custom',
|
||||||
|
options: {
|
||||||
|
id: PlusStalkOverlay,
|
||||||
|
create: () => {
|
||||||
|
const stalk = createElement('div', {}, `${PlusStalkOverlay} ${this.params.size}`);
|
||||||
|
return stalk;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.messageOverlay = this.endpoint.addOverlay({
|
||||||
|
type: 'Custom',
|
||||||
|
options: {
|
||||||
|
id: HoverMessageOverlay,
|
||||||
|
location: 0.5,
|
||||||
|
create: () => {
|
||||||
|
const hoverMessage = createElement('p', {}, `${HoverMessageOverlay} ${this.params.size}`);
|
||||||
|
hoverMessage.innerHTML = this.params.hoverMessage;
|
||||||
|
return hoverMessage;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.endpoint.instance.setSuspendDrawing(false);
|
||||||
|
}
|
||||||
|
bindEvents() {
|
||||||
|
this.instance.bind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||||
|
this.instance.bind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||||
|
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||||
|
this.instance.bind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||||
|
}
|
||||||
|
unbindEvents() {
|
||||||
|
this.instance.unbind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||||
|
this.instance.unbind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||||
|
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||||
|
this.instance.unbind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||||
|
}
|
||||||
|
setStalkLabels = () => {
|
||||||
|
if (!this.endpoint) return;
|
||||||
|
|
||||||
|
const stalkOverlay = this.endpoint.getOverlay(PlusStalkOverlay);
|
||||||
|
const messageOverlay = this.endpoint.getOverlay(HoverMessageOverlay);
|
||||||
|
if (stalkOverlay && messageOverlay) {
|
||||||
|
// Increase the size of the stalk overlay if the label is too long
|
||||||
|
const fnKey = this.label.length > 10 ? 'add' : 'remove';
|
||||||
|
this.instance[`${fnKey}OverlayClass`](stalkOverlay, 'long-stalk');
|
||||||
|
this.instance[`${fnKey}OverlayClass`](messageOverlay, 'long-stalk');
|
||||||
|
this[`${fnKey}Class`]('long-stalk');
|
||||||
|
|
||||||
|
if (this.label) {
|
||||||
|
// @ts-expect-error: Overlay interface is missing the `canvas` property
|
||||||
|
stalkOverlay.canvas.setAttribute('data-label', this.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fireClickEvent = (endpoint: Endpoint) => {
|
||||||
|
if (endpoint === this.endpoint) {
|
||||||
|
this.instance.fire('plusEndpointClick', this.endpoint);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setHoverMessageVisible = (endpoint: Endpoint) => {
|
||||||
|
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||||
|
this.instance.addOverlayClass(this.messageOverlay, 'visible');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsetHoverMessageVisible = (endpoint: Endpoint) => {
|
||||||
|
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||||
|
this.instance.removeOverlayClass(this.messageOverlay, 'visible');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
clearOverlays() {
|
||||||
|
Object.keys(this.endpoint.getOverlays()).forEach((key) => {
|
||||||
|
this.endpoint.removeOverlay(key);
|
||||||
|
});
|
||||||
|
this.stalkOverlay = null;
|
||||||
|
this.messageOverlay = null;
|
||||||
|
}
|
||||||
|
getConnections() {
|
||||||
|
const connections = [
|
||||||
|
...this.endpoint.connections,
|
||||||
|
...this.params.connectedEndpoint.connections,
|
||||||
|
];
|
||||||
|
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
setIsVisible(visible: boolean) {
|
||||||
|
this.instance.setSuspendDrawing(true);
|
||||||
|
Object.keys(this.endpoint.getOverlays()).forEach((overlay) => {
|
||||||
|
this.endpoint.getOverlays()[overlay].setVisible(visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setVisible(visible);
|
||||||
|
|
||||||
|
// Re-trigger the success state if label is set
|
||||||
|
if (visible && this.label) {
|
||||||
|
this.setSuccessOutput(this.label);
|
||||||
|
}
|
||||||
|
this.instance.setSuspendDrawing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccessOutput(label: string) {
|
||||||
|
this.endpoint.addClass('ep-success');
|
||||||
|
if (this.params.showOutputLabel) {
|
||||||
|
this.label = label;
|
||||||
|
this.setStalkLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSuccessOutput() {
|
||||||
|
this.endpoint.removeOverlay('successOutputOverlay');
|
||||||
|
this.endpoint.removeClass('ep-success');
|
||||||
|
this.label = '';
|
||||||
|
this.setStalkLabels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const N8nPlusEndpointHandler: EndpointHandler<N8nPlusEndpoint, ComputedN8nPlusEndpoint> = {
|
||||||
|
type: N8nPlusEndpoint.type,
|
||||||
|
cls: N8nPlusEndpoint,
|
||||||
|
compute: (ep: N8nPlusEndpoint, anchorPoint: AnchorPlacement): ComputedN8nPlusEndpoint => {
|
||||||
|
const x = anchorPoint.curX - ep.params.dimensions / 2;
|
||||||
|
const y = anchorPoint.curY - ep.params.dimensions / 2;
|
||||||
|
const w = ep.params.dimensions;
|
||||||
|
const h = ep.params.dimensions;
|
||||||
|
|
||||||
|
ep.x = x;
|
||||||
|
ep.y = y;
|
||||||
|
ep.w = w;
|
||||||
|
ep.h = h;
|
||||||
|
|
||||||
|
ep.addClass('plus-endpoint');
|
||||||
|
return [x, y, w, h, ep.params.dimensions];
|
||||||
|
},
|
||||||
|
|
||||||
|
getParams: (ep: N8nPlusEndpoint): N8nPlusEndpointParams => {
|
||||||
|
return ep.params;
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,29 +1,44 @@
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { jsPlumb } from 'jsplumb';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import normalizeWheel from 'normalize-wheel';
|
import normalizeWheel from 'normalize-wheel';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows';
|
import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||||
import { useUIStore } from '@/stores/ui';
|
import { useUIStore } from '@/stores/ui';
|
||||||
|
import { useHistoryStore } from '@/stores/history';
|
||||||
import { INodeUi, XYPosition } from '@/Interface';
|
import { INodeUi, XYPosition } from '@/Interface';
|
||||||
import { scaleBigger, scaleReset, scaleSmaller } from '@/utils';
|
import { scaleBigger, scaleReset, scaleSmaller } from '@/utils';
|
||||||
import { START_NODE_TYPE } from '@/constants';
|
import { START_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
||||||
import '@/plugins/N8nCustomConnectorType';
|
import type {
|
||||||
import '@/plugins/PlusEndpointType';
|
BeforeStartEventParams,
|
||||||
|
BrowserJsPlumbInstance,
|
||||||
|
DragStopEventParams,
|
||||||
|
} from '@jsplumb/browser-ui';
|
||||||
|
import { newInstance } from '@jsplumb/browser-ui';
|
||||||
|
import { N8nPlusEndpointHandler } from '@/plugins/endpoints/N8nPlusEndpointType';
|
||||||
|
import * as N8nPlusEndpointRenderer from '@/plugins/endpoints/N8nPlusEndpointRenderer';
|
||||||
|
import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector';
|
||||||
|
import { EndpointFactory, Connectors } from '@jsplumb/core';
|
||||||
|
import { MoveNodeCommand } from '@/models/history';
|
||||||
import {
|
import {
|
||||||
DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
|
DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
|
||||||
getMidCanvasPosition,
|
getMidCanvasPosition,
|
||||||
getNewNodePosition,
|
getNewNodePosition,
|
||||||
getZoomToFit,
|
getZoomToFit,
|
||||||
PLACEHOLDER_TRIGGER_NODE_SIZE,
|
PLACEHOLDER_TRIGGER_NODE_SIZE,
|
||||||
|
CONNECTOR_FLOWCHART_TYPE,
|
||||||
|
GRID_SIZE,
|
||||||
} from '@/utils/nodeViewUtils';
|
} from '@/utils/nodeViewUtils';
|
||||||
|
import { PointXY } from '@jsplumb/util';
|
||||||
|
|
||||||
export const useCanvasStore = defineStore('canvas', () => {
|
export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const workflowStore = useWorkflowsStore();
|
const workflowStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const jsPlumbInstance = jsPlumb.getInstance();
|
const historyStore = useHistoryStore();
|
||||||
|
|
||||||
|
const jsPlumbInstance = ref<BrowserJsPlumbInstance>();
|
||||||
|
const isDragging = ref<boolean>(false);
|
||||||
|
|
||||||
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
|
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
|
||||||
const triggerNodes = computed<INodeUi[]>(() =>
|
const triggerNodes = computed<INodeUi[]>(() =>
|
||||||
|
@ -35,6 +50,10 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const nodeViewScale = ref<number>(1);
|
const nodeViewScale = ref<number>(1);
|
||||||
const canvasAddButtonPosition = ref<XYPosition>([1, 1]);
|
const canvasAddButtonPosition = ref<XYPosition>([1, 1]);
|
||||||
|
|
||||||
|
Connectors.register(N8nConnector.type, N8nConnector);
|
||||||
|
N8nPlusEndpointRenderer.register();
|
||||||
|
EndpointFactory.registerHandler(N8nPlusEndpointHandler);
|
||||||
|
|
||||||
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
|
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
|
||||||
const position = getMidCanvasPosition(nodeViewScale.value, offset || [0, 0]);
|
const position = getMidCanvasPosition(nodeViewScale.value, offset || [0, 0]);
|
||||||
|
|
||||||
|
@ -59,7 +78,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
|
|
||||||
const setZoomLevel = (zoomLevel: number, offset: XYPosition) => {
|
const setZoomLevel = (zoomLevel: number, offset: XYPosition) => {
|
||||||
nodeViewScale.value = zoomLevel;
|
nodeViewScale.value = zoomLevel;
|
||||||
jsPlumbInstance.setZoom(zoomLevel);
|
jsPlumbInstance.value?.setZoom(zoomLevel);
|
||||||
uiStore.nodeViewOffsetPosition = offset;
|
uiStore.nodeViewOffsetPosition = offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,8 +141,106 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
wheelMoveWorkflow(e);
|
wheelMoveWorkflow(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function initInstance(container: Element) {
|
||||||
|
// Make sure to clean-up previous instance if it exists
|
||||||
|
if (jsPlumbInstance.value) {
|
||||||
|
jsPlumbInstance.value.destroy();
|
||||||
|
jsPlumbInstance.value.reset();
|
||||||
|
jsPlumbInstance.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
jsPlumbInstance.value = newInstance({
|
||||||
|
container,
|
||||||
|
connector: CONNECTOR_FLOWCHART_TYPE,
|
||||||
|
resizeObserver: false,
|
||||||
|
dragOptions: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
grid: { w: GRID_SIZE, h: GRID_SIZE },
|
||||||
|
start: (params: BeforeStartEventParams) => {
|
||||||
|
const draggedNode = params.drag.getDragElement();
|
||||||
|
const nodeName = draggedNode.getAttribute('data-name');
|
||||||
|
if (!nodeName) return;
|
||||||
|
isDragging.value = true;
|
||||||
|
|
||||||
|
const isSelected = uiStore.isNodeSelected(nodeName);
|
||||||
|
|
||||||
|
if (params.e && !isSelected) {
|
||||||
|
// Only the node which gets dragged directly gets an event, for all others it is
|
||||||
|
// undefined. So check if the currently dragged node is selected and if not clear
|
||||||
|
// the drag-selection.
|
||||||
|
jsPlumbInstance.value?.clearDragSelection();
|
||||||
|
uiStore.resetSelectedNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
uiStore.addActiveAction('dragActive');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
stop: (params: DragStopEventParams) => {
|
||||||
|
const draggedNode = params.drag.getDragElement();
|
||||||
|
const nodeName = draggedNode.getAttribute('data-name');
|
||||||
|
if (!nodeName) return;
|
||||||
|
const nodeData = workflowStore.getNodeByName(nodeName);
|
||||||
|
isDragging.value = false;
|
||||||
|
if (uiStore.isActionActive('dragActive') && nodeData) {
|
||||||
|
const moveNodes = uiStore.getSelectedNodes.slice();
|
||||||
|
const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name);
|
||||||
|
if (!selectedNodeNames.includes(nodeData.name)) {
|
||||||
|
// If the current node is not in selected add it to the nodes which
|
||||||
|
// got moved manually
|
||||||
|
moveNodes.push(nodeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moveNodes.length > 1) {
|
||||||
|
historyStore.startRecordingUndo();
|
||||||
|
}
|
||||||
|
// This does for some reason just get called once for the node that got clicked
|
||||||
|
// 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 newNodePosition: XYPosition;
|
||||||
|
moveNodes.forEach((node: INodeUi) => {
|
||||||
|
const element = document.getElementById(node.id);
|
||||||
|
if (element === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newNodePosition = [
|
||||||
|
parseInt(element.style.left!.slice(0, -2), 10),
|
||||||
|
parseInt(element.style.top!.slice(0, -2), 10),
|
||||||
|
];
|
||||||
|
|
||||||
|
const updateInformation = {
|
||||||
|
name: node.name,
|
||||||
|
properties: {
|
||||||
|
position: newNodePosition,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldPosition = node.position;
|
||||||
|
if (oldPosition[0] !== newNodePosition[0] || oldPosition[1] !== newNodePosition[1]) {
|
||||||
|
historyStore.pushCommandToUndo(
|
||||||
|
new MoveNodeCommand(node.name, oldPosition, newNodePosition),
|
||||||
|
);
|
||||||
|
workflowStore.updateNodeProperties(updateInformation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (moveNodes.length > 1) {
|
||||||
|
historyStore.stopRecordingUndo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
jsPlumbInstance.value?.setDragConstrainFunction((pos: PointXY) => {
|
||||||
|
const isReadOnly = uiStore.isReadOnlyView;
|
||||||
|
if (isReadOnly) {
|
||||||
|
// Do not allow to move nodes in readOnly mode
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
jsPlumbInstance,
|
|
||||||
isDemo,
|
isDemo,
|
||||||
nodeViewScale,
|
nodeViewScale,
|
||||||
canvasAddButtonPosition,
|
canvasAddButtonPosition,
|
||||||
|
@ -135,5 +252,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
zoomOut,
|
zoomOut,
|
||||||
zoomToFit,
|
zoomToFit,
|
||||||
wheelScroll,
|
wheelScroll,
|
||||||
|
initInstance,
|
||||||
|
jsPlumbInstance,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -251,6 +251,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
return (id: string) =>
|
return (id: string) =>
|
||||||
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
|
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
|
||||||
},
|
},
|
||||||
|
isReadOnlyView(): boolean {
|
||||||
|
return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW].includes(this.currentView as VIEWS);
|
||||||
|
},
|
||||||
isNodeView(): boolean {
|
isNodeView(): boolean {
|
||||||
return [
|
return [
|
||||||
VIEWS.NEW_WORKFLOW.toString(),
|
VIEWS.NEW_WORKFLOW.toString(),
|
||||||
|
|
|
@ -195,7 +195,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
},
|
},
|
||||||
getNodeById() {
|
getNodeById() {
|
||||||
return (nodeId: string): INodeUi | undefined =>
|
return (nodeId: string): INodeUi | undefined =>
|
||||||
this.workflow.nodes.find((node: INodeUi) => node.id === nodeId);
|
this.workflow.nodes.find((node: INodeUi) => {
|
||||||
|
return node.id === nodeId;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
nodesIssuesExist(): boolean {
|
nodesIssuesExist(): boolean {
|
||||||
for (const node of this.workflow.nodes) {
|
for (const node of this.workflow.nodes) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { MAIN_HEADER_TABS, VIEWS } from '@/constants';
|
import { MAIN_HEADER_TABS, VIEWS } from '@/constants';
|
||||||
import { IZoomConfig } from '@/Interface';
|
import { IZoomConfig } from '@/Interface';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows';
|
import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
import { OnConnectionBindInfo } from 'jsplumb';
|
import { ConnectionDetachedParams } from '@jsplumb/core';
|
||||||
import { IConnection } from 'n8n-workflow';
|
import { IConnection } from 'n8n-workflow';
|
||||||
import { Route } from 'vue-router';
|
import { Route } from 'vue-router';
|
||||||
|
|
||||||
|
@ -94,10 +94,10 @@ export const getNodeViewTab = (route: Route): string | null => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConnectionInfo = (
|
export const getConnectionInfo = (
|
||||||
connection: OnConnectionBindInfo,
|
connection: ConnectionDetachedParams,
|
||||||
): [IConnection, IConnection] | null => {
|
): [IConnection, IConnection] | null => {
|
||||||
const sourceInfo = connection.sourceEndpoint.getParameters();
|
const sourceInfo = connection.sourceEndpoint.parameters;
|
||||||
const targetInfo = connection.targetEndpoint.getParameters();
|
const targetInfo = connection.targetEndpoint.parameters;
|
||||||
const sourceNode = useWorkflowsStore().getNodeById(sourceInfo.nodeId);
|
const sourceNode = useWorkflowsStore().getNodeById(sourceInfo.nodeId);
|
||||||
const targetNode = useWorkflowsStore().getNodeById(targetInfo.nodeId);
|
const targetNode = useWorkflowsStore().getNodeById(targetInfo.nodeId);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { closestNumberDivisibleBy, getStyleTokenValue, isNumber } from '@/utils';
|
import { getStyleTokenValue } from '@/utils/htmlUtils';
|
||||||
|
import { isNumber } from '@/utils';
|
||||||
import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from '@/constants';
|
import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from '@/constants';
|
||||||
import { EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface';
|
import { EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface';
|
||||||
import { AnchorArraySpec, Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from 'jsplumb';
|
import { ArrayAnchorSpec, ConnectorSpec, OverlaySpec, PaintStyle } from '@jsplumb/common';
|
||||||
|
import { Endpoint, Connection } from '@jsplumb/core';
|
||||||
|
import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector';
|
||||||
|
import { closestNumberDivisibleBy } from '@/utils';
|
||||||
import {
|
import {
|
||||||
IConnection,
|
IConnection,
|
||||||
INode,
|
INode,
|
||||||
|
@ -10,6 +14,7 @@ import {
|
||||||
NodeInputConnections,
|
NodeInputConnections,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { EVENT_CONNECTION_MOUSEOUT, EVENT_CONNECTION_MOUSEOVER } from '@jsplumb/browser-ui';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Canvas constants and functions.
|
Canvas constants and functions.
|
||||||
|
@ -24,8 +29,7 @@ export const OVERLAY_RUN_ITEMS_ID = 'run-items-label';
|
||||||
export const OVERLAY_CONNECTION_ACTIONS_ID = 'connection-actions';
|
export const OVERLAY_CONNECTION_ACTIONS_ID = 'connection-actions';
|
||||||
export const JSPLUMB_FLOWCHART_STUB = 26;
|
export const JSPLUMB_FLOWCHART_STUB = 26;
|
||||||
export const OVERLAY_INPUT_NAME_LABEL = 'input-name-label';
|
export const OVERLAY_INPUT_NAME_LABEL = 'input-name-label';
|
||||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION = [-3, 0.5];
|
export const OVERLAY_INPUT_NAME_MOVED_CLASS = 'node-input-endpoint-label--moved';
|
||||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED = [-4.5, 0.5];
|
|
||||||
export const OVERLAY_OUTPUT_NAME_LABEL = 'output-name-label';
|
export const OVERLAY_OUTPUT_NAME_LABEL = 'output-name-label';
|
||||||
export const GRID_SIZE = 20;
|
export const GRID_SIZE = 20;
|
||||||
|
|
||||||
|
@ -69,9 +73,9 @@ export const WELCOME_STICKY_NODE = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CONNECTOR_FLOWCHART_TYPE = [
|
export const CONNECTOR_FLOWCHART_TYPE: ConnectorSpec = {
|
||||||
'N8nCustom',
|
type: N8nConnector.type,
|
||||||
{
|
options: {
|
||||||
cornerRadius: 12,
|
cornerRadius: 12,
|
||||||
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
||||||
targetGap: 4,
|
targetGap: 4,
|
||||||
|
@ -91,7 +95,7 @@ export const CONNECTOR_FLOWCHART_TYPE = [
|
||||||
return index * indexOffset + labelOffset + outputsOffset;
|
return index * indexOffset + labelOffset + outputsOffset;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
};
|
||||||
|
|
||||||
export const CONNECTOR_PAINT_STYLE_DEFAULT: PaintStyle = {
|
export const CONNECTOR_PAINT_STYLE_DEFAULT: PaintStyle = {
|
||||||
stroke: getStyleTokenValue('--color-foreground-dark'),
|
stroke: getStyleTokenValue('--color-foreground-dark'),
|
||||||
|
@ -110,15 +114,10 @@ export const CONNECTOR_PAINT_STYLE_PRIMARY = {
|
||||||
stroke: getStyleTokenValue('--color-primary'),
|
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[] = [
|
export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
||||||
[
|
|
||||||
'Arrow',
|
|
||||||
{
|
{
|
||||||
|
type: 'Arrow',
|
||||||
|
options: {
|
||||||
id: OVERLAY_ENDPOINT_ARROW_ID,
|
id: OVERLAY_ENDPOINT_ARROW_ID,
|
||||||
location: 1,
|
location: 1,
|
||||||
width: 12,
|
width: 12,
|
||||||
|
@ -126,10 +125,10 @@ export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
||||||
length: 10,
|
length: 10,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
[
|
|
||||||
'Arrow',
|
|
||||||
{
|
{
|
||||||
|
type: 'Arrow',
|
||||||
|
options: {
|
||||||
id: OVERLAY_MIDPOINT_ARROW_ID,
|
id: OVERLAY_MIDPOINT_ARROW_ID,
|
||||||
location: 0.5,
|
location: 0.5,
|
||||||
width: 12,
|
width: 12,
|
||||||
|
@ -137,12 +136,12 @@ export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
||||||
length: 10,
|
length: 10,
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ANCHOR_POSITIONS: {
|
export const ANCHOR_POSITIONS: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
[key: number]: AnchorArraySpec[];
|
[key: number]: ArrayAnchorSpec[];
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
input: {
|
input: {
|
||||||
|
@ -194,33 +193,42 @@ export const getInputEndpointStyle = (
|
||||||
lineWidth: 0,
|
lineWidth: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getInputNameOverlay = (label: string): OverlaySpec => [
|
export const getInputNameOverlay = (labelText: string): OverlaySpec => ({
|
||||||
'Label',
|
type: 'Custom',
|
||||||
{
|
options: {
|
||||||
id: OVERLAY_INPUT_NAME_LABEL,
|
id: OVERLAY_INPUT_NAME_LABEL,
|
||||||
location: OVERLAY_INPUT_NAME_LABEL_POSITION,
|
|
||||||
label,
|
|
||||||
cssClass: 'node-input-endpoint-label',
|
|
||||||
visible: true,
|
visible: true,
|
||||||
|
create: (component: Endpoint) => {
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.innerHTML = labelText;
|
||||||
|
label.classList.add('node-input-endpoint-label');
|
||||||
|
return label;
|
||||||
},
|
},
|
||||||
];
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const getOutputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({
|
export const getOutputEndpointStyle = (
|
||||||
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
nodeTypeData: INodeTypeDescription,
|
||||||
|
color: string,
|
||||||
|
): PaintStyle => ({
|
||||||
|
strokeWidth: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||||
fill: getStyleTokenValue(color),
|
fill: getStyleTokenValue(color),
|
||||||
outlineStroke: 'none',
|
outlineStroke: 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getOutputNameOverlay = (label: string): OverlaySpec => [
|
export const getOutputNameOverlay = (labelText: string): OverlaySpec => ({
|
||||||
'Label',
|
type: 'Custom',
|
||||||
{
|
options: {
|
||||||
id: OVERLAY_OUTPUT_NAME_LABEL,
|
id: OVERLAY_OUTPUT_NAME_LABEL,
|
||||||
location: [1.9, 0.5],
|
|
||||||
label,
|
|
||||||
cssClass: 'node-output-endpoint-label',
|
|
||||||
visible: true,
|
visible: true,
|
||||||
|
create: (component: Endpoint) => {
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.innerHTML = labelText;
|
||||||
|
label.classList.add('node-output-endpoint-label');
|
||||||
|
return label;
|
||||||
},
|
},
|
||||||
];
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const addOverlays = (connection: Connection, overlays: OverlaySpec[]) => {
|
export const addOverlays = (connection: Connection, overlays: OverlaySpec[]) => {
|
||||||
overlays.forEach((overlay: OverlaySpec) => {
|
overlays.forEach((overlay: OverlaySpec) => {
|
||||||
|
@ -302,25 +310,34 @@ export const showOrHideMidpointArrow = (connection: Connection) => {
|
||||||
if (!connection || !connection.endpoints || connection.endpoints.length !== 2) {
|
if (!connection || !connection.endpoints || connection.endpoints.length !== 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasItemsLabel = !!getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
const hasItemsLabel = !!getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||||
|
|
||||||
const sourceEndpoint = connection.endpoints[0];
|
const sourceEndpoint = connection.endpoints[0];
|
||||||
const targetEndpoint = connection.endpoints[1];
|
const targetEndpoint = connection.endpoints[1];
|
||||||
|
const sourcePosition = sourceEndpoint._anchor.computedPosition?.curX ?? 0;
|
||||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
const targetPosition = targetEndpoint._anchor.computedPosition?.curX ?? sourcePosition + 1;
|
||||||
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 minimum = hasItemsLabel ? 150 : 0;
|
||||||
const isBackwards = sourcePosition >= targetPosition;
|
const isBackwards = sourcePosition >= targetPosition;
|
||||||
const isTooLong = Math.abs(sourcePosition - targetPosition) >= minimum;
|
const isTooLong = Math.abs(sourcePosition - targetPosition) >= minimum;
|
||||||
|
const isActionsOverlayHovered = getOverlay(
|
||||||
|
connection,
|
||||||
|
OVERLAY_CONNECTION_ACTIONS_ID,
|
||||||
|
)?.component.isHover();
|
||||||
|
const isConnectionHovered = connection.isHover();
|
||||||
|
|
||||||
const arrow = getOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
const arrow = getOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||||
|
const isArrowVisible =
|
||||||
|
isBackwards &&
|
||||||
|
isTooLong &&
|
||||||
|
!isActionsOverlayHovered &&
|
||||||
|
!isConnectionHovered &&
|
||||||
|
!connection.instance.isConnectionBeingDragged;
|
||||||
|
|
||||||
if (arrow) {
|
if (arrow) {
|
||||||
arrow.setVisible(isBackwards && isTooLong);
|
arrow.setVisible(isArrowVisible);
|
||||||
arrow.setLocation(hasItemsLabel ? 0.6 : 0.5);
|
arrow.setLocation(hasItemsLabel ? 0.6 : 0.5);
|
||||||
|
connection.instance.repaint(arrow.canvas);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -329,8 +346,8 @@ export const getConnectorLengths = (connection: Connection): [number, number] =>
|
||||||
return [0, 0];
|
return [0, 0];
|
||||||
}
|
}
|
||||||
const bounds = connection.connector.bounds;
|
const bounds = connection.connector.bounds;
|
||||||
const diffX = Math.abs(bounds.maxX - bounds.minX);
|
const diffX = Math.abs(bounds.xmax - bounds.xmin);
|
||||||
const diffY = Math.abs(bounds.maxY - bounds.minY);
|
const diffY = Math.abs(bounds.ymax - bounds.ymin);
|
||||||
|
|
||||||
return [diffX, diffY];
|
return [diffX, diffY];
|
||||||
};
|
};
|
||||||
|
@ -339,36 +356,30 @@ const isLoopingBackwards = (connection: Connection) => {
|
||||||
const sourceEndpoint = connection.endpoints[0];
|
const sourceEndpoint = connection.endpoints[0];
|
||||||
const targetEndpoint = connection.endpoints[1];
|
const targetEndpoint = connection.endpoints[1];
|
||||||
|
|
||||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
const sourcePosition = sourceEndpoint._anchor.computedPosition?.curX ?? 0;
|
||||||
const targetPosition = targetEndpoint.anchor.lastReturnValue[0];
|
const targetPosition = targetEndpoint._anchor.computedPosition?.curX ?? 0;
|
||||||
|
|
||||||
return targetPosition - sourcePosition < -1 * LOOPBACK_MINIMUM;
|
return targetPosition - sourcePosition < -1 * LOOPBACK_MINIMUM;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showOrHideItemsLabel = (connection: Connection) => {
|
export const showOrHideItemsLabel = (connection: Connection) => {
|
||||||
if (!connection || !connection.connector) {
|
if (!connection?.connector) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlay = getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
const overlay = getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||||
if (!overlay) {
|
if (!overlay) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionsOverlay = getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
const actionsOverlay = getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||||
if (actionsOverlay && actionsOverlay.visible) {
|
const isActionsOverlayHovered = actionsOverlay?.component.isHover();
|
||||||
|
|
||||||
|
if (isActionsOverlayHovered) {
|
||||||
overlay.setVisible(false);
|
overlay.setVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [diffX, diffY] = getConnectorLengths(connection);
|
const [diffX, diffY] = getConnectorLengths(connection);
|
||||||
|
const isHidden = diffX < MIN_X_TO_SHOW_OUTPUT_LABEL && diffY < MIN_Y_TO_SHOW_OUTPUT_LABEL;
|
||||||
|
|
||||||
if (diffX < MIN_X_TO_SHOW_OUTPUT_LABEL && diffY < MIN_Y_TO_SHOW_OUTPUT_LABEL) {
|
overlay.setVisible(!isHidden);
|
||||||
overlay.setVisible(false);
|
|
||||||
} else {
|
|
||||||
overlay.setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const innerElement = overlay.canvas && overlay.canvas.querySelector('span');
|
const innerElement = overlay.canvas && overlay.canvas.querySelector('span');
|
||||||
if (innerElement) {
|
if (innerElement) {
|
||||||
if (diffY === 0 || isLoopingBackwards(connection)) {
|
if (diffY === 0 || isLoopingBackwards(connection)) {
|
||||||
|
@ -503,22 +514,27 @@ export const getBackgroundStyles = (
|
||||||
return styles;
|
return styles;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hideConnectionActions = (connection: Connection | null) => {
|
export const hideConnectionActions = (connection: Connection) => {
|
||||||
if (connection && connection.connector) {
|
connection.instance.setSuspendDrawing(true);
|
||||||
hideOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
hideOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||||
showOrHideItemsLabel(connection);
|
|
||||||
showOrHideMidpointArrow(connection);
|
showOrHideMidpointArrow(connection);
|
||||||
}
|
showOrHideItemsLabel(connection);
|
||||||
|
connection.instance.setSuspendDrawing(false);
|
||||||
|
(connection.endpoints || []).forEach((endpoint) => {
|
||||||
|
connection.instance.repaint(endpoint.element);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showConnectionActions = (connection: Connection | null) => {
|
export const showConnectionActions = (connection: Connection) => {
|
||||||
if (connection && connection.connector) {
|
|
||||||
showOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
showOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||||
hideOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
hideOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||||
if (!getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
if (!getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||||
hideOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
hideOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
(connection.endpoints || []).forEach((endpoint) => {
|
||||||
|
connection.instance.repaint(endpoint.element);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputConnections) => {
|
export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputConnections) => {
|
||||||
|
@ -586,11 +602,9 @@ export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputCo
|
||||||
|
|
||||||
export const resetConnection = (connection: Connection) => {
|
export const resetConnection = (connection: Connection) => {
|
||||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
connection.removeClass('success');
|
||||||
showOrHideMidpointArrow(connection);
|
showOrHideMidpointArrow(connection);
|
||||||
if (connection.canvas) {
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||||
connection.canvas.classList.remove('success');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRunItemsLabel = (output: { total: number; iterations: number }): string => {
|
export const getRunItemsLabel = (output: { total: number; iterations: number }): string => {
|
||||||
|
@ -604,27 +618,36 @@ export const addConnectionOutputSuccess = (
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
output: { total: number; iterations: number },
|
output: { total: number; iterations: number },
|
||||||
) => {
|
) => {
|
||||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_SUCCESS);
|
connection.addClass('success');
|
||||||
if (connection.canvas) {
|
|
||||||
connection.canvas.classList.add('success');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
if (getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.addOverlay([
|
const overlay = connection.addOverlay({
|
||||||
'Label',
|
type: 'Custom',
|
||||||
{
|
options: {
|
||||||
id: OVERLAY_RUN_ITEMS_ID,
|
id: OVERLAY_RUN_ITEMS_ID,
|
||||||
label: `<span>${getRunItemsLabel(output)}</span>`,
|
create() {
|
||||||
cssClass: 'connection-run-items-label',
|
const container = document.createElement('div');
|
||||||
|
const span = document.createElement('span');
|
||||||
|
|
||||||
|
container.classList.add('connection-run-items-label');
|
||||||
|
span.classList.add('floating');
|
||||||
|
span.innerHTML = getRunItemsLabel(output);
|
||||||
|
container.appendChild(span);
|
||||||
|
return container;
|
||||||
|
},
|
||||||
location: 0.5,
|
location: 0.5,
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
|
|
||||||
|
overlay.setVisible(true);
|
||||||
showOrHideItemsLabel(connection);
|
showOrHideItemsLabel(connection);
|
||||||
showOrHideMidpointArrow(connection);
|
showOrHideMidpointArrow(connection);
|
||||||
|
|
||||||
|
(connection.endpoints || []).forEach((endpoint) => {
|
||||||
|
connection.instance.repaint(endpoint.element);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getContentDimensions = (): { editorWidth: number; editorHeight: number } => {
|
const getContentDimensions = (): { editorWidth: number; editorHeight: number } => {
|
||||||
|
@ -677,9 +700,10 @@ export const getZoomToFit = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
||||||
if (connection && connection.connector) {
|
if (connection?.connector) {
|
||||||
|
const connector = connection.connector as N8nConnector;
|
||||||
if (targetEndpoint) {
|
if (targetEndpoint) {
|
||||||
connection.connector.setTargetEndpoint(targetEndpoint);
|
connector.setTargetEndpoint(targetEndpoint);
|
||||||
}
|
}
|
||||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
||||||
hideOverlay(connection, OVERLAY_DROP_NODE_ID);
|
hideOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||||
|
@ -687,31 +711,33 @@ export const showDropConnectionState = (connection: Connection, targetEndpoint?:
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showPullConnectionState = (connection: Connection) => {
|
export const showPullConnectionState = (connection: Connection) => {
|
||||||
if (connection && connection.connector) {
|
if (connection?.connector) {
|
||||||
connection.connector.resetTargetEndpoint();
|
const connector = connection.connector as N8nConnector;
|
||||||
|
connector.resetTargetEndpoint();
|
||||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PULL);
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PULL);
|
||||||
showOverlay(connection, OVERLAY_DROP_NODE_ID);
|
showOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetConnectionAfterPull = (connection: Connection) => {
|
export const resetConnectionAfterPull = (connection: Connection) => {
|
||||||
if (connection && connection.connector) {
|
if (connection?.connector) {
|
||||||
connection.connector.resetTargetEndpoint();
|
const connector = connection.connector as N8nConnector;
|
||||||
|
connector.resetTargetEndpoint();
|
||||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetInputLabelPosition = (targetEndpoint: Endpoint) => {
|
export const resetInputLabelPosition = (targetEndpoint: Connection | Endpoint) => {
|
||||||
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||||
if (inputNameOverlay) {
|
if (inputNameOverlay) {
|
||||||
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION);
|
targetEndpoint.instance.removeOverlayClass(inputNameOverlay, OVERLAY_INPUT_NAME_MOVED_CLASS);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveBackInputLabelPosition = (targetEndpoint: Endpoint) => {
|
export const moveBackInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||||
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||||
if (inputNameOverlay) {
|
if (inputNameOverlay) {
|
||||||
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED);
|
targetEndpoint.instance.addOverlayClass(inputNameOverlay, OVERLAY_INPUT_NAME_MOVED_CLASS);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -720,36 +746,42 @@ export const addConnectionActionsOverlay = (
|
||||||
onDelete: Function,
|
onDelete: Function,
|
||||||
onAdd: Function,
|
onAdd: Function,
|
||||||
) => {
|
) => {
|
||||||
if (getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID)) {
|
const overlay = connection.addOverlay({
|
||||||
return; // avoid free floating actions when moving connection from one node to another
|
type: 'Custom',
|
||||||
}
|
options: {
|
||||||
connection.addOverlay([
|
|
||||||
'Label',
|
|
||||||
{
|
|
||||||
id: OVERLAY_CONNECTION_ACTIONS_ID,
|
id: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||||
label: `<div class="add">${getIcon('plus')}</div> <div class="delete">${getIcon(
|
create: (component: Connection) => {
|
||||||
'trash',
|
const div = document.createElement('div');
|
||||||
)}</div>`,
|
const addButton = document.createElement('button');
|
||||||
cssClass: OVERLAY_CONNECTION_ACTIONS_ID,
|
const deleteButton = document.createElement('button');
|
||||||
visible: false,
|
|
||||||
events: {
|
div.classList.add(OVERLAY_CONNECTION_ACTIONS_ID);
|
||||||
mousedown: (overlay: Overlay, event: MouseEvent) => {
|
addButton.classList.add('add');
|
||||||
const element = event.target as HTMLElement;
|
deleteButton.classList.add('delete');
|
||||||
if (
|
|
||||||
element.classList.contains('delete') ||
|
addButton.innerHTML = getIcon('plus');
|
||||||
(element.parentElement && element.parentElement.classList.contains('delete'))
|
deleteButton.innerHTML = getIcon('trash');
|
||||||
) {
|
|
||||||
onDelete();
|
addButton.addEventListener('click', () => onAdd());
|
||||||
} else if (
|
deleteButton.addEventListener('click', () => onDelete());
|
||||||
element.classList.contains('add') ||
|
|
||||||
(element.parentElement && element.parentElement.classList.contains('add'))
|
// We have to manually trigger connection mouse events because the overlay
|
||||||
) {
|
// is not part of the connection element
|
||||||
onAdd();
|
div.addEventListener('mouseout', () =>
|
||||||
}
|
connection.instance.fire(EVENT_CONNECTION_MOUSEOUT, component),
|
||||||
|
);
|
||||||
|
div.addEventListener('mouseover', () =>
|
||||||
|
connection.instance.fire(EVENT_CONNECTION_MOUSEOVER, component),
|
||||||
|
);
|
||||||
|
|
||||||
|
div.appendChild(addButton);
|
||||||
|
div.appendChild(deleteButton);
|
||||||
|
return div;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
|
overlay.setVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOutputEndpointUUID = (nodeId: string, outputIndex: number) => {
|
export const getOutputEndpointUUID = (nodeId: string, outputIndex: number) => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="$style.container"
|
:class="$style.canvasAddButton"
|
||||||
:style="containerCssVars"
|
:style="containerCssVars"
|
||||||
ref="container"
|
ref="container"
|
||||||
data-test-id="canvas-add-button"
|
data-test-id="canvas-add-button"
|
||||||
|
@ -44,7 +44,7 @@ const containerCssVars = computed(() => ({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.container {
|
.canvasAddButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style['content']">
|
<div :class="$style['content']">
|
||||||
<div
|
<div
|
||||||
class="node-view-root"
|
class="node-view-root do-not-select"
|
||||||
id="node-view-root"
|
id="node-view-root"
|
||||||
data-test-id="node-view-root"
|
data-test-id="node-view-root"
|
||||||
@dragover="onDragOver"
|
@dragover="onDragOver"
|
||||||
|
@ -37,10 +37,11 @@
|
||||||
v-show="showCanvasAddButton"
|
v-show="showCanvasAddButton"
|
||||||
:showTooltip="!containsTrigger && showTriggerMissingTooltip"
|
:showTooltip="!containsTrigger && showTriggerMissingTooltip"
|
||||||
:position="canvasStore.canvasAddButtonPosition"
|
:position="canvasStore.canvasAddButtonPosition"
|
||||||
|
ref="canvasAddButton"
|
||||||
@hook:mounted="canvasStore.setRecenteredCanvasAddButtonPosition"
|
@hook:mounted="canvasStore.setRecenteredCanvasAddButtonPosition"
|
||||||
data-test-id="canvas-add-button"
|
data-test-id="canvas-add-button"
|
||||||
/>
|
/>
|
||||||
<div v-for="nodeData in nodes" :key="nodeData.id">
|
<template v-for="nodeData in nodes">
|
||||||
<node
|
<node
|
||||||
v-if="nodeData.type !== STICKY_NODE_TYPE"
|
v-if="nodeData.type !== STICKY_NODE_TYPE"
|
||||||
@duplicateNode="duplicateNode"
|
@duplicateNode="duplicateNode"
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
@runWorkflow="onRunNode"
|
@runWorkflow="onRunNode"
|
||||||
@moved="onNodeMoved"
|
@moved="onNodeMoved"
|
||||||
@run="onNodeRun"
|
@run="onNodeRun"
|
||||||
|
:ref="`node-${nodeData.id}`"
|
||||||
:key="`${nodeData.id}_node`"
|
:key="`${nodeData.id}_node`"
|
||||||
:name="nodeData.name"
|
:name="nodeData.name"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
|
@ -74,6 +76,7 @@
|
||||||
@nodeSelected="nodeSelectedByName"
|
@nodeSelected="nodeSelectedByName"
|
||||||
@removeNode="(name) => removeNode(name, true)"
|
@removeNode="(name) => removeNode(name, true)"
|
||||||
:key="`${nodeData.id}_sticky`"
|
:key="`${nodeData.id}_sticky`"
|
||||||
|
:ref="`node-${nodeData.id}`"
|
||||||
:name="nodeData.name"
|
:name="nodeData.name"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
|
@ -82,7 +85,7 @@
|
||||||
:gridSize="GRID_SIZE"
|
:gridSize="GRID_SIZE"
|
||||||
:hideActions="pullConnActive"
|
:hideActions="pullConnActive"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<node-details-view
|
<node-details-view
|
||||||
|
@ -157,15 +160,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue, { ComponentInstance } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import type {
|
|
||||||
OnConnectionBindInfo,
|
import {
|
||||||
Connection,
|
|
||||||
Endpoint,
|
Endpoint,
|
||||||
N8nPlusEndpoint,
|
Connection,
|
||||||
jsPlumbInstance,
|
EVENT_CONNECTION,
|
||||||
} from 'jsplumb';
|
ConnectionEstablishedParams,
|
||||||
|
EVENT_CONNECTION_DETACHED,
|
||||||
|
EVENT_CONNECTION_MOVED,
|
||||||
|
INTERCEPT_BEFORE_DROP,
|
||||||
|
BeforeDropParams,
|
||||||
|
ConnectionDetachedParams,
|
||||||
|
ConnectionMovedParams,
|
||||||
|
} from '@jsplumb/core';
|
||||||
import type { MessageBoxInputData } from 'element-ui/types/message-box';
|
import type { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -206,7 +215,6 @@ import Node from '@/components/Node.vue';
|
||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import Sticky from '@/components/Sticky.vue';
|
import Sticky from '@/components/Sticky.vue';
|
||||||
import CanvasAddButton from './CanvasAddButton.vue';
|
import CanvasAddButton from './CanvasAddButton.vue';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import {
|
import {
|
||||||
|
@ -275,7 +283,20 @@ import {
|
||||||
RemoveConnectionCommand,
|
RemoveConnectionCommand,
|
||||||
RemoveNodeCommand,
|
RemoveNodeCommand,
|
||||||
RenameNodeCommand,
|
RenameNodeCommand,
|
||||||
|
historyBus,
|
||||||
} from '@/models/history';
|
} from '@/models/history';
|
||||||
|
import {
|
||||||
|
EVENT_ENDPOINT_MOUSEOVER,
|
||||||
|
EVENT_ENDPOINT_MOUSEOUT,
|
||||||
|
EVENT_DRAG_MOVE,
|
||||||
|
EVENT_CONNECTION_DRAG,
|
||||||
|
EVENT_CONNECTION_ABORT,
|
||||||
|
EVENT_CONNECTION_MOUSEOUT,
|
||||||
|
EVENT_CONNECTION_MOUSEOVER,
|
||||||
|
BrowserJsPlumbInstance,
|
||||||
|
ready,
|
||||||
|
} from '@jsplumb/browser-ui';
|
||||||
|
import { N8nPlusEndpoint } from '@/plugins/endpoints/N8nPlusEndpointType';
|
||||||
|
|
||||||
interface AddNodeOptions {
|
interface AddNodeOptions {
|
||||||
position?: XYPosition;
|
position?: XYPosition;
|
||||||
|
@ -313,7 +334,6 @@ export default mixins(
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
registerCustomAction,
|
registerCustomAction,
|
||||||
unregisterCustomAction,
|
unregisterCustomAction,
|
||||||
|
@ -563,7 +583,7 @@ export default mixins(
|
||||||
nodeViewScale(): number {
|
nodeViewScale(): number {
|
||||||
return this.canvasStore.nodeViewScale;
|
return this.canvasStore.nodeViewScale;
|
||||||
},
|
},
|
||||||
instance(): jsPlumbInstance {
|
instance(): BrowserJsPlumbInstance {
|
||||||
return this.canvasStore.jsPlumbInstance;
|
return this.canvasStore.jsPlumbInstance;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -587,7 +607,10 @@ export default mixins(
|
||||||
isExecutionPreview: false,
|
isExecutionPreview: false,
|
||||||
showTriggerMissingTooltip: false,
|
showTriggerMissingTooltip: false,
|
||||||
workflowData: null as INewWorkflowData | null,
|
workflowData: null as INewWorkflowData | null,
|
||||||
|
activeConnection: null as null | Connection,
|
||||||
isProductionExecutionPreview: false,
|
isProductionExecutionPreview: false,
|
||||||
|
enterTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
||||||
|
exitTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
||||||
// jsplumb automatically deletes all loose connections which is in turn recorded
|
// jsplumb automatically deletes all loose connections which is in turn recorded
|
||||||
// in undo history as a user action.
|
// in undo history as a user action.
|
||||||
// This should prevent automatically removed connections from populating undo stack
|
// This should prevent automatically removed connections from populating undo stack
|
||||||
|
@ -1488,7 +1511,7 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.importWorkflowData(workflowData!, false, 'paste');
|
return this.importWorkflowData(workflowData!, 'paste', false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1516,9 +1539,8 @@ export default mixins(
|
||||||
// Imports the given workflow data into the current workflow
|
// Imports the given workflow data into the current workflow
|
||||||
async importWorkflowData(
|
async importWorkflowData(
|
||||||
workflowData: IWorkflowToShare,
|
workflowData: IWorkflowToShare,
|
||||||
// eslint-disable-next-line @typescript-eslint/default-param-last
|
|
||||||
importTags = true,
|
|
||||||
source: string,
|
source: string,
|
||||||
|
importTags = true,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// eslint-disable-line @typescript-eslint/default-param-last
|
// eslint-disable-line @typescript-eslint/default-param-last
|
||||||
// If it is JSON check if it looks on the first look like data we can use
|
// If it is JSON check if it looks on the first look like data we can use
|
||||||
|
@ -2032,14 +2054,14 @@ export default mixins(
|
||||||
this.historyStore.stopRecordingUndo();
|
this.historyStore.stopRecordingUndo();
|
||||||
},
|
},
|
||||||
initNodeView() {
|
initNodeView() {
|
||||||
this.instance.importDefaults({
|
this.instance?.importDefaults({
|
||||||
Connector: NodeViewUtils.CONNECTOR_FLOWCHART_TYPE,
|
endpoint: {
|
||||||
Endpoint: ['Dot', { radius: 5 }],
|
type: 'Dot',
|
||||||
DragOptions: { cursor: 'pointer', zIndex: 5000 },
|
options: { radius: 5 },
|
||||||
PaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_DEFAULT,
|
},
|
||||||
HoverPaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_PRIMARY,
|
paintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||||
ConnectionOverlays: NodeViewUtils.CONNECTOR_ARROW_OVERLAYS,
|
hoverPaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_PRIMARY,
|
||||||
Container: '#node-view',
|
connectionOverlays: NodeViewUtils.CONNECTOR_ARROW_OVERLAYS,
|
||||||
});
|
});
|
||||||
|
|
||||||
const insertNodeAfterSelected = (info: {
|
const insertNodeAfterSelected = (info: {
|
||||||
|
@ -2067,7 +2089,7 @@ export default mixins(
|
||||||
this.onToggleNodeCreator({ source: info.eventSource, createNodeActive: true });
|
this.onToggleNodeCreator({ source: info.eventSource, createNodeActive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.instance.bind('connectionAborted', (connection) => {
|
this.instance.bind(EVENT_CONNECTION_ABORT, (connection: Connection) => {
|
||||||
try {
|
try {
|
||||||
if (this.dropPrevented) {
|
if (this.dropPrevented) {
|
||||||
this.dropPrevented = false;
|
this.dropPrevented = false;
|
||||||
|
@ -2075,10 +2097,10 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pullConnActiveNodeName) {
|
if (this.pullConnActiveNodeName) {
|
||||||
const sourceNode = this.workflowsStore.getNodeById(connection.sourceId);
|
const sourceNode = this.workflowsStore.getNodeById(connection.parameters.nodeId);
|
||||||
if (sourceNode) {
|
if (sourceNode) {
|
||||||
const sourceNodeName = sourceNode.name;
|
const sourceNodeName = sourceNode.name;
|
||||||
const outputIndex = connection.getParameters().index;
|
const outputIndex = connection.parameters.index;
|
||||||
|
|
||||||
this.connectTwoNodes(
|
this.connectTwoNodes(
|
||||||
sourceNodeName,
|
sourceNodeName,
|
||||||
|
@ -2093,8 +2115,8 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
insertNodeAfterSelected({
|
insertNodeAfterSelected({
|
||||||
sourceId: connection.sourceId,
|
sourceId: connection.parameters.nodeId,
|
||||||
index: connection.getParameters().index,
|
index: connection.parameters.index,
|
||||||
eventSource: 'node_connection_drop',
|
eventSource: 'node_connection_drop',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -2102,11 +2124,10 @@ export default mixins(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance.bind('beforeDrop', (info) => {
|
this.instance.bind(INTERCEPT_BEFORE_DROP, (info: BeforeDropParams) => {
|
||||||
try {
|
try {
|
||||||
const sourceInfo = info.connection.endpoints[0].getParameters();
|
const sourceInfo = info.connection.endpoints[0].parameters;
|
||||||
// @ts-ignore
|
const targetInfo = info.dropEndpoint.parameters;
|
||||||
const targetInfo = info.dropEndpoint.getParameters();
|
|
||||||
|
|
||||||
const sourceNodeName = this.workflowsStore.getNodeById(sourceInfo.nodeId)?.name || '';
|
const sourceNodeName = this.workflowsStore.getNodeById(sourceInfo.nodeId)?.name || '';
|
||||||
const targetNodeName = this.workflowsStore.getNodeById(targetInfo.nodeId)?.name || '';
|
const targetNodeName = this.workflowsStore.getNodeById(targetInfo.nodeId)?.name || '';
|
||||||
|
@ -2127,13 +2148,10 @@ export default mixins(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// only one set of visible actions should be visible at the same time
|
this.instance.bind(EVENT_CONNECTION, (info: ConnectionEstablishedParams) => {
|
||||||
let activeConnection: null | Connection = null;
|
|
||||||
|
|
||||||
this.instance.bind('connection', (info: OnConnectionBindInfo) => {
|
|
||||||
try {
|
try {
|
||||||
const sourceInfo = info.sourceEndpoint.getParameters();
|
const sourceInfo = info.sourceEndpoint.parameters;
|
||||||
const targetInfo = info.targetEndpoint.getParameters();
|
const targetInfo = info.targetEndpoint.parameters;
|
||||||
|
|
||||||
const sourceNodeName = this.workflowsStore.getNodeById(sourceInfo.nodeId)?.name;
|
const sourceNodeName = this.workflowsStore.getNodeById(sourceInfo.nodeId)?.name;
|
||||||
const targetNodeName = this.workflowsStore.getNodeById(targetInfo.nodeId)?.name;
|
const targetNodeName = this.workflowsStore.getNodeById(targetInfo.nodeId)?.name;
|
||||||
|
@ -2148,86 +2166,6 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeViewUtils.resetConnection(info.connection);
|
NodeViewUtils.resetConnection(info.connection);
|
||||||
|
|
||||||
if (!this.isReadOnly) {
|
|
||||||
let exitTimer: NodeJS.Timeout | undefined;
|
|
||||||
let enterTimer: NodeJS.Timeout | undefined;
|
|
||||||
info.connection.bind('mouseover', (connection: Connection) => {
|
|
||||||
try {
|
|
||||||
if (exitTimer !== undefined) {
|
|
||||||
clearTimeout(exitTimer);
|
|
||||||
exitTimer = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enterTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.connection || info.connection === activeConnection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NodeViewUtils.hideConnectionActions(activeConnection);
|
|
||||||
|
|
||||||
enterTimer = setTimeout(() => {
|
|
||||||
enterTimer = undefined;
|
|
||||||
if (info.connection) {
|
|
||||||
activeConnection = info.connection;
|
|
||||||
NodeViewUtils.showConnectionActions(info.connection);
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
info.connection.bind('mouseout', (connection: Connection) => {
|
|
||||||
try {
|
|
||||||
if (exitTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enterTimer) {
|
|
||||||
clearTimeout(enterTimer);
|
|
||||||
enterTimer = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!info.connection || activeConnection !== info.connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
exitTimer = setTimeout(() => {
|
|
||||||
exitTimer = undefined;
|
|
||||||
|
|
||||||
if (info.connection && activeConnection === info.connection) {
|
|
||||||
NodeViewUtils.hideConnectionActions(activeConnection);
|
|
||||||
activeConnection = null;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
NodeViewUtils.addConnectionActionsOverlay(
|
|
||||||
info.connection,
|
|
||||||
() => {
|
|
||||||
activeConnection = null;
|
|
||||||
this.__deleteJSPlumbConnection(info.connection);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
insertNodeAfterSelected({
|
|
||||||
sourceId: info.sourceId,
|
|
||||||
index: sourceInfo.index,
|
|
||||||
connection: info.connection,
|
|
||||||
eventSource: 'node_connection_action',
|
|
||||||
});
|
|
||||||
}, 150);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NodeViewUtils.moveBackInputLabelPosition(info.targetEndpoint);
|
NodeViewUtils.moveBackInputLabelPosition(info.targetEndpoint);
|
||||||
|
|
||||||
const connectionData: [IConnection, IConnection] = [
|
const connectionData: [IConnection, IConnection] = [
|
||||||
|
@ -2247,26 +2185,105 @@ export default mixins(
|
||||||
connection: connectionData,
|
connection: connectionData,
|
||||||
setStateDirty: true,
|
setStateDirty: true,
|
||||||
});
|
});
|
||||||
|
this.dropPrevented = true;
|
||||||
if (!this.suspendRecordingDetachedConnections) {
|
if (!this.suspendRecordingDetachedConnections) {
|
||||||
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData, this));
|
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData));
|
||||||
|
}
|
||||||
|
if (!this.isReadOnly) {
|
||||||
|
NodeViewUtils.addConnectionActionsOverlay(
|
||||||
|
info.connection,
|
||||||
|
() => {
|
||||||
|
this.activeConnection = null;
|
||||||
|
this.__deleteJSPlumbConnection(info.connection);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
insertNodeAfterSelected({
|
||||||
|
sourceId: info.sourceEndpoint.parameters.nodeId,
|
||||||
|
index: sourceInfo.index,
|
||||||
|
connection: info.connection,
|
||||||
|
eventSource: 'node_connection_action',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e); // eslint-disable-line no-console
|
console.error(e); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance.bind('connectionMoved', (info) => {
|
this.instance.bind(EVENT_DRAG_MOVE, () => {
|
||||||
|
this.instance?.connections.forEach((connection) => {
|
||||||
|
NodeViewUtils.showOrHideItemsLabel(connection);
|
||||||
|
NodeViewUtils.showOrHideMidpointArrow(connection);
|
||||||
|
|
||||||
|
Object.values(connection.overlays).forEach((overlay) => {
|
||||||
|
if (!overlay.canvas) return;
|
||||||
|
this.instance?.repaint(overlay.canvas);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.instance.bind(EVENT_CONNECTION_MOUSEOVER, (connection: Connection) => {
|
||||||
|
try {
|
||||||
|
if (this.exitTimer !== undefined) {
|
||||||
|
clearTimeout(this.exitTimer);
|
||||||
|
this.exitTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isReadOnly ||
|
||||||
|
this.enterTimer ||
|
||||||
|
!connection ||
|
||||||
|
connection === this.activeConnection
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.activeConnection) NodeViewUtils.hideConnectionActions(this.activeConnection);
|
||||||
|
|
||||||
|
this.enterTimer = setTimeout(() => {
|
||||||
|
this.enterTimer = undefined;
|
||||||
|
if (connection) {
|
||||||
|
NodeViewUtils.showConnectionActions(connection);
|
||||||
|
this.activeConnection = connection;
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.instance.bind(EVENT_CONNECTION_MOUSEOUT, (connection: Connection) => {
|
||||||
|
try {
|
||||||
|
if (this.exitTimer) return;
|
||||||
|
|
||||||
|
if (this.enterTimer) {
|
||||||
|
clearTimeout(this.enterTimer);
|
||||||
|
this.enterTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isReadOnly || !connection || this.activeConnection?.id !== connection.id) return;
|
||||||
|
|
||||||
|
this.exitTimer = setTimeout(() => {
|
||||||
|
this.exitTimer = undefined;
|
||||||
|
|
||||||
|
if (connection && this.activeConnection === connection) {
|
||||||
|
NodeViewUtils.hideConnectionActions(this.activeConnection);
|
||||||
|
this.activeConnection = null;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.instance.bind(EVENT_CONNECTION_MOVED, (info: ConnectionMovedParams) => {
|
||||||
try {
|
try {
|
||||||
// When a connection gets moved from one node to another it for some reason
|
// When a connection gets moved from one node to another it for some reason
|
||||||
// calls the "connection" event but not the "connectionDetached" one. So we listen
|
// calls the "connection" event but not the "connectionDetached" one. So we listen
|
||||||
// additionally to the "connectionMoved" event and then only delete the existing connection.
|
// additionally to the "connectionMoved" event and then only delete the existing connection.
|
||||||
|
|
||||||
NodeViewUtils.resetInputLabelPosition(info.originalTargetEndpoint);
|
NodeViewUtils.resetInputLabelPosition(info.connection);
|
||||||
|
|
||||||
// @ts-ignore
|
const sourceInfo = info.connection.parameters;
|
||||||
const sourceInfo = info.originalSourceEndpoint.getParameters();
|
const targetInfo = info.originalEndpoint.parameters;
|
||||||
// @ts-ignore
|
|
||||||
const targetInfo = info.originalTargetEndpoint.getParameters();
|
|
||||||
|
|
||||||
const connectionInfo = [
|
const connectionInfo = [
|
||||||
{
|
{
|
||||||
|
@ -2286,8 +2303,18 @@ export default mixins(
|
||||||
console.error(e); // eslint-disable-line no-console
|
console.error(e); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.instance.bind(EVENT_ENDPOINT_MOUSEOVER, (endpoint: Endpoint, mouse) => {
|
||||||
this.instance.bind('connectionDetached', async (info) => {
|
// This event seems bugged. It gets called constantly even when the mouse is not over the endpoint
|
||||||
|
// if the endpoint has a connection attached to it. So we need to check if the mouse is actually over
|
||||||
|
// the endpoint.
|
||||||
|
if (!endpoint.isTarget || mouse.target !== endpoint.endpoint.canvas) return;
|
||||||
|
this.instance.setHover(endpoint, true);
|
||||||
|
});
|
||||||
|
this.instance.bind(EVENT_ENDPOINT_MOUSEOUT, (endpoint: Endpoint) => {
|
||||||
|
if (!endpoint.isTarget) return;
|
||||||
|
this.instance.setHover(endpoint, false);
|
||||||
|
});
|
||||||
|
this.instance.bind(EVENT_CONNECTION_DETACHED, async (info: ConnectionDetachedParams) => {
|
||||||
try {
|
try {
|
||||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||||
NodeViewUtils.resetInputLabelPosition(info.targetEndpoint);
|
NodeViewUtils.resetInputLabelPosition(info.targetEndpoint);
|
||||||
|
@ -2297,14 +2324,12 @@ export default mixins(
|
||||||
if (this.pullConnActiveNodeName) {
|
if (this.pullConnActiveNodeName) {
|
||||||
// establish new connection when dragging connection from one node to another
|
// establish new connection when dragging connection from one node to another
|
||||||
this.historyStore.startRecordingUndo();
|
this.historyStore.startRecordingUndo();
|
||||||
const sourceNode = this.workflowsStore.getNodeById(info.connection.sourceId);
|
const sourceNode = this.workflowsStore.getNodeById(info.connection.parameters.nodeId);
|
||||||
const sourceNodeName = sourceNode.name;
|
const sourceNodeName = sourceNode.name;
|
||||||
const outputIndex = info.connection.getParameters().index;
|
const outputIndex = info.connection.parameters.index;
|
||||||
|
|
||||||
if (connectionInfo) {
|
if (connectionInfo) {
|
||||||
this.historyStore.pushCommandToUndo(
|
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
||||||
new RemoveConnectionCommand(connectionInfo, this),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0, true);
|
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0, true);
|
||||||
this.pullConnActiveNodeName = null;
|
this.pullConnActiveNodeName = null;
|
||||||
|
@ -2324,26 +2349,27 @@ export default mixins(
|
||||||
console.error(e); // eslint-disable-line no-console
|
console.error(e); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.instance.bind(EVENT_CONNECTION_DRAG, (connection: Connection) => {
|
||||||
// @ts-ignore
|
// The overlays are visible by default so we need to hide the midpoint arrow
|
||||||
this.instance.bind('connectionDrag', (connection: Connection) => {
|
// manually
|
||||||
|
connection.overlays['midpoint-arrow']?.setVisible(false);
|
||||||
try {
|
try {
|
||||||
this.pullConnActiveNodeName = null;
|
this.pullConnActiveNodeName = null;
|
||||||
this.pullConnActive = true;
|
this.pullConnActive = true;
|
||||||
this.newNodeInsertPosition = null;
|
this.newNodeInsertPosition = null;
|
||||||
NodeViewUtils.resetConnection(connection);
|
NodeViewUtils.resetConnection(connection);
|
||||||
|
|
||||||
const nodes = [...document.querySelectorAll('.node-default')];
|
const nodes = [...document.querySelectorAll('.node-wrapper')];
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent | TouchEvent) => {
|
const onMouseMove = (e: MouseEvent | TouchEvent) => {
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = document.querySelector('.jtk-endpoint.dropHover');
|
const element = document.querySelector('.jtk-endpoint.jtk-drag-hover');
|
||||||
if (element) {
|
if (element) {
|
||||||
// @ts-ignore
|
const endpoint = element.jtk.endpoint;
|
||||||
NodeViewUtils.showDropConnectionState(connection, element._jsPlumb);
|
NodeViewUtils.showDropConnectionState(connection, endpoint);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2360,7 +2386,7 @@ export default mixins(
|
||||||
this.pullConnActiveNodeName = node.name;
|
this.pullConnActiveNodeName = node.name;
|
||||||
const endpointUUID = this.getInputEndpointUUID(nodeName, 0);
|
const endpointUUID = this.getInputEndpointUUID(nodeName, 0);
|
||||||
if (endpointUUID) {
|
if (endpointUUID) {
|
||||||
const endpoint = this.instance.getEndpoint(endpointUUID);
|
const endpoint = this.instance?.getEndpoint(endpointUUID);
|
||||||
|
|
||||||
NodeViewUtils.showDropConnectionState(connection, endpoint);
|
NodeViewUtils.showDropConnectionState(connection, endpoint);
|
||||||
|
|
||||||
|
@ -2395,8 +2421,17 @@ export default mixins(
|
||||||
console.error(e); // eslint-disable-line no-console
|
console.error(e); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.instance.bind(
|
||||||
// @ts-ignore
|
[EVENT_CONNECTION_DRAG, EVENT_CONNECTION_ABORT, EVENT_CONNECTION_DETACHED],
|
||||||
|
(connection: Connection) => {
|
||||||
|
Object.values(this.instance?.endpointsByElement)
|
||||||
|
.flatMap((endpoints) => Object.values(endpoints))
|
||||||
|
.filter((endpoint) => endpoint.endpoint.type === 'N8nPlus')
|
||||||
|
.forEach((endpoint) =>
|
||||||
|
setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
this.instance.bind('plusEndpointClick', (endpoint: Endpoint) => {
|
this.instance.bind('plusEndpointClick', (endpoint: Endpoint) => {
|
||||||
if (endpoint && endpoint.__meta) {
|
if (endpoint && endpoint.__meta) {
|
||||||
insertNodeAfterSelected({
|
insertNodeAfterSelected({
|
||||||
|
@ -2555,10 +2590,8 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid: [string, string] = [outputUuid, inputUuid];
|
const uuid: [string, string] = [outputUuid, inputUuid];
|
||||||
|
|
||||||
// Create connections in DOM
|
// Create connections in DOM
|
||||||
// @ts-ignore
|
this.instance?.connect({
|
||||||
this.instance.connect({
|
|
||||||
uuids: uuid,
|
uuids: uuid,
|
||||||
detachable: !this.isReadOnly,
|
detachable: !this.isReadOnly,
|
||||||
});
|
});
|
||||||
|
@ -2581,15 +2614,12 @@ export default mixins(
|
||||||
if (!sourceNode || !targetNode) {
|
if (!sourceNode || !targetNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const connections = this.instance?.getConnections({
|
||||||
// @ts-ignore
|
|
||||||
const connections = this.instance.getConnections({
|
|
||||||
source: sourceNode.id,
|
source: sourceNode.id,
|
||||||
target: targetNode.id,
|
target: targetNode.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
connections.forEach((connectionInstance: Connection) => {
|
||||||
connections.forEach((connectionInstance) => {
|
|
||||||
if (connectionInstance.__meta) {
|
if (connectionInstance.__meta) {
|
||||||
// Only delete connections from specific indexes (if it can be determined by meta)
|
// Only delete connections from specific indexes (if it can be determined by meta)
|
||||||
if (
|
if (
|
||||||
|
@ -2611,13 +2641,8 @@ export default mixins(
|
||||||
// it visibly stays behind free floating without a connection.
|
// it visibly stays behind free floating without a connection.
|
||||||
connection.removeOverlays();
|
connection.removeOverlays();
|
||||||
|
|
||||||
const sourceEndpoint = connection.endpoints && connection.endpoints[0];
|
|
||||||
this.pullConnActiveNodeName = null; // prevent new connections when connectionDetached is triggered
|
this.pullConnActiveNodeName = null; // prevent new connections when connectionDetached is triggered
|
||||||
this.instance.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store
|
this.instance?.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store
|
||||||
if (sourceEndpoint) {
|
|
||||||
const endpoints = this.instance.getEndpoints(sourceEndpoint.elementId);
|
|
||||||
endpoints.forEach((endpoint: Endpoint) => endpoint.repaint()); // repaint both circle and plus endpoint
|
|
||||||
}
|
|
||||||
if (trackHistory && connection.__meta) {
|
if (trackHistory && connection.__meta) {
|
||||||
const connectionData: [IConnection, IConnection] = [
|
const connectionData: [IConnection, IConnection] = [
|
||||||
{
|
{
|
||||||
|
@ -2635,18 +2660,14 @@ export default mixins(
|
||||||
this.historyStore.pushCommandToUndo(removeCommand);
|
this.historyStore.pushCommandToUndo(removeCommand);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
__removeConnectionByConnectionInfo(
|
__removeConnectionByConnectionInfo(info, removeVisualConnection = false, trackHistory = false) {
|
||||||
info: OnConnectionBindInfo,
|
|
||||||
removeVisualConnection = false,
|
|
||||||
trackHistory = false,
|
|
||||||
) {
|
|
||||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||||
|
|
||||||
if (connectionInfo) {
|
if (connectionInfo) {
|
||||||
if (removeVisualConnection) {
|
if (removeVisualConnection) {
|
||||||
this.__deleteJSPlumbConnection(info.connection, trackHistory);
|
this.__deleteJSPlumbConnection(info.connection, trackHistory);
|
||||||
} else if (trackHistory) {
|
} else if (trackHistory) {
|
||||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo, this));
|
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
||||||
}
|
}
|
||||||
this.workflowsStore.removeConnection({ connection: connectionInfo });
|
this.workflowsStore.removeConnection({ connection: connectionInfo });
|
||||||
}
|
}
|
||||||
|
@ -2751,7 +2772,7 @@ export default mixins(
|
||||||
const targetEndpoint = NodeViewUtils.getInputEndpointUUID(targetId, targetInputIndex);
|
const targetEndpoint = NodeViewUtils.getInputEndpointUUID(targetId, targetInputIndex);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const connections = this.instance.getConnections({
|
const connections = this.instance?.getConnections({
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
target: targetId,
|
target: targetId,
|
||||||
}) as Connection[];
|
}) as Connection[];
|
||||||
|
@ -2763,14 +2784,19 @@ export default mixins(
|
||||||
},
|
},
|
||||||
getJSPlumbEndpoints(nodeName: string): Endpoint[] {
|
getJSPlumbEndpoints(nodeName: string): Endpoint[] {
|
||||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||||
return this.instance.getEndpoints(node !== null ? node.id : '');
|
const nodeEls: Element = (this.$refs[`node-${node?.id}`] as ComponentInstance[])[0]
|
||||||
|
.$el as Element;
|
||||||
|
|
||||||
|
const endpoints = this.instance?.getEndpoints(nodeEls);
|
||||||
|
|
||||||
|
return endpoints as Endpoint[];
|
||||||
},
|
},
|
||||||
getPlusEndpoint(nodeName: string, outputIndex: number): Endpoint | undefined {
|
getPlusEndpoint(nodeName: string, outputIndex: number): Endpoint | undefined {
|
||||||
const endpoints = this.getJSPlumbEndpoints(nodeName);
|
const endpoints = this.getJSPlumbEndpoints(nodeName);
|
||||||
// @ts-ignore
|
|
||||||
return endpoints.find(
|
return endpoints.find(
|
||||||
(endpoint: Endpoint) =>
|
(endpoint: Endpoint) =>
|
||||||
endpoint.type === 'N8nPlus' && endpoint.__meta && endpoint.__meta.index === outputIndex,
|
// @ts-ignore
|
||||||
|
endpoint.endpoint.type === 'N8nPlus' && endpoint?.__meta?.index === outputIndex,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getIncomingOutgoingConnections(nodeName: string): {
|
getIncomingOutgoingConnections(nodeName: string): {
|
||||||
|
@ -2781,12 +2807,12 @@ export default mixins(
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const outgoing = this.instance.getConnections({
|
const outgoing = this.instance?.getConnections({
|
||||||
source: node.id,
|
source: node.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const incoming = this.instance.getConnections({
|
const incoming = this.instance?.getConnections({
|
||||||
target: node.id,
|
target: node.id,
|
||||||
}) as Connection[];
|
}) as Connection[];
|
||||||
|
|
||||||
|
@ -2823,8 +2849,7 @@ export default mixins(
|
||||||
const sourceId = sourceNode !== null ? sourceNode.id : '';
|
const sourceId = sourceNode !== null ? sourceNode.id : '';
|
||||||
|
|
||||||
if (data === null || data.length === 0 || waiting) {
|
if (data === null || data.length === 0 || waiting) {
|
||||||
// @ts-ignore
|
const outgoing = this.instance?.getConnections({
|
||||||
const outgoing = this.instance.getConnections({
|
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
}) as Connection[];
|
}) as Connection[];
|
||||||
|
|
||||||
|
@ -2833,8 +2858,7 @@ export default mixins(
|
||||||
});
|
});
|
||||||
const endpoints = this.getJSPlumbEndpoints(sourceNodeName);
|
const endpoints = this.getJSPlumbEndpoints(sourceNodeName);
|
||||||
endpoints.forEach((endpoint: Endpoint) => {
|
endpoints.forEach((endpoint: Endpoint) => {
|
||||||
// @ts-ignore
|
if (endpoint.endpoint.type === 'N8nPlus') {
|
||||||
if (endpoint.type === 'N8nPlus') {
|
|
||||||
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
|
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2875,6 +2899,7 @@ export default mixins(
|
||||||
);
|
);
|
||||||
if (endpoint && endpoint.endpoint) {
|
if (endpoint && endpoint.endpoint) {
|
||||||
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
|
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
|
||||||
|
|
||||||
if (output && output.total > 0) {
|
if (output && output.total > 0) {
|
||||||
(endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(
|
(endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(
|
||||||
NodeViewUtils.getRunItemsLabel(output),
|
NodeViewUtils.getRunItemsLabel(output),
|
||||||
|
@ -2963,7 +2988,7 @@ export default mixins(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (waitForNewConnection) {
|
if (waitForNewConnection) {
|
||||||
this.instance.setSuspendDrawing(false, true);
|
this.instance?.setSuspendDrawing(false, true);
|
||||||
waitForNewConnection = false;
|
waitForNewConnection = false;
|
||||||
}
|
}
|
||||||
}, 100); // just to make it clear to users that this is a new connection
|
}, 100); // just to make it clear to users that this is a new connection
|
||||||
|
@ -2973,14 +2998,10 @@ export default mixins(
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Suspend drawing
|
// Suspend drawing
|
||||||
this.instance.setSuspendDrawing(true);
|
this.instance?.setSuspendDrawing(true);
|
||||||
|
(this.instance?.endpointsByElement[node.id] || [])
|
||||||
// Remove all endpoints and the connections in jsplumb
|
.flat()
|
||||||
this.instance.removeAllEndpoints(node.id);
|
.forEach((endpoint) => this.instance?.deleteEndpoint(endpoint));
|
||||||
|
|
||||||
// Remove the draggable
|
|
||||||
// @ts-ignore
|
|
||||||
this.instance.destroyDraggable(node.id);
|
|
||||||
|
|
||||||
// Remove the connections in data
|
// Remove the connections in data
|
||||||
this.workflowsStore.removeAllNodeConnection(node);
|
this.workflowsStore.removeAllNodeConnection(node);
|
||||||
|
@ -2989,13 +3010,13 @@ export default mixins(
|
||||||
|
|
||||||
if (!waitForNewConnection) {
|
if (!waitForNewConnection) {
|
||||||
// Now it can draw again
|
// Now it can draw again
|
||||||
this.instance.setSuspendDrawing(false, true);
|
this.instance?.setSuspendDrawing(false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove node from selected index if found in it
|
// Remove node from selected index if found in it
|
||||||
this.uiStore.removeNodeFromSelection(node);
|
this.uiStore.removeNodeFromSelection(node);
|
||||||
if (trackHistory) {
|
if (trackHistory) {
|
||||||
this.historyStore.pushCommandToUndo(new RemoveNodeCommand(node, this));
|
this.historyStore.pushCommandToUndo(new RemoveNodeCommand(node));
|
||||||
}
|
}
|
||||||
}, 0); // allow other events to finish like drag stop
|
}, 0); // allow other events to finish like drag stop
|
||||||
if (trackHistory && trackBulk) {
|
if (trackHistory && trackBulk) {
|
||||||
|
@ -3069,7 +3090,7 @@ export default mixins(
|
||||||
workflow.renameNode(currentName, newName);
|
workflow.renameNode(currentName, newName);
|
||||||
|
|
||||||
if (trackHistory) {
|
if (trackHistory) {
|
||||||
this.historyStore.pushCommandToUndo(new RenameNodeCommand(currentName, newName, this));
|
this.historyStore.pushCommandToUndo(new RenameNodeCommand(currentName, newName));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update also last selected node and execution data
|
// Update also last selected node and execution data
|
||||||
|
@ -3104,18 +3125,12 @@ export default mixins(
|
||||||
deleteEveryEndpoint() {
|
deleteEveryEndpoint() {
|
||||||
// Check as it does not exist on first load
|
// Check as it does not exist on first load
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
const nodes = this.workflowsStore.allNodes;
|
this.instance?.reset();
|
||||||
nodes.forEach((node: INodeUi) => {
|
Object.values(this.instance?.endpointsByElement)
|
||||||
try {
|
.flatMap((endpoint) => endpoint)
|
||||||
// important to prevent memory leak
|
.forEach((endpoint) => endpoint.destroy());
|
||||||
// @ts-ignore
|
|
||||||
this.instance.destroyDraggable(node.id);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.instance.deleteEveryEndpoint();
|
this.instance.deleteEveryConnection({ fireEvent: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
matchCredentials(node: INodeUi) {
|
matchCredentials(node: INodeUi) {
|
||||||
|
@ -3241,7 +3256,7 @@ export default mixins(
|
||||||
|
|
||||||
this.workflowsStore.addNode(node);
|
this.workflowsStore.addNode(node);
|
||||||
if (trackHistory) {
|
if (trackHistory) {
|
||||||
this.historyStore.pushCommandToUndo(new AddNodeCommand(node, this));
|
this.historyStore.pushCommandToUndo(new AddNodeCommand(node));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3249,7 +3264,7 @@ export default mixins(
|
||||||
await Vue.nextTick();
|
await Vue.nextTick();
|
||||||
|
|
||||||
// Suspend drawing
|
// Suspend drawing
|
||||||
this.instance.setSuspendDrawing(true);
|
this.instance?.setSuspendDrawing(true);
|
||||||
|
|
||||||
// Load the connections
|
// Load the connections
|
||||||
if (connections !== undefined) {
|
if (connections !== undefined) {
|
||||||
|
@ -3285,9 +3300,8 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now it can draw again
|
// Now it can draw again
|
||||||
this.instance.setSuspendDrawing(false, true);
|
this.instance?.setSuspendDrawing(false, true);
|
||||||
},
|
},
|
||||||
async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise<IWorkflowDataUpdate> {
|
async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise<IWorkflowDataUpdate> {
|
||||||
// Because nodes with the same name maybe already exist, it could
|
// Because nodes with the same name maybe already exist, it could
|
||||||
|
@ -3412,6 +3426,7 @@ export default mixins(
|
||||||
tempWorkflow.connectionsBySourceNode,
|
tempWorkflow.connectionsBySourceNode,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.historyStore.stopRecordingUndo();
|
this.historyStore.stopRecordingUndo();
|
||||||
|
|
||||||
this.uiStore.stateIsDirty = true;
|
this.uiStore.stateIsDirty = true;
|
||||||
|
@ -3458,7 +3473,6 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep only the connection to node which get also exported
|
// Keep only the connection to node which get also exported
|
||||||
// @ts-ignore
|
|
||||||
typeConnections = {};
|
typeConnections = {};
|
||||||
for (type of Object.keys(connections)) {
|
for (type of Object.keys(connections)) {
|
||||||
for (sourceIndex = 0; sourceIndex < connections[type].length; sourceIndex++) {
|
for (sourceIndex = 0; sourceIndex < connections[type].length; sourceIndex++) {
|
||||||
|
@ -3508,14 +3522,12 @@ export default mixins(
|
||||||
// Ignore all errors
|
// Ignore all errors
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.workflowsStore.removeAllConnections({ setStateDirty: false });
|
this.workflowsStore.removeAllConnections({ setStateDirty: false });
|
||||||
this.workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
|
this.workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
|
||||||
|
|
||||||
// Reset workflow execution data
|
// Reset workflow execution data
|
||||||
this.workflowsStore.setWorkflowExecutionData(null);
|
this.workflowsStore.setWorkflowExecutionData(null);
|
||||||
this.workflowsStore.resetAllNodesIssues();
|
this.workflowsStore.resetAllNodesIssues();
|
||||||
// vm.$forceUpdate();
|
|
||||||
|
|
||||||
this.workflowsStore.setActive(false);
|
this.workflowsStore.setActive(false);
|
||||||
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||||
|
@ -3622,12 +3634,12 @@ export default mixins(
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
async onImportWorkflowDataEvent(data: IDataObject) {
|
async onImportWorkflowDataEvent(data: IDataObject) {
|
||||||
await this.importWorkflowData(data.data as IWorkflowDataUpdate, undefined, 'file');
|
await this.importWorkflowData(data.data as IWorkflowDataUpdate, 'file');
|
||||||
},
|
},
|
||||||
async onImportWorkflowUrlEvent(data: IDataObject) {
|
async onImportWorkflowUrlEvent(data: IDataObject) {
|
||||||
const workflowData = await this.getWorkflowDataFromUrl(data.url as string);
|
const workflowData = await this.getWorkflowDataFromUrl(data.url as string);
|
||||||
if (workflowData !== undefined) {
|
if (workflowData !== undefined) {
|
||||||
await this.importWorkflowData(workflowData, undefined, 'url');
|
await this.importWorkflowData(workflowData, 'url');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addPinDataConnections(pinData: IPinData) {
|
addPinDataConnections(pinData: IPinData) {
|
||||||
|
@ -3638,7 +3650,7 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const connections = this.instance.getConnections({
|
const connections = this.instance?.getConnections({
|
||||||
source: node.id,
|
source: node.id,
|
||||||
}) as Connection[];
|
}) as Connection[];
|
||||||
|
|
||||||
|
@ -3658,11 +3670,13 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const connections = this.instance.getConnections({
|
const connections = this.instance?.getConnections({
|
||||||
source: node.id,
|
source: node.id,
|
||||||
}) as Connection[];
|
}) as Connection[];
|
||||||
|
|
||||||
|
this.instance.setSuspendDrawing(true);
|
||||||
connections.forEach(NodeViewUtils.resetConnection);
|
connections.forEach(NodeViewUtils.resetConnection);
|
||||||
|
this.instance.setSuspendDrawing(false, true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onToggleNodeCreator({
|
onToggleNodeCreator({
|
||||||
|
@ -3738,10 +3752,10 @@ export default mixins(
|
||||||
},
|
},
|
||||||
onMoveNode({ nodeName, position }: { nodeName: string; position: XYPosition }): void {
|
onMoveNode({ nodeName, position }: { nodeName: string; position: XYPosition }): void {
|
||||||
this.workflowsStore.updateNodeProperties({ name: nodeName, properties: { position } });
|
this.workflowsStore.updateNodeProperties({ name: nodeName, properties: { position } });
|
||||||
setTimeout(() => {
|
|
||||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||||
|
setTimeout(() => {
|
||||||
if (node) {
|
if (node) {
|
||||||
this.instance.repaintEverything();
|
this.instance?.repaintEverything();
|
||||||
this.onNodeMoved(node);
|
this.onNodeMoved(node);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -3780,12 +3794,12 @@ export default mixins(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.resetWorkspace();
|
||||||
|
this.canvasStore.initInstance(this.$refs.nodeView as HTMLElement);
|
||||||
this.$titleReset();
|
this.$titleReset();
|
||||||
window.addEventListener('message', this.onPostMessageReceived);
|
window.addEventListener('message', this.onPostMessageReceived);
|
||||||
|
|
||||||
this.startLoading();
|
this.startLoading();
|
||||||
this.resetWorkspace();
|
|
||||||
|
|
||||||
const loadPromises = [
|
const loadPromises = [
|
||||||
this.loadActiveWorkflows(),
|
this.loadActiveWorkflows(),
|
||||||
this.loadCredentials(),
|
this.loadCredentials(),
|
||||||
|
@ -3806,8 +3820,7 @@ export default mixins(
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ready(async () => {
|
||||||
this.instance.ready(async () => {
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
this.initNodeView();
|
this.initNodeView();
|
||||||
|
@ -3879,6 +3892,7 @@ export default mixins(
|
||||||
}
|
}
|
||||||
this.uiStore.addFirstStepOnLoad = false;
|
this.uiStore.addFirstStepOnLoad = false;
|
||||||
|
|
||||||
|
this.initNodeView();
|
||||||
document.addEventListener('keydown', this.keyDown);
|
document.addEventListener('keydown', this.keyDown);
|
||||||
document.addEventListener('keyup', this.keyUp);
|
document.addEventListener('keyup', this.keyUp);
|
||||||
window.addEventListener('message', this.onPostMessageReceived);
|
window.addEventListener('message', this.onPostMessageReceived);
|
||||||
|
@ -3886,13 +3900,13 @@ export default mixins(
|
||||||
this.$root.$on('newWorkflow', this.newWorkflow);
|
this.$root.$on('newWorkflow', this.newWorkflow);
|
||||||
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
|
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||||
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||||
this.$root.$on('nodeMove', this.onMoveNode);
|
historyBus.$on('nodeMove', this.onMoveNode);
|
||||||
this.$root.$on('revertAddNode', this.onRevertAddNode);
|
historyBus.$on('revertAddNode', this.onRevertAddNode);
|
||||||
this.$root.$on('revertRemoveNode', this.onRevertRemoveNode);
|
historyBus.$on('revertRemoveNode', this.onRevertRemoveNode);
|
||||||
this.$root.$on('revertAddConnection', this.onRevertAddConnection);
|
historyBus.$on('revertAddConnection', this.onRevertAddConnection);
|
||||||
this.$root.$on('revertRemoveConnection', this.onRevertRemoveConnection);
|
historyBus.$on('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||||
this.$root.$on('revertRenameNode', this.onRevertNameChange);
|
historyBus.$on('revertRenameNode', this.onRevertNameChange);
|
||||||
this.$root.$on('enableNodeToggle', this.onRevertEnableToggle);
|
historyBus.$on('enableNodeToggle', this.onRevertEnableToggle);
|
||||||
|
|
||||||
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
||||||
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
||||||
|
@ -3908,20 +3922,23 @@ export default mixins(
|
||||||
this.$root.$off('newWorkflow', this.newWorkflow);
|
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||||
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||||
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||||
this.$root.$off('nodeMove', this.onMoveNode);
|
historyBus.$off('nodeMove', this.onMoveNode);
|
||||||
this.$root.$off('revertAddNode', this.onRevertAddNode);
|
historyBus.$off('revertAddNode', this.onRevertAddNode);
|
||||||
this.$root.$off('revertRemoveNode', this.onRevertRemoveNode);
|
historyBus.$off('revertRemoveNode', this.onRevertRemoveNode);
|
||||||
this.$root.$off('revertAddConnection', this.onRevertAddConnection);
|
historyBus.$off('revertAddConnection', this.onRevertAddConnection);
|
||||||
this.$root.$off('revertRemoveConnection', this.onRevertRemoveConnection);
|
historyBus.$off('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||||
this.$root.$off('revertRenameNode', this.onRevertNameChange);
|
historyBus.$off('revertRenameNode', this.onRevertNameChange);
|
||||||
this.$root.$off('enableNodeToggle', this.onRevertEnableToggle);
|
historyBus.$off('enableNodeToggle', this.onRevertEnableToggle);
|
||||||
|
|
||||||
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
||||||
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
||||||
nodeViewEventBus.$off('saveWorkflow', this.saveCurrentWorkflowExternal);
|
nodeViewEventBus.$off('saveWorkflow', this.saveCurrentWorkflowExternal);
|
||||||
|
this.instance.unbind();
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
this.instance.unbind();
|
||||||
|
this.instance.destroy();
|
||||||
this.uiStore.stateIsDirty = false;
|
this.uiStore.stateIsDirty = false;
|
||||||
window.removeEventListener('message', this.onPostMessageReceived);
|
window.removeEventListener('message', this.onPostMessageReceived);
|
||||||
this.$root.$off('newWorkflow', this.newWorkflow);
|
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||||
|
@ -4016,40 +4033,6 @@ export default mixins(
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.connection-run-items-label {
|
|
||||||
span {
|
|
||||||
border-radius: 7px;
|
|
||||||
background-color: hsla(
|
|
||||||
var(--color-canvas-background-h),
|
|
||||||
var(--color-canvas-background-s),
|
|
||||||
var(--color-canvas-background-l),
|
|
||||||
0.85
|
|
||||||
);
|
|
||||||
line-height: 1.3em;
|
|
||||||
padding: 0px 3px;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
font-weight: var(--font-weight-regular);
|
|
||||||
color: var(--color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating {
|
|
||||||
position: absolute;
|
|
||||||
top: -22px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-input-name-label {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
span {
|
|
||||||
position: absolute;
|
|
||||||
top: -10px;
|
|
||||||
left: -60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-add-node-label {
|
.drop-add-node-label {
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
@ -4058,30 +4041,12 @@ export default mixins(
|
||||||
background-color: #ffffff55;
|
background-color: #ffffff55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-input-endpoint-label,
|
|
||||||
.node-output-endpoint-label {
|
|
||||||
background-color: hsla(
|
|
||||||
var(--color-canvas-background-h),
|
|
||||||
var(--color-canvas-background-s),
|
|
||||||
var(--color-canvas-background-l),
|
|
||||||
0.85
|
|
||||||
);
|
|
||||||
border-radius: 7px;
|
|
||||||
font-size: 0.7em;
|
|
||||||
padding: 2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-input-endpoint-label {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-actions {
|
.connection-actions {
|
||||||
&:hover {
|
&:hover {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
> button {
|
||||||
color: var(--color-foreground-xdark);
|
color: var(--color-foreground-xdark);
|
||||||
border: 2px solid var(--color-foreground-xdark);
|
border: 2px solid var(--color-foreground-xdark);
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
|
|
|
@ -28,6 +28,7 @@ importers:
|
||||||
'@types/node': ^16.11.22
|
'@types/node': ^16.11.22
|
||||||
cross-env: ^7.0.3
|
cross-env: ^7.0.3
|
||||||
cypress: ^10.0.3
|
cypress: ^10.0.3
|
||||||
|
cypress-real-events: ^1.7.6
|
||||||
jest: ^29.3.1
|
jest: ^29.3.1
|
||||||
jest-environment-jsdom: ^29.3.1
|
jest-environment-jsdom: ^29.3.1
|
||||||
jest-mock: ^29.3.1
|
jest-mock: ^29.3.1
|
||||||
|
@ -52,6 +53,7 @@ importers:
|
||||||
'@types/node': 16.11.65
|
'@types/node': 16.11.65
|
||||||
cross-env: 7.0.3
|
cross-env: 7.0.3
|
||||||
cypress: 10.11.0
|
cypress: 10.11.0
|
||||||
|
cypress-real-events: 1.7.6_cypress@10.11.0
|
||||||
jest: 29.3.1_@types+node@16.11.65
|
jest: 29.3.1_@types+node@16.11.65
|
||||||
jest-environment-jsdom: 29.3.1
|
jest-environment-jsdom: 29.3.1
|
||||||
jest-mock: 29.3.1
|
jest-mock: 29.3.1
|
||||||
|
@ -532,6 +534,11 @@ importers:
|
||||||
'@fortawesome/free-regular-svg-icons': ^6.1.1
|
'@fortawesome/free-regular-svg-icons': ^6.1.1
|
||||||
'@fortawesome/free-solid-svg-icons': ^5.15.3
|
'@fortawesome/free-solid-svg-icons': ^5.15.3
|
||||||
'@fortawesome/vue-fontawesome': ^2.0.2
|
'@fortawesome/vue-fontawesome': ^2.0.2
|
||||||
|
'@jsplumb/browser-ui': ^5.13.2
|
||||||
|
'@jsplumb/common': ^5.13.2
|
||||||
|
'@jsplumb/connector-bezier': ^5.13.2
|
||||||
|
'@jsplumb/core': ^5.13.2
|
||||||
|
'@jsplumb/util': ^5.13.2
|
||||||
'@pinia/testing': ^0.0.14
|
'@pinia/testing': ^0.0.14
|
||||||
'@testing-library/jest-dom': ^5.16.5
|
'@testing-library/jest-dom': ^5.16.5
|
||||||
'@testing-library/vue': ^5.8.3
|
'@testing-library/vue': ^5.8.3
|
||||||
|
@ -561,7 +568,6 @@ importers:
|
||||||
jquery: ^3.4.1
|
jquery: ^3.4.1
|
||||||
jshint: ^2.9.7
|
jshint: ^2.9.7
|
||||||
jsonpath: ^1.1.1
|
jsonpath: ^1.1.1
|
||||||
jsplumb: 2.15.4
|
|
||||||
lodash-es: ^4.17.21
|
lodash-es: ^4.17.21
|
||||||
lodash.camelcase: ^4.3.0
|
lodash.camelcase: ^4.3.0
|
||||||
lodash.debounce: ^4.0.8
|
lodash.debounce: ^4.0.8
|
||||||
|
@ -614,6 +620,11 @@ importers:
|
||||||
'@fortawesome/free-regular-svg-icons': 6.2.0
|
'@fortawesome/free-regular-svg-icons': 6.2.0
|
||||||
'@fortawesome/free-solid-svg-icons': 5.15.4
|
'@fortawesome/free-solid-svg-icons': 5.15.4
|
||||||
'@fortawesome/vue-fontawesome': 2.0.8_dh3wzfumpzw6zsszdpw5cxouqy
|
'@fortawesome/vue-fontawesome': 2.0.8_dh3wzfumpzw6zsszdpw5cxouqy
|
||||||
|
'@jsplumb/browser-ui': 5.13.2
|
||||||
|
'@jsplumb/common': 5.13.2
|
||||||
|
'@jsplumb/connector-bezier': 5.13.2
|
||||||
|
'@jsplumb/core': 5.13.2
|
||||||
|
'@jsplumb/util': 5.13.2
|
||||||
axios: 0.21.4
|
axios: 0.21.4
|
||||||
codemirror-lang-html-n8n: 1.0.0
|
codemirror-lang-html-n8n: 1.0.0
|
||||||
codemirror-lang-n8n-expression: 0.1.0_zyklskjzaprvz25ee7sq7godcq
|
codemirror-lang-n8n-expression: 0.1.0_zyklskjzaprvz25ee7sq7godcq
|
||||||
|
@ -625,7 +636,6 @@ importers:
|
||||||
humanize-duration: 3.27.3
|
humanize-duration: 3.27.3
|
||||||
jquery: 3.6.1
|
jquery: 3.6.1
|
||||||
jsonpath: 1.1.1
|
jsonpath: 1.1.1
|
||||||
jsplumb: 2.15.4
|
|
||||||
lodash-es: 4.17.21
|
lodash-es: 4.17.21
|
||||||
lodash.camelcase: 4.3.0
|
lodash.camelcase: 4.3.0
|
||||||
lodash.debounce: 4.0.8
|
lodash.debounce: 4.0.8
|
||||||
|
@ -2071,7 +2081,7 @@ packages:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.20.12
|
'@babel/core': 7.20.12
|
||||||
'@babel/helper-plugin-utils': 7.20.2
|
'@babel/helper-plugin-utils': 7.19.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.19.3:
|
/@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.19.3:
|
||||||
|
@ -3711,6 +3721,34 @@ packages:
|
||||||
/@jsdevtools/ono/7.1.3:
|
/@jsdevtools/ono/7.1.3:
|
||||||
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
||||||
|
|
||||||
|
/@jsplumb/browser-ui/5.13.2:
|
||||||
|
resolution: {integrity: sha512-BZ76kPtxESMIdhcCtWXPdICMudJyBVzDxaKY4jlne93Zq1T2ErfpNQ3E6f3JZfvoyvlNbKgh0udYkZ7Yg7BmIQ==}
|
||||||
|
dependencies:
|
||||||
|
'@jsplumb/core': 5.13.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@jsplumb/common/5.13.2:
|
||||||
|
resolution: {integrity: sha512-ZX/EvvYi4HBkRVtsuSSAa/AuAz4p2wr3RrRz6l+r8yeElzX3lrrBx/fkERY2qwZPkKcOoLCr5ezZ7sslVMnl0Q==}
|
||||||
|
dependencies:
|
||||||
|
'@jsplumb/util': 5.13.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@jsplumb/connector-bezier/5.13.2:
|
||||||
|
resolution: {integrity: sha512-AALmOvkiP3ouGag6TGkBcd7SbCewPNwsKu9gku9AZqIq+fFu321zJ2IpfoyCFgkoFFSQjJ9jo1sWBbD3gnEXrg==}
|
||||||
|
dependencies:
|
||||||
|
'@jsplumb/core': 5.13.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@jsplumb/core/5.13.2:
|
||||||
|
resolution: {integrity: sha512-IODXQzhpq9QEzGKhPir6+ea8m4KeU3gzJsYjIu8oqSQ4jDhvEYF7TvSfeaNgy9sUAMt3OoKCqxCS4ga9J7LS5A==}
|
||||||
|
dependencies:
|
||||||
|
'@jsplumb/common': 5.13.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@jsplumb/util/5.13.2:
|
||||||
|
resolution: {integrity: sha512-POrqlZMOo821oa49Xbxb+pNmnxu0z2oS7FOeklRxKuYXR+7nsP0j9PpXjo8E8Ily4TaP+pdUnatb53vAaONO3g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@kafkajs/confluent-schema-registry/1.0.6:
|
/@kafkajs/confluent-schema-registry/1.0.6:
|
||||||
resolution: {integrity: sha512-NrZL1peOIlmlLKvheQcJAx9PHdnc4kaW+9+Yt4jXUfbbYR9EFNCZt6yApI4SwlFilaiZieReM6XslWy1LZAvoQ==}
|
resolution: {integrity: sha512-NrZL1peOIlmlLKvheQcJAx9PHdnc4kaW+9+Yt4jXUfbbYR9EFNCZt6yApI4SwlFilaiZieReM6XslWy1LZAvoQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -10253,6 +10291,14 @@ packages:
|
||||||
resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
|
resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cypress-real-events/1.7.6_cypress@10.11.0:
|
||||||
|
resolution: {integrity: sha512-yP6GnRrbm6HK5q4DH6Nnupz37nOfZu/xn1xFYqsE2o4G73giPWQOdu6375QYpwfU1cvHNCgyD2bQ2hPH9D7NMw==}
|
||||||
|
peerDependencies:
|
||||||
|
cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x
|
||||||
|
dependencies:
|
||||||
|
cypress: 10.11.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/cypress/10.11.0:
|
/cypress/10.11.0:
|
||||||
resolution: {integrity: sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==}
|
resolution: {integrity: sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
@ -15088,10 +15134,6 @@ packages:
|
||||||
semver: 7.3.8
|
semver: 7.3.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/jsplumb/2.15.4:
|
|
||||||
resolution: {integrity: sha512-QssfhXe0YRxY4V2WHPmKwsE3bPHNj4Vts9oinys66ci+4m9lJvFDcEMDygqueiSFL8Jb8CnFyQC9fvL+YHJS7g==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/jsprim/1.4.2:
|
/jsprim/1.4.2:
|
||||||
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
|
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
|
||||||
engines: {node: '>=0.6.0'}
|
engines: {node: '>=0.6.0'}
|
||||||
|
|
Loading…
Reference in a new issue