mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08: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(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
||||
cy.get('.connection-actions .add').invoke('show');
|
||||
cy.get('.connection-actions .add').should('be.visible');
|
||||
cy.get('.connection-actions .add').click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().realHover();
|
||||
cy.get('.connection-actions .add').filter(':visible').click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME, false);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.hitUndo();
|
||||
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', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
||||
cy.get('.connection-actions .delete').click();
|
||||
WorkflowPage.getters.nodeConnections().realHover();
|
||||
cy.get('.connection-actions .delete').filter(':visible').should('be.visible').click();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
|
|
|
@ -61,11 +61,9 @@ describe('Canvas Actions', () => {
|
|||
it('should add note between two connected nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
||||
cy.get('.connection-actions .add').as('AddNodeConnectionButton');
|
||||
cy.get('@AddNodeConnectionButton').invoke('show');
|
||||
cy.get('@AddNodeConnectionButton').should('be.visible').click();
|
||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||
cy.get('.connection-actions .add').filter(':visible').should('be.visible').click()
|
||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, false);
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
||||
// 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', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().first().trigger('mouseover', { force: true });
|
||||
cy.get('.connection-actions .delete').click();
|
||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||
cy.get('.connection-actions .delete').filter(':visible').should('be.visible').click();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ describe('Credentials', () => {
|
|||
it('should correctly render required and optional credentials', () => {
|
||||
workflowPage.actions.visit();
|
||||
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('{enter}');
|
||||
// Select incoming authentication
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('NDV', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
cy.get('.has-issues').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', () => {
|
||||
workflowPage.actions.addNodeToCanvas('Manual Trigger');
|
||||
workflowPage.actions.addNodeToCanvas('Airtable', true);
|
||||
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
|
||||
ndv.getters.container().should('be.visible');
|
||||
cy.get('.has-issues').should('have.length', 0);
|
||||
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
|
||||
|
|
|
@ -92,8 +92,11 @@ export class WorkflowPage extends BasePage {
|
|||
this.getters.nodeCreatorSearchBar().type('{enter}');
|
||||
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.nodeCreatorSearchBar().type(nodeDisplayName);
|
||||
this.getters.nodeCreatorSearchBar().type('{enter}');
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
import "cypress-real-events";
|
||||
import { WorkflowsPage, SigninPage, SignupPage } from '../pages';
|
||||
import { N8N_AUTH_COOKIE } from '../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"@types/node": "^16.11.22",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^10.0.3",
|
||||
"cypress-real-events": "^1.7.6",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"jest-mock": "^29.3.1",
|
||||
|
|
|
@ -39,6 +39,11 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@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",
|
||||
"codemirror-lang-html-n8n": "^1.0.0",
|
||||
"codemirror-lang-n8n-expression": "^0.1.0",
|
||||
|
@ -50,7 +55,6 @@
|
|||
"humanize-duration": "^3.27.2",
|
||||
"jquery": "^3.4.1",
|
||||
"jsonpath": "^1.1.1",
|
||||
"jsplumb": "2.15.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
|
|
|
@ -1,17 +1,6 @@
|
|||
import { CREDENTIAL_EDIT_MODAL_KEY } from './constants';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { IMenuItem } from 'n8n-design-system';
|
||||
import {
|
||||
jsPlumbInstance,
|
||||
DragOptions,
|
||||
DropOptions,
|
||||
ElementGroupRef,
|
||||
Endpoint,
|
||||
EndpointOptions,
|
||||
EndpointRectangle,
|
||||
EndpointRectangleOptions,
|
||||
EndpointSpec,
|
||||
} from 'jsplumb';
|
||||
import {
|
||||
GenericValue,
|
||||
IConnections,
|
||||
|
@ -47,98 +36,6 @@ import { BulkCommand, Undoable } from '@/models/history';
|
|||
|
||||
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 = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
@ -152,21 +49,6 @@ export type EndpointStyle = {
|
|||
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 {
|
||||
name: string;
|
||||
key?: string;
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
<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="{
|
||||
|
@ -7,8 +14,6 @@
|
|||
'touch-active': isTouchActive,
|
||||
'is-touch-device': isTouchDevice,
|
||||
}"
|
||||
:data-name="data.name"
|
||||
:ref="data.name"
|
||||
>
|
||||
<div
|
||||
:class="nodeClass"
|
||||
|
@ -515,7 +520,6 @@ export default mixins(
|
|||
this.data.name,
|
||||
!this.data.disabled,
|
||||
this.data.disabled === true,
|
||||
this,
|
||||
),
|
||||
);
|
||||
this.$telemetry.track('User clicked node hover button', {
|
||||
|
@ -698,6 +702,7 @@ export default mixins(
|
|||
|
||||
.items-count {
|
||||
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 */
|
||||
.jtk-connector {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.jtk-floating-endpoint {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.jtk-connector path {
|
||||
transition: stroke 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.jtk-connector.success {
|
||||
.jtk-overlay {
|
||||
z-index: 3;
|
||||
}
|
||||
.jtk-connector {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.node-input-endpoint-label,
|
||||
.node-output-endpoint-label,
|
||||
.connection-run-items-label {
|
||||
z-index: 5;
|
||||
}
|
||||
.jtk-connector.jtk-hover {
|
||||
z-index: 6;
|
||||
}
|
||||
|
@ -828,30 +846,25 @@ export default mixins(
|
|||
.jtk-endpoint.plus-endpoint {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.jtk-endpoint.dot-output-endpoint {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.jtk-overlay {
|
||||
z-index: 7;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.disabled-linethrough {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.jtk-connector.jtk-dragging {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.jtk-drag-active.dot-output-endpoint,
|
||||
.jtk-drag-active.rect-input-endpoint {
|
||||
z-index: 9;
|
||||
}
|
||||
.rect-input-endpoint > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
z-index: 10;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.node-options {
|
||||
|
@ -861,88 +874,207 @@ export default mixins(
|
|||
.drop-add-node-label {
|
||||
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 lang="scss">
|
||||
$--stalklength: 40px;
|
||||
$--box-size-medium: 24px;
|
||||
$--box-size-small: 18px;
|
||||
:root {
|
||||
--endpoint-size-small: 14px;
|
||||
--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 {
|
||||
cursor: pointer;
|
||||
|
||||
.plus-stalk {
|
||||
border-top: 2px solid var(--color-foreground-dark);
|
||||
position: absolute;
|
||||
width: $--stalklength;
|
||||
height: 0;
|
||||
right: 100%;
|
||||
top: calc(50% - 1px);
|
||||
z-index: 10;
|
||||
margin-left: calc((var(--stalk-size) + var(--plus-endpoint-box-size) / 2) - 1px);
|
||||
g {
|
||||
fill: var(--color-background-xlight);
|
||||
pointer-events: none;
|
||||
|
||||
.connection-run-items-label {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
left: calc(50% + 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plus-container {
|
||||
color: var(--color-foreground-xdark);
|
||||
border: 2px solid var(--color-foreground-xdark);
|
||||
background-color: var(--color-background-xlight);
|
||||
border-radius: var(--border-radius-base);
|
||||
height: $--box-size-medium;
|
||||
width: $--box-size-medium;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-2xs);
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
&:hover {
|
||||
path {
|
||||
fill: var(--color-primary);
|
||||
}
|
||||
rect {
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
}
|
||||
path {
|
||||
fill: var(--color-foreground-xdark);
|
||||
}
|
||||
rect {
|
||||
stroke: var(--color-foreground-xdark);
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: $--box-size-small;
|
||||
width: $--box-size-small;
|
||||
font-size: 8px;
|
||||
margin-left: calc((var(--stalk-size) + var(--plus-endpoint-box-size-small) / 2));
|
||||
g {
|
||||
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 {
|
||||
width: 1em;
|
||||
}
|
||||
&.hidden {
|
||||
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-size: var(--font-size-2xs);
|
||||
line-height: var(--font-line-height-regular);
|
||||
color: var(--color-text-light);
|
||||
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: calc(100% + 8px);
|
||||
width: 200px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hidden > * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.success .plus-stalk {
|
||||
border-color: var(--color-success-light);
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
width: var(--hover-message-width);
|
||||
margin-left: calc(
|
||||
(var(--hover-message-width) / 2) + var(--stalk-size) + var(--plus-endpoint-box-size) +
|
||||
var(--spacing-2xs)
|
||||
);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
&.small {
|
||||
margin-left: calc(
|
||||
(var(--hover-message-width) / 2) + var(--stalk-size) + var(--plus-endpoint-box-size-small) +
|
||||
var(--spacing-2xs)
|
||||
);
|
||||
}
|
||||
&.visible {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.ep-success {
|
||||
--stalk-size: var(--stalk-success-size);
|
||||
}
|
||||
.long-stalk {
|
||||
--stalk-size: var(--stalk-long-size);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -549,7 +549,7 @@ export default mixins(externalHooks, nodeHelpers).extend({
|
|||
},
|
||||
nameChanged(name: string) {
|
||||
if (this.node) {
|
||||
this.historyStore.pushCommandToUndo(new RenameNodeCommand(this.node.name, name, this));
|
||||
this.historyStore.pushCommandToUndo(new RenameNodeCommand(this.node.name, name));
|
||||
}
|
||||
// @ts-ignore
|
||||
this.valueChanged({
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<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
|
||||
:class="{
|
||||
'sticky-default': true,
|
||||
|
@ -11,8 +17,6 @@
|
|||
<div class="select-sticky-background" v-show="isSelected" />
|
||||
<div
|
||||
class="sticky-box"
|
||||
:data-name="data.name"
|
||||
:ref="data.name"
|
||||
@click.left="mouseLeftClick"
|
||||
v-touch:start="touchStart"
|
||||
v-touch:end="touchEnd"
|
||||
|
@ -186,9 +190,6 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
if (!this.isSelected && this.node) {
|
||||
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 }) {
|
||||
if (!this.node) {
|
||||
|
@ -202,7 +203,6 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
},
|
||||
onResizeEnd() {
|
||||
this.isResizing = false;
|
||||
this.__makeInstanceDraggable(this.data);
|
||||
},
|
||||
setParameters(params: { content?: string; height?: number; width?: number }) {
|
||||
if (this.node) {
|
||||
|
@ -252,8 +252,6 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
position: absolute;
|
||||
|
||||
.sticky-default {
|
||||
position: absolute;
|
||||
|
||||
.sticky-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'prismjs';
|
|||
import 'prismjs/themes/prism.css';
|
||||
import 'vue-prism-editor/dist/VuePrismEditor.css';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
|
||||
import '@jsplumb/browser-ui/css/jsplumbtoolkit.css';
|
||||
import 'n8n-design-system/css/index.scss';
|
||||
import './n8n-theme.scss';
|
||||
|
||||
|
|
|
@ -203,12 +203,12 @@ export const mouseSelect = mixins(deviceSupportHelpers).extend({
|
|||
nodeDeselected(node: INodeUi) {
|
||||
this.uiStore.removeNodeFromSelection(node);
|
||||
// @ts-ignore
|
||||
this.instance.removeFromDragSelection(node.id);
|
||||
this.instance.removeFromDragSelection(this.$refs[`node-${node.id}`][0].$el);
|
||||
},
|
||||
nodeSelected(node: INodeUi) {
|
||||
this.uiStore.addSelectedNode(node);
|
||||
// @ts-ignore
|
||||
this.instance.addToDragSelection(node.id);
|
||||
this.instance.addToDragSelection(this.$refs[`node-${node.id}`][0].$el);
|
||||
},
|
||||
deselectAllNodes() {
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { PropType } from 'vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { IJsPlumbInstance, IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
|
||||
import { INodeUi } from '@/Interface';
|
||||
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 { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||
import { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||
import { EndpointOptions } from '@jsplumb/core';
|
||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||
import { getStyleTokenValue } from '@/utils';
|
||||
import { useHistoryStore } from '@/stores/history';
|
||||
import { MoveNodeCommand } from '@/models/history';
|
||||
import { useCanvasStore } from '@/stores/canvas';
|
||||
|
||||
export const nodeBase = mixins(deviceSupportHelpers).extend({
|
||||
mounted() {
|
||||
|
@ -27,7 +28,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNodeTypesStore, useUIStore, useWorkflowsStore, useHistoryStore),
|
||||
...mapStores(useNodeTypesStore, useUIStore, useCanvasStore, useWorkflowsStore, useHistoryStore),
|
||||
data(): INodeUi | null {
|
||||
return this.workflowsStore.getNodeByName(this.name);
|
||||
},
|
||||
|
@ -40,7 +41,7 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
type: String,
|
||||
},
|
||||
instance: {
|
||||
type: Object as PropType<IJsPlumbInstance>,
|
||||
type: Object as PropType<BrowserJsPlumbInstance>,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
|
@ -79,18 +80,15 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
const anchorPosition =
|
||||
NodeViewUtils.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
|
||||
|
||||
const newEndpointData: IEndpointOptions = {
|
||||
const newEndpointData: EndpointOptions = {
|
||||
uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: -1,
|
||||
endpoint: 'Rectangle',
|
||||
endpointStyle: NodeViewUtils.getInputEndpointStyle(
|
||||
nodeTypeData,
|
||||
'--color-foreground-xdark',
|
||||
),
|
||||
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,
|
||||
paintStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
|
||||
hoverPaintStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||
source: false,
|
||||
target: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
|
||||
parameters: {
|
||||
nodeId: this.nodeId,
|
||||
type: inputName,
|
||||
|
@ -99,20 +97,17 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
enabled: !this.isReadOnly, // enabled in default case to allow dragging
|
||||
cssClass: 'rect-input-endpoint',
|
||||
dragAllowedWhenFull: true,
|
||||
dropOptions: {
|
||||
tolerance: 'touch',
|
||||
hoverClass: 'dropHover',
|
||||
},
|
||||
};
|
||||
|
||||
const endpoint = this.instance?.addEndpoint(
|
||||
this.$refs[this.data.name] as Element,
|
||||
newEndpointData,
|
||||
);
|
||||
if (nodeTypeData.inputNames) {
|
||||
// Apply input names if they got set
|
||||
newEndpointData.overlays = [
|
||||
NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]),
|
||||
];
|
||||
endpoint.addOverlay(NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]));
|
||||
}
|
||||
|
||||
const endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
|
||||
if (!Array.isArray(endpoint)) {
|
||||
endpoint.__meta = {
|
||||
nodeName: node.name,
|
||||
|
@ -133,6 +128,9 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
// 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) {
|
||||
let index;
|
||||
|
@ -153,37 +151,46 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
const anchorPosition =
|
||||
NodeViewUtils.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
|
||||
|
||||
const newEndpointData: IEndpointOptions = {
|
||||
const newEndpointData: EndpointOptions = {
|
||||
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: -1,
|
||||
endpoint: 'Dot',
|
||||
endpointStyle: NodeViewUtils.getOutputEndpointStyle(
|
||||
|
||||
endpoint: {
|
||||
type: 'Dot',
|
||||
options: {
|
||||
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||
},
|
||||
},
|
||||
paintStyle: NodeViewUtils.getOutputEndpointStyle(
|
||||
nodeTypeData,
|
||||
'--color-foreground-xdark',
|
||||
),
|
||||
endpointHoverStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||
isSource: true,
|
||||
isTarget: false,
|
||||
hoverPaintStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
|
||||
source: true,
|
||||
target: false,
|
||||
enabled: !this.isReadOnly,
|
||||
parameters: {
|
||||
nodeId: this.nodeId,
|
||||
type: inputName,
|
||||
index,
|
||||
},
|
||||
hoverClass: 'dot-output-endpoint-hover',
|
||||
connectionsDirected: true,
|
||||
cssClass: 'dot-output-endpoint',
|
||||
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) {
|
||||
// Apply output names if they got set
|
||||
newEndpointData.overlays = [
|
||||
NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]),
|
||||
];
|
||||
const overlaySpec = NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]);
|
||||
const overlay = endpoint.addOverlay(overlaySpec);
|
||||
}
|
||||
|
||||
const endpoint = this.instance.addEndpoint(this.nodeId, { ...newEndpointData });
|
||||
if (!Array.isArray(endpoint)) {
|
||||
endpoint.__meta = {
|
||||
nodeName: node.name,
|
||||
|
@ -194,26 +201,28 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
}
|
||||
|
||||
if (!this.isReadOnly) {
|
||||
const plusEndpointData: IEndpointOptions = {
|
||||
const plusEndpointData: EndpointOptions = {
|
||||
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
|
||||
anchor: anchorPosition,
|
||||
maxConnections: -1,
|
||||
endpoint: 'N8nPlus',
|
||||
isSource: true,
|
||||
isTarget: false,
|
||||
enabled: !this.isReadOnly,
|
||||
endpointStyle: {
|
||||
fill: getStyleTokenValue('--color-xdark'),
|
||||
outlineStroke: 'none',
|
||||
hover: false,
|
||||
endpoint: {
|
||||
type: 'N8nPlus',
|
||||
options: {
|
||||
dimensions: 24,
|
||||
connectedEndpoint: endpoint,
|
||||
showOutputLabel: nodeTypeData.outputs.length === 1,
|
||||
size: nodeTypeData.outputs.length >= 3 ? 'small' : 'medium',
|
||||
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
|
||||
},
|
||||
endpointHoverStyle: {
|
||||
fill: getStyleTokenValue('--color-primary'),
|
||||
},
|
||||
source: true,
|
||||
target: false,
|
||||
enabled: !this.isReadOnly,
|
||||
paintStyle: {
|
||||
outlineStroke: 'none',
|
||||
},
|
||||
hoverPaintStyle: {
|
||||
outlineStroke: 'none',
|
||||
hover: true, // hack to distinguish hover state
|
||||
},
|
||||
parameters: {
|
||||
nodeId: this.nodeId,
|
||||
|
@ -222,10 +231,12 @@ export const nodeBase = mixins(deviceSupportHelpers).extend({
|
|||
},
|
||||
cssClass: 'plus-draggable-endpoint',
|
||||
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)) {
|
||||
plusEndpoint.__meta = {
|
||||
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) {
|
||||
let nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
if (!nodeTypeData) {
|
||||
// If node type is not know use by default the base.noOp data to display it
|
||||
nodeTypeData = this.nodeTypesStore.getNodeType(NO_OP_NODE_TYPE);
|
||||
}
|
||||
const nodeTypeData = (this.nodeTypesStore.getNodeType(node.type, node.typeVersion) ??
|
||||
this.nodeTypesStore.getNodeType(NO_OP_NODE_TYPE)) as INodeTypeDescription;
|
||||
|
||||
this.__addInputEndpoints(node, nodeTypeData);
|
||||
this.__addOutputEndpoints(node, nodeTypeData);
|
||||
this.__makeInstanceDraggable(node);
|
||||
},
|
||||
touchEnd(e: MouseEvent) {
|
||||
if (this.isTouchDevice) {
|
||||
|
|
|
@ -505,7 +505,7 @@ export const nodeHelpers = mixins(restApi).extend({
|
|||
this.updateNodeCredentialIssues(node);
|
||||
if (trackHistory) {
|
||||
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
|
||||
// (0 is usually enough but leaving this just in case)
|
||||
const CANVAS_ACTION_TIMEOUT = 10;
|
||||
export const historyBus = new Vue();
|
||||
|
||||
export abstract class Undoable {}
|
||||
|
||||
export abstract class Command extends Undoable {
|
||||
readonly name: string;
|
||||
eventBus: Vue;
|
||||
|
||||
constructor(name: string, eventBus: Vue) {
|
||||
constructor(name: string) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
abstract getReverseCommand(): Command;
|
||||
abstract isEqualTo(anotherCommand: Command): boolean;
|
||||
|
@ -52,15 +51,15 @@ export class MoveNodeCommand extends Command {
|
|||
oldPosition: XYPosition;
|
||||
newPosition: XYPosition;
|
||||
|
||||
constructor(nodeName: string, oldPosition: XYPosition, newPosition: XYPosition, eventBus: Vue) {
|
||||
super(COMMANDS.MOVE_NODE, eventBus);
|
||||
constructor(nodeName: string, oldPosition: XYPosition, newPosition: XYPosition) {
|
||||
super(COMMANDS.MOVE_NODE);
|
||||
this.nodeName = nodeName;
|
||||
this.newPosition = newPosition;
|
||||
this.oldPosition = oldPosition;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -76,7 +75,7 @@ export class MoveNodeCommand extends Command {
|
|||
|
||||
async revert(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.eventBus.$root.$emit('nodeMove', {
|
||||
historyBus.$emit('nodeMove', {
|
||||
nodeName: this.nodeName,
|
||||
position: this.oldPosition,
|
||||
});
|
||||
|
@ -88,13 +87,13 @@ export class MoveNodeCommand extends Command {
|
|||
export class AddNodeCommand extends Command {
|
||||
node: INodeUi;
|
||||
|
||||
constructor(node: INodeUi, eventBus: Vue) {
|
||||
super(COMMANDS.ADD_NODE, eventBus);
|
||||
constructor(node: INodeUi) {
|
||||
super(COMMANDS.ADD_NODE);
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
getReverseCommand(): Command {
|
||||
return new RemoveNodeCommand(this.node, this.eventBus);
|
||||
return new RemoveNodeCommand(this.node);
|
||||
}
|
||||
|
||||
isEqualTo(anotherCommand: Command): boolean {
|
||||
|
@ -103,7 +102,7 @@ export class AddNodeCommand extends Command {
|
|||
|
||||
async revert(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.eventBus.$root.$emit('revertAddNode', { node: this.node });
|
||||
historyBus.$emit('revertAddNode', { node: this.node });
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
@ -112,13 +111,13 @@ export class AddNodeCommand extends Command {
|
|||
export class RemoveNodeCommand extends Command {
|
||||
node: INodeUi;
|
||||
|
||||
constructor(node: INodeUi, eventBus: Vue) {
|
||||
super(COMMANDS.REMOVE_NODE, eventBus);
|
||||
constructor(node: INodeUi) {
|
||||
super(COMMANDS.REMOVE_NODE);
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
getReverseCommand(): Command {
|
||||
return new AddNodeCommand(this.node, this.eventBus);
|
||||
return new AddNodeCommand(this.node);
|
||||
}
|
||||
|
||||
isEqualTo(anotherCommand: Command): boolean {
|
||||
|
@ -127,7 +126,7 @@ export class RemoveNodeCommand extends Command {
|
|||
|
||||
async revert(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.eventBus.$root.$emit('revertRemoveNode', { node: this.node });
|
||||
historyBus.$emit('revertRemoveNode', { node: this.node });
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
@ -136,13 +135,13 @@ export class RemoveNodeCommand extends Command {
|
|||
export class AddConnectionCommand extends Command {
|
||||
connectionData: [IConnection, IConnection];
|
||||
|
||||
constructor(connectionData: [IConnection, IConnection], eventBus: Vue) {
|
||||
super(COMMANDS.ADD_CONNECTION, eventBus);
|
||||
constructor(connectionData: [IConnection, IConnection]) {
|
||||
super(COMMANDS.ADD_CONNECTION);
|
||||
this.connectionData = connectionData;
|
||||
}
|
||||
|
||||
getReverseCommand(): Command {
|
||||
return new RemoveConnectionCommand(this.connectionData, this.eventBus);
|
||||
return new RemoveConnectionCommand(this.connectionData);
|
||||
}
|
||||
|
||||
isEqualTo(anotherCommand: Command): boolean {
|
||||
|
@ -157,7 +156,7 @@ export class AddConnectionCommand extends Command {
|
|||
|
||||
async revert(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.eventBus.$root.$emit('revertAddConnection', { connection: this.connectionData });
|
||||
historyBus.$emit('revertAddConnection', { connection: this.connectionData });
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
@ -166,13 +165,13 @@ export class AddConnectionCommand extends Command {
|
|||
export class RemoveConnectionCommand extends Command {
|
||||
connectionData: [IConnection, IConnection];
|
||||
|
||||
constructor(connectionData: [IConnection, IConnection], eventBus: Vue) {
|
||||
super(COMMANDS.REMOVE_CONNECTION, eventBus);
|
||||
constructor(connectionData: [IConnection, IConnection]) {
|
||||
super(COMMANDS.REMOVE_CONNECTION);
|
||||
this.connectionData = connectionData;
|
||||
}
|
||||
|
||||
getReverseCommand(): Command {
|
||||
return new AddConnectionCommand(this.connectionData, this.eventBus);
|
||||
return new AddConnectionCommand(this.connectionData);
|
||||
}
|
||||
|
||||
isEqualTo(anotherCommand: Command): boolean {
|
||||
|
@ -188,7 +187,7 @@ export class RemoveConnectionCommand extends Command {
|
|||
async revert(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.eventBus.$root.$emit('revertRemoveConnection', { connection: this.connectionData });
|
||||
historyBus.$emit('revertRemoveConnection', { connection: this.connectionData });
|
||||
resolve();
|
||||
}, CANVAS_ACTION_TIMEOUT);
|
||||
});
|
||||
|
@ -200,15 +199,15 @@ export class EnableNodeToggleCommand extends Command {
|
|||
oldState: boolean;
|
||||
newState: boolean;
|
||||
|
||||
constructor(nodeName: string, oldState: boolean, newState: boolean, eventBus: Vue) {
|
||||
super(COMMANDS.ENABLE_NODE_TOGGLE, eventBus);
|
||||
constructor(nodeName: string, oldState: boolean, newState: boolean) {
|
||||
super(COMMANDS.ENABLE_NODE_TOGGLE);
|
||||
this.nodeName = nodeName;
|
||||
this.newState = newState;
|
||||
this.oldState = oldState;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -219,7 +218,7 @@ export class EnableNodeToggleCommand extends Command {
|
|||
|
||||
async revert(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.eventBus.$root.$emit('enableNodeToggle', {
|
||||
historyBus.$emit('enableNodeToggle', {
|
||||
nodeName: this.nodeName,
|
||||
isDisabled: this.oldState,
|
||||
});
|
||||
|
@ -232,14 +231,14 @@ export class RenameNodeCommand extends Command {
|
|||
currentName: string;
|
||||
newName: string;
|
||||
|
||||
constructor(currentName: string, newName: string, eventBus: Vue) {
|
||||
super(COMMANDS.RENAME_NODE, eventBus);
|
||||
constructor(currentName: string, newName: string) {
|
||||
super(COMMANDS.RENAME_NODE);
|
||||
this.currentName = currentName;
|
||||
this.newName = newName;
|
||||
}
|
||||
|
||||
getReverseCommand(): Command {
|
||||
return new RenameNodeCommand(this.newName, this.currentName, this.eventBus);
|
||||
return new RenameNodeCommand(this.newName, this.currentName);
|
||||
}
|
||||
|
||||
isEqualTo(anotherCommand: Command): boolean {
|
||||
|
@ -252,7 +251,7 @@ export class RenameNodeCommand extends Command {
|
|||
|
||||
async revert(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
this.eventBus.$root.$emit('revertRenameNode', {
|
||||
historyBus.$emit('revertRenameNode', {
|
||||
currentName: this.currentName,
|
||||
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 { defineStore } from 'pinia';
|
||||
import { jsPlumb } from 'jsplumb';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import normalizeWheel from 'normalize-wheel';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
import { useHistoryStore } from '@/stores/history';
|
||||
import { INodeUi, XYPosition } from '@/Interface';
|
||||
import { scaleBigger, scaleReset, scaleSmaller } from '@/utils';
|
||||
import { START_NODE_TYPE } from '@/constants';
|
||||
import '@/plugins/N8nCustomConnectorType';
|
||||
import '@/plugins/PlusEndpointType';
|
||||
import { START_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
||||
import type {
|
||||
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 {
|
||||
DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
|
||||
getMidCanvasPosition,
|
||||
getNewNodePosition,
|
||||
getZoomToFit,
|
||||
PLACEHOLDER_TRIGGER_NODE_SIZE,
|
||||
CONNECTOR_FLOWCHART_TYPE,
|
||||
GRID_SIZE,
|
||||
} from '@/utils/nodeViewUtils';
|
||||
import { PointXY } from '@jsplumb/util';
|
||||
|
||||
export const useCanvasStore = defineStore('canvas', () => {
|
||||
const workflowStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
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 triggerNodes = computed<INodeUi[]>(() =>
|
||||
|
@ -35,6 +50,10 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||
const nodeViewScale = ref<number>(1);
|
||||
const canvasAddButtonPosition = ref<XYPosition>([1, 1]);
|
||||
|
||||
Connectors.register(N8nConnector.type, N8nConnector);
|
||||
N8nPlusEndpointRenderer.register();
|
||||
EndpointFactory.registerHandler(N8nPlusEndpointHandler);
|
||||
|
||||
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
|
||||
const position = getMidCanvasPosition(nodeViewScale.value, offset || [0, 0]);
|
||||
|
||||
|
@ -59,7 +78,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||
|
||||
const setZoomLevel = (zoomLevel: number, offset: XYPosition) => {
|
||||
nodeViewScale.value = zoomLevel;
|
||||
jsPlumbInstance.setZoom(zoomLevel);
|
||||
jsPlumbInstance.value?.setZoom(zoomLevel);
|
||||
uiStore.nodeViewOffsetPosition = offset;
|
||||
};
|
||||
|
||||
|
@ -122,8 +141,106 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||
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 {
|
||||
jsPlumbInstance,
|
||||
isDemo,
|
||||
nodeViewScale,
|
||||
canvasAddButtonPosition,
|
||||
|
@ -135,5 +252,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||
zoomOut,
|
||||
zoomToFit,
|
||||
wheelScroll,
|
||||
initInstance,
|
||||
jsPlumbInstance,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -251,6 +251,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
return (id: string) =>
|
||||
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
|
||||
},
|
||||
isReadOnlyView(): boolean {
|
||||
return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW].includes(this.currentView as VIEWS);
|
||||
},
|
||||
isNodeView(): boolean {
|
||||
return [
|
||||
VIEWS.NEW_WORKFLOW.toString(),
|
||||
|
|
|
@ -195,7 +195,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
|||
},
|
||||
getNodeById() {
|
||||
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 {
|
||||
for (const node of this.workflow.nodes) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { MAIN_HEADER_TABS, VIEWS } from '@/constants';
|
||||
import { IZoomConfig } from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { OnConnectionBindInfo } from 'jsplumb';
|
||||
import { ConnectionDetachedParams } from '@jsplumb/core';
|
||||
import { IConnection } from 'n8n-workflow';
|
||||
import { Route } from 'vue-router';
|
||||
|
||||
|
@ -94,10 +94,10 @@ export const getNodeViewTab = (route: Route): string | null => {
|
|||
};
|
||||
|
||||
export const getConnectionInfo = (
|
||||
connection: OnConnectionBindInfo,
|
||||
connection: ConnectionDetachedParams,
|
||||
): [IConnection, IConnection] | null => {
|
||||
const sourceInfo = connection.sourceEndpoint.getParameters();
|
||||
const targetInfo = connection.targetEndpoint.getParameters();
|
||||
const sourceInfo = connection.sourceEndpoint.parameters;
|
||||
const targetInfo = connection.targetEndpoint.parameters;
|
||||
const sourceNode = useWorkflowsStore().getNodeById(sourceInfo.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 { 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 {
|
||||
IConnection,
|
||||
INode,
|
||||
|
@ -10,6 +14,7 @@ import {
|
|||
NodeInputConnections,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { EVENT_CONNECTION_MOUSEOUT, EVENT_CONNECTION_MOUSEOVER } from '@jsplumb/browser-ui';
|
||||
|
||||
/*
|
||||
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 JSPLUMB_FLOWCHART_STUB = 26;
|
||||
export const OVERLAY_INPUT_NAME_LABEL = 'input-name-label';
|
||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION = [-3, 0.5];
|
||||
export const OVERLAY_INPUT_NAME_LABEL_POSITION_MOVED = [-4.5, 0.5];
|
||||
export const OVERLAY_INPUT_NAME_MOVED_CLASS = 'node-input-endpoint-label--moved';
|
||||
export const OVERLAY_OUTPUT_NAME_LABEL = 'output-name-label';
|
||||
export const GRID_SIZE = 20;
|
||||
|
||||
|
@ -69,9 +73,9 @@ export const WELCOME_STICKY_NODE = {
|
|||
},
|
||||
};
|
||||
|
||||
export const CONNECTOR_FLOWCHART_TYPE = [
|
||||
'N8nCustom',
|
||||
{
|
||||
export const CONNECTOR_FLOWCHART_TYPE: ConnectorSpec = {
|
||||
type: N8nConnector.type,
|
||||
options: {
|
||||
cornerRadius: 12,
|
||||
stub: JSPLUMB_FLOWCHART_STUB + 10,
|
||||
targetGap: 4,
|
||||
|
@ -91,7 +95,7 @@ export const CONNECTOR_FLOWCHART_TYPE = [
|
|||
return index * indexOffset + labelOffset + outputsOffset;
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const CONNECTOR_PAINT_STYLE_DEFAULT: PaintStyle = {
|
||||
stroke: getStyleTokenValue('--color-foreground-dark'),
|
||||
|
@ -110,15 +114,10 @@ export const CONNECTOR_PAINT_STYLE_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[] = [
|
||||
[
|
||||
'Arrow',
|
||||
{
|
||||
type: 'Arrow',
|
||||
options: {
|
||||
id: OVERLAY_ENDPOINT_ARROW_ID,
|
||||
location: 1,
|
||||
width: 12,
|
||||
|
@ -126,10 +125,10 @@ export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
|||
length: 10,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'Arrow',
|
||||
},
|
||||
{
|
||||
type: 'Arrow',
|
||||
options: {
|
||||
id: OVERLAY_MIDPOINT_ARROW_ID,
|
||||
location: 0.5,
|
||||
width: 12,
|
||||
|
@ -137,12 +136,12 @@ export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
|
|||
length: 10,
|
||||
visible: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ANCHOR_POSITIONS: {
|
||||
[key: string]: {
|
||||
[key: number]: AnchorArraySpec[];
|
||||
[key: number]: ArrayAnchorSpec[];
|
||||
};
|
||||
} = {
|
||||
input: {
|
||||
|
@ -194,33 +193,42 @@ export const getInputEndpointStyle = (
|
|||
lineWidth: 0,
|
||||
});
|
||||
|
||||
export const getInputNameOverlay = (label: string): OverlaySpec => [
|
||||
'Label',
|
||||
{
|
||||
export const getInputNameOverlay = (labelText: string): OverlaySpec => ({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
id: OVERLAY_INPUT_NAME_LABEL,
|
||||
location: OVERLAY_INPUT_NAME_LABEL_POSITION,
|
||||
label,
|
||||
cssClass: 'node-input-endpoint-label',
|
||||
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) => ({
|
||||
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||
export const getOutputEndpointStyle = (
|
||||
nodeTypeData: INodeTypeDescription,
|
||||
color: string,
|
||||
): PaintStyle => ({
|
||||
strokeWidth: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
|
||||
fill: getStyleTokenValue(color),
|
||||
outlineStroke: 'none',
|
||||
});
|
||||
|
||||
export const getOutputNameOverlay = (label: string): OverlaySpec => [
|
||||
'Label',
|
||||
{
|
||||
export const getOutputNameOverlay = (labelText: string): OverlaySpec => ({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
id: OVERLAY_OUTPUT_NAME_LABEL,
|
||||
location: [1.9, 0.5],
|
||||
label,
|
||||
cssClass: 'node-output-endpoint-label',
|
||||
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[]) => {
|
||||
overlays.forEach((overlay: OverlaySpec) => {
|
||||
|
@ -302,25 +310,34 @@ export const showOrHideMidpointArrow = (connection: Connection) => {
|
|||
if (!connection || !connection.endpoints || connection.endpoints.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasItemsLabel = !!getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
|
||||
const sourceEndpoint = connection.endpoints[0];
|
||||
const targetEndpoint = connection.endpoints[1];
|
||||
|
||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||
const targetPosition = targetEndpoint.anchor.lastReturnValue
|
||||
? targetEndpoint.anchor.lastReturnValue[0]
|
||||
: sourcePosition + 1; // lastReturnValue is null when moving connections from node to another
|
||||
const sourcePosition = sourceEndpoint._anchor.computedPosition?.curX ?? 0;
|
||||
const targetPosition = targetEndpoint._anchor.computedPosition?.curX ?? sourcePosition + 1;
|
||||
|
||||
const minimum = hasItemsLabel ? 150 : 0;
|
||||
const isBackwards = sourcePosition >= targetPosition;
|
||||
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 isArrowVisible =
|
||||
isBackwards &&
|
||||
isTooLong &&
|
||||
!isActionsOverlayHovered &&
|
||||
!isConnectionHovered &&
|
||||
!connection.instance.isConnectionBeingDragged;
|
||||
|
||||
if (arrow) {
|
||||
arrow.setVisible(isBackwards && isTooLong);
|
||||
arrow.setVisible(isArrowVisible);
|
||||
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];
|
||||
}
|
||||
const bounds = connection.connector.bounds;
|
||||
const diffX = Math.abs(bounds.maxX - bounds.minX);
|
||||
const diffY = Math.abs(bounds.maxY - bounds.minY);
|
||||
const diffX = Math.abs(bounds.xmax - bounds.xmin);
|
||||
const diffY = Math.abs(bounds.ymax - bounds.ymin);
|
||||
|
||||
return [diffX, diffY];
|
||||
};
|
||||
|
@ -339,36 +356,30 @@ const isLoopingBackwards = (connection: Connection) => {
|
|||
const sourceEndpoint = connection.endpoints[0];
|
||||
const targetEndpoint = connection.endpoints[1];
|
||||
|
||||
const sourcePosition = sourceEndpoint.anchor.lastReturnValue[0];
|
||||
const targetPosition = targetEndpoint.anchor.lastReturnValue[0];
|
||||
const sourcePosition = sourceEndpoint._anchor.computedPosition?.curX ?? 0;
|
||||
const targetPosition = targetEndpoint._anchor.computedPosition?.curX ?? 0;
|
||||
|
||||
return targetPosition - sourcePosition < -1 * LOOPBACK_MINIMUM;
|
||||
};
|
||||
|
||||
export const showOrHideItemsLabel = (connection: Connection) => {
|
||||
if (!connection || !connection.connector) {
|
||||
return;
|
||||
}
|
||||
if (!connection?.connector) return;
|
||||
|
||||
const overlay = getOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
if (!overlay) return;
|
||||
|
||||
const actionsOverlay = getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
if (actionsOverlay && actionsOverlay.visible) {
|
||||
const isActionsOverlayHovered = actionsOverlay?.component.isHover();
|
||||
|
||||
if (isActionsOverlayHovered) {
|
||||
overlay.setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
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(false);
|
||||
} else {
|
||||
overlay.setVisible(true);
|
||||
}
|
||||
|
||||
overlay.setVisible(!isHidden);
|
||||
const innerElement = overlay.canvas && overlay.canvas.querySelector('span');
|
||||
if (innerElement) {
|
||||
if (diffY === 0 || isLoopingBackwards(connection)) {
|
||||
|
@ -503,22 +514,27 @@ export const getBackgroundStyles = (
|
|||
return styles;
|
||||
};
|
||||
|
||||
export const hideConnectionActions = (connection: Connection | null) => {
|
||||
if (connection && connection.connector) {
|
||||
export const hideConnectionActions = (connection: Connection) => {
|
||||
connection.instance.setSuspendDrawing(true);
|
||||
hideOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
showOrHideItemsLabel(connection);
|
||||
showOrHideMidpointArrow(connection);
|
||||
}
|
||||
showOrHideItemsLabel(connection);
|
||||
connection.instance.setSuspendDrawing(false);
|
||||
(connection.endpoints || []).forEach((endpoint) => {
|
||||
connection.instance.repaint(endpoint.element);
|
||||
});
|
||||
};
|
||||
|
||||
export const showConnectionActions = (connection: Connection | null) => {
|
||||
if (connection && connection.connector) {
|
||||
export const showConnectionActions = (connection: Connection) => {
|
||||
showOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
hideOverlay(connection, OVERLAY_RUN_ITEMS_ID);
|
||||
if (!getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||
hideOverlay(connection, OVERLAY_MIDPOINT_ARROW_ID);
|
||||
}
|
||||
}
|
||||
|
||||
(connection.endpoints || []).forEach((endpoint) => {
|
||||
connection.instance.repaint(endpoint.element);
|
||||
});
|
||||
};
|
||||
|
||||
export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputConnections) => {
|
||||
|
@ -586,11 +602,9 @@ export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputCo
|
|||
|
||||
export const resetConnection = (connection: Connection) => {
|
||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||
connection.removeClass('success');
|
||||
showOrHideMidpointArrow(connection);
|
||||
if (connection.canvas) {
|
||||
connection.canvas.classList.remove('success');
|
||||
}
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_DEFAULT);
|
||||
};
|
||||
|
||||
export const getRunItemsLabel = (output: { total: number; iterations: number }): string => {
|
||||
|
@ -604,27 +618,36 @@ export const addConnectionOutputSuccess = (
|
|||
connection: Connection,
|
||||
output: { total: number; iterations: number },
|
||||
) => {
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_SUCCESS);
|
||||
if (connection.canvas) {
|
||||
connection.canvas.classList.add('success');
|
||||
}
|
||||
|
||||
connection.addClass('success');
|
||||
if (getOverlay(connection, OVERLAY_RUN_ITEMS_ID)) {
|
||||
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
|
||||
}
|
||||
|
||||
connection.addOverlay([
|
||||
'Label',
|
||||
{
|
||||
const overlay = connection.addOverlay({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
id: OVERLAY_RUN_ITEMS_ID,
|
||||
label: `<span>${getRunItemsLabel(output)}</span>`,
|
||||
cssClass: 'connection-run-items-label',
|
||||
create() {
|
||||
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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
overlay.setVisible(true);
|
||||
showOrHideItemsLabel(connection);
|
||||
showOrHideMidpointArrow(connection);
|
||||
|
||||
(connection.endpoints || []).forEach((endpoint) => {
|
||||
connection.instance.repaint(endpoint.element);
|
||||
});
|
||||
};
|
||||
|
||||
const getContentDimensions = (): { editorWidth: number; editorHeight: number } => {
|
||||
|
@ -677,9 +700,10 @@ export const getZoomToFit = (
|
|||
};
|
||||
|
||||
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
||||
if (connection && connection.connector) {
|
||||
if (connection?.connector) {
|
||||
const connector = connection.connector as N8nConnector;
|
||||
if (targetEndpoint) {
|
||||
connection.connector.setTargetEndpoint(targetEndpoint);
|
||||
connector.setTargetEndpoint(targetEndpoint);
|
||||
}
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PRIMARY);
|
||||
hideOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||
|
@ -687,31 +711,33 @@ export const showDropConnectionState = (connection: Connection, targetEndpoint?:
|
|||
};
|
||||
|
||||
export const showPullConnectionState = (connection: Connection) => {
|
||||
if (connection && connection.connector) {
|
||||
connection.connector.resetTargetEndpoint();
|
||||
if (connection?.connector) {
|
||||
const connector = connection.connector as N8nConnector;
|
||||
connector.resetTargetEndpoint();
|
||||
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_PULL);
|
||||
showOverlay(connection, OVERLAY_DROP_NODE_ID);
|
||||
}
|
||||
};
|
||||
|
||||
export const resetConnectionAfterPull = (connection: Connection) => {
|
||||
if (connection && connection.connector) {
|
||||
connection.connector.resetTargetEndpoint();
|
||||
if (connection?.connector) {
|
||||
const connector = connection.connector as N8nConnector;
|
||||
connector.resetTargetEndpoint();
|
||||
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);
|
||||
if (inputNameOverlay) {
|
||||
inputNameOverlay.setLocation(OVERLAY_INPUT_NAME_LABEL_POSITION);
|
||||
targetEndpoint.instance.removeOverlayClass(inputNameOverlay, OVERLAY_INPUT_NAME_MOVED_CLASS);
|
||||
}
|
||||
};
|
||||
|
||||
export const moveBackInputLabelPosition = (targetEndpoint: Endpoint) => {
|
||||
const inputNameOverlay = getOverlay(targetEndpoint, OVERLAY_INPUT_NAME_LABEL);
|
||||
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,
|
||||
onAdd: Function,
|
||||
) => {
|
||||
if (getOverlay(connection, OVERLAY_CONNECTION_ACTIONS_ID)) {
|
||||
return; // avoid free floating actions when moving connection from one node to another
|
||||
}
|
||||
connection.addOverlay([
|
||||
'Label',
|
||||
{
|
||||
const overlay = connection.addOverlay({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
id: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||
label: `<div class="add">${getIcon('plus')}</div> <div class="delete">${getIcon(
|
||||
'trash',
|
||||
)}</div>`,
|
||||
cssClass: OVERLAY_CONNECTION_ACTIONS_ID,
|
||||
visible: false,
|
||||
events: {
|
||||
mousedown: (overlay: Overlay, event: MouseEvent) => {
|
||||
const element = event.target as HTMLElement;
|
||||
if (
|
||||
element.classList.contains('delete') ||
|
||||
(element.parentElement && element.parentElement.classList.contains('delete'))
|
||||
) {
|
||||
onDelete();
|
||||
} else if (
|
||||
element.classList.contains('add') ||
|
||||
(element.parentElement && element.parentElement.classList.contains('add'))
|
||||
) {
|
||||
onAdd();
|
||||
}
|
||||
create: (component: Connection) => {
|
||||
const div = document.createElement('div');
|
||||
const addButton = document.createElement('button');
|
||||
const deleteButton = document.createElement('button');
|
||||
|
||||
div.classList.add(OVERLAY_CONNECTION_ACTIONS_ID);
|
||||
addButton.classList.add('add');
|
||||
deleteButton.classList.add('delete');
|
||||
|
||||
addButton.innerHTML = getIcon('plus');
|
||||
deleteButton.innerHTML = getIcon('trash');
|
||||
|
||||
addButton.addEventListener('click', () => onAdd());
|
||||
deleteButton.addEventListener('click', () => onDelete());
|
||||
|
||||
// We have to manually trigger connection mouse events because the overlay
|
||||
// is not part of the connection element
|
||||
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) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
:class="$style.container"
|
||||
:class="$style.canvasAddButton"
|
||||
:style="containerCssVars"
|
||||
ref="container"
|
||||
data-test-id="canvas-add-button"
|
||||
|
@ -44,7 +44,7 @@ const containerCssVars = computed(() => ({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
.canvasAddButton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div :class="$style['content']">
|
||||
<div
|
||||
class="node-view-root"
|
||||
class="node-view-root do-not-select"
|
||||
id="node-view-root"
|
||||
data-test-id="node-view-root"
|
||||
@dragover="onDragOver"
|
||||
|
@ -37,10 +37,11 @@
|
|||
v-show="showCanvasAddButton"
|
||||
:showTooltip="!containsTrigger && showTriggerMissingTooltip"
|
||||
:position="canvasStore.canvasAddButtonPosition"
|
||||
ref="canvasAddButton"
|
||||
@hook:mounted="canvasStore.setRecenteredCanvasAddButtonPosition"
|
||||
data-test-id="canvas-add-button"
|
||||
/>
|
||||
<div v-for="nodeData in nodes" :key="nodeData.id">
|
||||
<template v-for="nodeData in nodes">
|
||||
<node
|
||||
v-if="nodeData.type !== STICKY_NODE_TYPE"
|
||||
@duplicateNode="duplicateNode"
|
||||
|
@ -51,6 +52,7 @@
|
|||
@runWorkflow="onRunNode"
|
||||
@moved="onNodeMoved"
|
||||
@run="onNodeRun"
|
||||
:ref="`node-${nodeData.id}`"
|
||||
:key="`${nodeData.id}_node`"
|
||||
:name="nodeData.name"
|
||||
:isReadOnly="isReadOnly"
|
||||
|
@ -74,6 +76,7 @@
|
|||
@nodeSelected="nodeSelectedByName"
|
||||
@removeNode="(name) => removeNode(name, true)"
|
||||
:key="`${nodeData.id}_sticky`"
|
||||
:ref="`node-${nodeData.id}`"
|
||||
:name="nodeData.name"
|
||||
:isReadOnly="isReadOnly"
|
||||
:instance="instance"
|
||||
|
@ -82,7 +85,7 @@
|
|||
:gridSize="GRID_SIZE"
|
||||
:hideActions="pullConnActive"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<node-details-view
|
||||
|
@ -157,15 +160,21 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Vue, { ComponentInstance } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type {
|
||||
OnConnectionBindInfo,
|
||||
Connection,
|
||||
|
||||
import {
|
||||
Endpoint,
|
||||
N8nPlusEndpoint,
|
||||
jsPlumbInstance,
|
||||
} from 'jsplumb';
|
||||
Connection,
|
||||
EVENT_CONNECTION,
|
||||
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 {
|
||||
|
@ -206,7 +215,6 @@ import Node from '@/components/Node.vue';
|
|||
import NodeSettings from '@/components/NodeSettings.vue';
|
||||
import Sticky from '@/components/Sticky.vue';
|
||||
import CanvasAddButton from './CanvasAddButton.vue';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
|
@ -275,7 +283,20 @@ import {
|
|||
RemoveConnectionCommand,
|
||||
RemoveNodeCommand,
|
||||
RenameNodeCommand,
|
||||
historyBus,
|
||||
} 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 {
|
||||
position?: XYPosition;
|
||||
|
@ -313,7 +334,6 @@ export default mixins(
|
|||
},
|
||||
setup() {
|
||||
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
||||
|
||||
return {
|
||||
registerCustomAction,
|
||||
unregisterCustomAction,
|
||||
|
@ -563,7 +583,7 @@ export default mixins(
|
|||
nodeViewScale(): number {
|
||||
return this.canvasStore.nodeViewScale;
|
||||
},
|
||||
instance(): jsPlumbInstance {
|
||||
instance(): BrowserJsPlumbInstance {
|
||||
return this.canvasStore.jsPlumbInstance;
|
||||
},
|
||||
},
|
||||
|
@ -587,7 +607,10 @@ export default mixins(
|
|||
isExecutionPreview: false,
|
||||
showTriggerMissingTooltip: false,
|
||||
workflowData: null as INewWorkflowData | null,
|
||||
activeConnection: null as null | Connection,
|
||||
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
|
||||
// in undo history as a user action.
|
||||
// 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
|
||||
async importWorkflowData(
|
||||
workflowData: IWorkflowToShare,
|
||||
// eslint-disable-next-line @typescript-eslint/default-param-last
|
||||
importTags = true,
|
||||
source: string,
|
||||
importTags = true,
|
||||
): Promise<void> {
|
||||
// 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
|
||||
|
@ -2032,14 +2054,14 @@ export default mixins(
|
|||
this.historyStore.stopRecordingUndo();
|
||||
},
|
||||
initNodeView() {
|
||||
this.instance.importDefaults({
|
||||
Connector: NodeViewUtils.CONNECTOR_FLOWCHART_TYPE,
|
||||
Endpoint: ['Dot', { radius: 5 }],
|
||||
DragOptions: { cursor: 'pointer', zIndex: 5000 },
|
||||
PaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
HoverPaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_PRIMARY,
|
||||
ConnectionOverlays: NodeViewUtils.CONNECTOR_ARROW_OVERLAYS,
|
||||
Container: '#node-view',
|
||||
this.instance?.importDefaults({
|
||||
endpoint: {
|
||||
type: 'Dot',
|
||||
options: { radius: 5 },
|
||||
},
|
||||
paintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
hoverPaintStyle: NodeViewUtils.CONNECTOR_PAINT_STYLE_PRIMARY,
|
||||
connectionOverlays: NodeViewUtils.CONNECTOR_ARROW_OVERLAYS,
|
||||
});
|
||||
|
||||
const insertNodeAfterSelected = (info: {
|
||||
|
@ -2067,7 +2089,7 @@ export default mixins(
|
|||
this.onToggleNodeCreator({ source: info.eventSource, createNodeActive: true });
|
||||
};
|
||||
|
||||
this.instance.bind('connectionAborted', (connection) => {
|
||||
this.instance.bind(EVENT_CONNECTION_ABORT, (connection: Connection) => {
|
||||
try {
|
||||
if (this.dropPrevented) {
|
||||
this.dropPrevented = false;
|
||||
|
@ -2075,10 +2097,10 @@ export default mixins(
|
|||
}
|
||||
|
||||
if (this.pullConnActiveNodeName) {
|
||||
const sourceNode = this.workflowsStore.getNodeById(connection.sourceId);
|
||||
const sourceNode = this.workflowsStore.getNodeById(connection.parameters.nodeId);
|
||||
if (sourceNode) {
|
||||
const sourceNodeName = sourceNode.name;
|
||||
const outputIndex = connection.getParameters().index;
|
||||
const outputIndex = connection.parameters.index;
|
||||
|
||||
this.connectTwoNodes(
|
||||
sourceNodeName,
|
||||
|
@ -2093,8 +2115,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
insertNodeAfterSelected({
|
||||
sourceId: connection.sourceId,
|
||||
index: connection.getParameters().index,
|
||||
sourceId: connection.parameters.nodeId,
|
||||
index: connection.parameters.index,
|
||||
eventSource: 'node_connection_drop',
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -2102,11 +2124,10 @@ export default mixins(
|
|||
}
|
||||
});
|
||||
|
||||
this.instance.bind('beforeDrop', (info) => {
|
||||
this.instance.bind(INTERCEPT_BEFORE_DROP, (info: BeforeDropParams) => {
|
||||
try {
|
||||
const sourceInfo = info.connection.endpoints[0].getParameters();
|
||||
// @ts-ignore
|
||||
const targetInfo = info.dropEndpoint.getParameters();
|
||||
const sourceInfo = info.connection.endpoints[0].parameters;
|
||||
const targetInfo = info.dropEndpoint.parameters;
|
||||
|
||||
const sourceNodeName = this.workflowsStore.getNodeById(sourceInfo.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
|
||||
let activeConnection: null | Connection = null;
|
||||
|
||||
this.instance.bind('connection', (info: OnConnectionBindInfo) => {
|
||||
this.instance.bind(EVENT_CONNECTION, (info: ConnectionEstablishedParams) => {
|
||||
try {
|
||||
const sourceInfo = info.sourceEndpoint.getParameters();
|
||||
const targetInfo = info.targetEndpoint.getParameters();
|
||||
const sourceInfo = info.sourceEndpoint.parameters;
|
||||
const targetInfo = info.targetEndpoint.parameters;
|
||||
|
||||
const sourceNodeName = this.workflowsStore.getNodeById(sourceInfo.nodeId)?.name;
|
||||
const targetNodeName = this.workflowsStore.getNodeById(targetInfo.nodeId)?.name;
|
||||
|
@ -2148,86 +2166,6 @@ export default mixins(
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
const connectionData: [IConnection, IConnection] = [
|
||||
|
@ -2247,26 +2185,105 @@ export default mixins(
|
|||
connection: connectionData,
|
||||
setStateDirty: true,
|
||||
});
|
||||
this.dropPrevented = true;
|
||||
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) {
|
||||
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 {
|
||||
// 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
|
||||
// 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.originalSourceEndpoint.getParameters();
|
||||
// @ts-ignore
|
||||
const targetInfo = info.originalTargetEndpoint.getParameters();
|
||||
const sourceInfo = info.connection.parameters;
|
||||
const targetInfo = info.originalEndpoint.parameters;
|
||||
|
||||
const connectionInfo = [
|
||||
{
|
||||
|
@ -2286,8 +2303,18 @@ export default mixins(
|
|||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
this.instance.bind('connectionDetached', async (info) => {
|
||||
this.instance.bind(EVENT_ENDPOINT_MOUSEOVER, (endpoint: Endpoint, mouse) => {
|
||||
// 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 {
|
||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||
NodeViewUtils.resetInputLabelPosition(info.targetEndpoint);
|
||||
|
@ -2297,14 +2324,12 @@ export default mixins(
|
|||
if (this.pullConnActiveNodeName) {
|
||||
// establish new connection when dragging connection from one node to another
|
||||
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 outputIndex = info.connection.getParameters().index;
|
||||
const outputIndex = info.connection.parameters.index;
|
||||
|
||||
if (connectionInfo) {
|
||||
this.historyStore.pushCommandToUndo(
|
||||
new RemoveConnectionCommand(connectionInfo, this),
|
||||
);
|
||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
||||
}
|
||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0, true);
|
||||
this.pullConnActiveNodeName = null;
|
||||
|
@ -2324,26 +2349,27 @@ export default mixins(
|
|||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.instance.bind('connectionDrag', (connection: Connection) => {
|
||||
this.instance.bind(EVENT_CONNECTION_DRAG, (connection: Connection) => {
|
||||
// The overlays are visible by default so we need to hide the midpoint arrow
|
||||
// manually
|
||||
connection.overlays['midpoint-arrow']?.setVisible(false);
|
||||
try {
|
||||
this.pullConnActiveNodeName = null;
|
||||
this.pullConnActive = true;
|
||||
this.newNodeInsertPosition = null;
|
||||
NodeViewUtils.resetConnection(connection);
|
||||
|
||||
const nodes = [...document.querySelectorAll('.node-default')];
|
||||
const nodes = [...document.querySelectorAll('.node-wrapper')];
|
||||
|
||||
const onMouseMove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = document.querySelector('.jtk-endpoint.dropHover');
|
||||
const element = document.querySelector('.jtk-endpoint.jtk-drag-hover');
|
||||
if (element) {
|
||||
// @ts-ignore
|
||||
NodeViewUtils.showDropConnectionState(connection, element._jsPlumb);
|
||||
const endpoint = element.jtk.endpoint;
|
||||
NodeViewUtils.showDropConnectionState(connection, endpoint);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2360,7 +2386,7 @@ export default mixins(
|
|||
this.pullConnActiveNodeName = node.name;
|
||||
const endpointUUID = this.getInputEndpointUUID(nodeName, 0);
|
||||
if (endpointUUID) {
|
||||
const endpoint = this.instance.getEndpoint(endpointUUID);
|
||||
const endpoint = this.instance?.getEndpoint(endpointUUID);
|
||||
|
||||
NodeViewUtils.showDropConnectionState(connection, endpoint);
|
||||
|
||||
|
@ -2395,8 +2421,17 @@ export default mixins(
|
|||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.instance.bind(
|
||||
[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) => {
|
||||
if (endpoint && endpoint.__meta) {
|
||||
insertNodeAfterSelected({
|
||||
|
@ -2555,10 +2590,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
const uuid: [string, string] = [outputUuid, inputUuid];
|
||||
|
||||
// Create connections in DOM
|
||||
// @ts-ignore
|
||||
this.instance.connect({
|
||||
this.instance?.connect({
|
||||
uuids: uuid,
|
||||
detachable: !this.isReadOnly,
|
||||
});
|
||||
|
@ -2581,15 +2614,12 @@ export default mixins(
|
|||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
const connections = this.instance?.getConnections({
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
connections.forEach((connectionInstance) => {
|
||||
connections.forEach((connectionInstance: Connection) => {
|
||||
if (connectionInstance.__meta) {
|
||||
// Only delete connections from specific indexes (if it can be determined by meta)
|
||||
if (
|
||||
|
@ -2611,13 +2641,8 @@ export default mixins(
|
|||
// it visibly stays behind free floating without a connection.
|
||||
connection.removeOverlays();
|
||||
|
||||
const sourceEndpoint = connection.endpoints && connection.endpoints[0];
|
||||
this.pullConnActiveNodeName = null; // prevent new connections when connectionDetached is triggered
|
||||
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
|
||||
}
|
||||
this.instance?.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store
|
||||
if (trackHistory && connection.__meta) {
|
||||
const connectionData: [IConnection, IConnection] = [
|
||||
{
|
||||
|
@ -2635,18 +2660,14 @@ export default mixins(
|
|||
this.historyStore.pushCommandToUndo(removeCommand);
|
||||
}
|
||||
},
|
||||
__removeConnectionByConnectionInfo(
|
||||
info: OnConnectionBindInfo,
|
||||
removeVisualConnection = false,
|
||||
trackHistory = false,
|
||||
) {
|
||||
__removeConnectionByConnectionInfo(info, removeVisualConnection = false, trackHistory = false) {
|
||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||
|
||||
if (connectionInfo) {
|
||||
if (removeVisualConnection) {
|
||||
this.__deleteJSPlumbConnection(info.connection, trackHistory);
|
||||
} else if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo, this));
|
||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
||||
}
|
||||
this.workflowsStore.removeConnection({ connection: connectionInfo });
|
||||
}
|
||||
|
@ -2751,7 +2772,7 @@ export default mixins(
|
|||
const targetEndpoint = NodeViewUtils.getInputEndpointUUID(targetId, targetInputIndex);
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
const connections = this.instance?.getConnections({
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
}) as Connection[];
|
||||
|
@ -2763,14 +2784,19 @@ export default mixins(
|
|||
},
|
||||
getJSPlumbEndpoints(nodeName: string): Endpoint[] {
|
||||
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 {
|
||||
const endpoints = this.getJSPlumbEndpoints(nodeName);
|
||||
// @ts-ignore
|
||||
return endpoints.find(
|
||||
(endpoint: Endpoint) =>
|
||||
endpoint.type === 'N8nPlus' && endpoint.__meta && endpoint.__meta.index === outputIndex,
|
||||
// @ts-ignore
|
||||
endpoint.endpoint.type === 'N8nPlus' && endpoint?.__meta?.index === outputIndex,
|
||||
);
|
||||
},
|
||||
getIncomingOutgoingConnections(nodeName: string): {
|
||||
|
@ -2781,12 +2807,12 @@ export default mixins(
|
|||
|
||||
if (node) {
|
||||
// @ts-ignore
|
||||
const outgoing = this.instance.getConnections({
|
||||
const outgoing = this.instance?.getConnections({
|
||||
source: node.id,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const incoming = this.instance.getConnections({
|
||||
const incoming = this.instance?.getConnections({
|
||||
target: node.id,
|
||||
}) as Connection[];
|
||||
|
||||
|
@ -2823,8 +2849,7 @@ export default mixins(
|
|||
const sourceId = sourceNode !== null ? sourceNode.id : '';
|
||||
|
||||
if (data === null || data.length === 0 || waiting) {
|
||||
// @ts-ignore
|
||||
const outgoing = this.instance.getConnections({
|
||||
const outgoing = this.instance?.getConnections({
|
||||
source: sourceId,
|
||||
}) as Connection[];
|
||||
|
||||
|
@ -2833,8 +2858,7 @@ export default mixins(
|
|||
});
|
||||
const endpoints = this.getJSPlumbEndpoints(sourceNodeName);
|
||||
endpoints.forEach((endpoint: Endpoint) => {
|
||||
// @ts-ignore
|
||||
if (endpoint.type === 'N8nPlus') {
|
||||
if (endpoint.endpoint.type === 'N8nPlus') {
|
||||
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
|
||||
}
|
||||
});
|
||||
|
@ -2875,6 +2899,7 @@ export default mixins(
|
|||
);
|
||||
if (endpoint && endpoint.endpoint) {
|
||||
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
|
||||
|
||||
if (output && output.total > 0) {
|
||||
(endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(
|
||||
NodeViewUtils.getRunItemsLabel(output),
|
||||
|
@ -2963,7 +2988,7 @@ export default mixins(
|
|||
);
|
||||
|
||||
if (waitForNewConnection) {
|
||||
this.instance.setSuspendDrawing(false, true);
|
||||
this.instance?.setSuspendDrawing(false, true);
|
||||
waitForNewConnection = false;
|
||||
}
|
||||
}, 100); // just to make it clear to users that this is a new connection
|
||||
|
@ -2973,14 +2998,10 @@ export default mixins(
|
|||
|
||||
setTimeout(() => {
|
||||
// Suspend drawing
|
||||
this.instance.setSuspendDrawing(true);
|
||||
|
||||
// Remove all endpoints and the connections in jsplumb
|
||||
this.instance.removeAllEndpoints(node.id);
|
||||
|
||||
// Remove the draggable
|
||||
// @ts-ignore
|
||||
this.instance.destroyDraggable(node.id);
|
||||
this.instance?.setSuspendDrawing(true);
|
||||
(this.instance?.endpointsByElement[node.id] || [])
|
||||
.flat()
|
||||
.forEach((endpoint) => this.instance?.deleteEndpoint(endpoint));
|
||||
|
||||
// Remove the connections in data
|
||||
this.workflowsStore.removeAllNodeConnection(node);
|
||||
|
@ -2989,13 +3010,13 @@ export default mixins(
|
|||
|
||||
if (!waitForNewConnection) {
|
||||
// Now it can draw again
|
||||
this.instance.setSuspendDrawing(false, true);
|
||||
this.instance?.setSuspendDrawing(false, true);
|
||||
}
|
||||
|
||||
// Remove node from selected index if found in it
|
||||
this.uiStore.removeNodeFromSelection(node);
|
||||
if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new RemoveNodeCommand(node, this));
|
||||
this.historyStore.pushCommandToUndo(new RemoveNodeCommand(node));
|
||||
}
|
||||
}, 0); // allow other events to finish like drag stop
|
||||
if (trackHistory && trackBulk) {
|
||||
|
@ -3069,7 +3090,7 @@ export default mixins(
|
|||
workflow.renameNode(currentName, newName);
|
||||
|
||||
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
|
||||
|
@ -3104,18 +3125,12 @@ export default mixins(
|
|||
deleteEveryEndpoint() {
|
||||
// Check as it does not exist on first load
|
||||
if (this.instance) {
|
||||
const nodes = this.workflowsStore.allNodes;
|
||||
nodes.forEach((node: INodeUi) => {
|
||||
try {
|
||||
// important to prevent memory leak
|
||||
// @ts-ignore
|
||||
this.instance.destroyDraggable(node.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
this.instance?.reset();
|
||||
Object.values(this.instance?.endpointsByElement)
|
||||
.flatMap((endpoint) => endpoint)
|
||||
.forEach((endpoint) => endpoint.destroy());
|
||||
|
||||
this.instance.deleteEveryEndpoint();
|
||||
this.instance.deleteEveryConnection({ fireEvent: true });
|
||||
}
|
||||
},
|
||||
matchCredentials(node: INodeUi) {
|
||||
|
@ -3241,7 +3256,7 @@ export default mixins(
|
|||
|
||||
this.workflowsStore.addNode(node);
|
||||
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();
|
||||
|
||||
// Suspend drawing
|
||||
this.instance.setSuspendDrawing(true);
|
||||
this.instance?.setSuspendDrawing(true);
|
||||
|
||||
// Load the connections
|
||||
if (connections !== undefined) {
|
||||
|
@ -3285,9 +3300,8 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now it can draw again
|
||||
this.instance.setSuspendDrawing(false, true);
|
||||
this.instance?.setSuspendDrawing(false, true);
|
||||
},
|
||||
async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise<IWorkflowDataUpdate> {
|
||||
// Because nodes with the same name maybe already exist, it could
|
||||
|
@ -3412,6 +3426,7 @@ export default mixins(
|
|||
tempWorkflow.connectionsBySourceNode,
|
||||
true,
|
||||
);
|
||||
|
||||
this.historyStore.stopRecordingUndo();
|
||||
|
||||
this.uiStore.stateIsDirty = true;
|
||||
|
@ -3458,7 +3473,6 @@ export default mixins(
|
|||
}
|
||||
|
||||
// Keep only the connection to node which get also exported
|
||||
// @ts-ignore
|
||||
typeConnections = {};
|
||||
for (type of Object.keys(connections)) {
|
||||
for (sourceIndex = 0; sourceIndex < connections[type].length; sourceIndex++) {
|
||||
|
@ -3508,14 +3522,12 @@ export default mixins(
|
|||
// Ignore all errors
|
||||
});
|
||||
}
|
||||
|
||||
this.workflowsStore.removeAllConnections({ setStateDirty: false });
|
||||
this.workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
|
||||
|
||||
// Reset workflow execution data
|
||||
this.workflowsStore.setWorkflowExecutionData(null);
|
||||
this.workflowsStore.resetAllNodesIssues();
|
||||
// vm.$forceUpdate();
|
||||
|
||||
this.workflowsStore.setActive(false);
|
||||
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||
|
@ -3622,12 +3634,12 @@ export default mixins(
|
|||
} catch (e) {}
|
||||
},
|
||||
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) {
|
||||
const workflowData = await this.getWorkflowDataFromUrl(data.url as string);
|
||||
if (workflowData !== undefined) {
|
||||
await this.importWorkflowData(workflowData, undefined, 'url');
|
||||
await this.importWorkflowData(workflowData, 'url');
|
||||
}
|
||||
},
|
||||
addPinDataConnections(pinData: IPinData) {
|
||||
|
@ -3638,7 +3650,7 @@ export default mixins(
|
|||
}
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
const connections = this.instance?.getConnections({
|
||||
source: node.id,
|
||||
}) as Connection[];
|
||||
|
||||
|
@ -3658,11 +3670,13 @@ export default mixins(
|
|||
}
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
const connections = this.instance?.getConnections({
|
||||
source: node.id,
|
||||
}) as Connection[];
|
||||
|
||||
this.instance.setSuspendDrawing(true);
|
||||
connections.forEach(NodeViewUtils.resetConnection);
|
||||
this.instance.setSuspendDrawing(false, true);
|
||||
});
|
||||
},
|
||||
onToggleNodeCreator({
|
||||
|
@ -3738,10 +3752,10 @@ export default mixins(
|
|||
},
|
||||
onMoveNode({ nodeName, position }: { nodeName: string; position: XYPosition }): void {
|
||||
this.workflowsStore.updateNodeProperties({ name: nodeName, properties: { position } });
|
||||
setTimeout(() => {
|
||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||
setTimeout(() => {
|
||||
if (node) {
|
||||
this.instance.repaintEverything();
|
||||
this.instance?.repaintEverything();
|
||||
this.onNodeMoved(node);
|
||||
}
|
||||
}, 0);
|
||||
|
@ -3780,12 +3794,12 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.resetWorkspace();
|
||||
this.canvasStore.initInstance(this.$refs.nodeView as HTMLElement);
|
||||
this.$titleReset();
|
||||
window.addEventListener('message', this.onPostMessageReceived);
|
||||
|
||||
this.startLoading();
|
||||
this.resetWorkspace();
|
||||
|
||||
const loadPromises = [
|
||||
this.loadActiveWorkflows(),
|
||||
this.loadCredentials(),
|
||||
|
@ -3806,8 +3820,7 @@ export default mixins(
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.instance.ready(async () => {
|
||||
ready(async () => {
|
||||
try {
|
||||
try {
|
||||
this.initNodeView();
|
||||
|
@ -3879,6 +3892,7 @@ export default mixins(
|
|||
}
|
||||
this.uiStore.addFirstStepOnLoad = false;
|
||||
|
||||
this.initNodeView();
|
||||
document.addEventListener('keydown', this.keyDown);
|
||||
document.addEventListener('keyup', this.keyUp);
|
||||
window.addEventListener('message', this.onPostMessageReceived);
|
||||
|
@ -3886,13 +3900,13 @@ export default mixins(
|
|||
this.$root.$on('newWorkflow', this.newWorkflow);
|
||||
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||
this.$root.$on('nodeMove', this.onMoveNode);
|
||||
this.$root.$on('revertAddNode', this.onRevertAddNode);
|
||||
this.$root.$on('revertRemoveNode', this.onRevertRemoveNode);
|
||||
this.$root.$on('revertAddConnection', this.onRevertAddConnection);
|
||||
this.$root.$on('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||
this.$root.$on('revertRenameNode', this.onRevertNameChange);
|
||||
this.$root.$on('enableNodeToggle', this.onRevertEnableToggle);
|
||||
historyBus.$on('nodeMove', this.onMoveNode);
|
||||
historyBus.$on('revertAddNode', this.onRevertAddNode);
|
||||
historyBus.$on('revertRemoveNode', this.onRevertRemoveNode);
|
||||
historyBus.$on('revertAddConnection', this.onRevertAddConnection);
|
||||
historyBus.$on('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||
historyBus.$on('revertRenameNode', this.onRevertNameChange);
|
||||
historyBus.$on('enableNodeToggle', this.onRevertEnableToggle);
|
||||
|
||||
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
||||
|
@ -3908,20 +3922,23 @@ export default mixins(
|
|||
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||
this.$root.$off('nodeMove', this.onMoveNode);
|
||||
this.$root.$off('revertAddNode', this.onRevertAddNode);
|
||||
this.$root.$off('revertRemoveNode', this.onRevertRemoveNode);
|
||||
this.$root.$off('revertAddConnection', this.onRevertAddConnection);
|
||||
this.$root.$off('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||
this.$root.$off('revertRenameNode', this.onRevertNameChange);
|
||||
this.$root.$off('enableNodeToggle', this.onRevertEnableToggle);
|
||||
historyBus.$off('nodeMove', this.onMoveNode);
|
||||
historyBus.$off('revertAddNode', this.onRevertAddNode);
|
||||
historyBus.$off('revertRemoveNode', this.onRevertRemoveNode);
|
||||
historyBus.$off('revertAddConnection', this.onRevertAddConnection);
|
||||
historyBus.$off('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||
historyBus.$off('revertRenameNode', this.onRevertNameChange);
|
||||
historyBus.$off('enableNodeToggle', this.onRevertEnableToggle);
|
||||
|
||||
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
||||
nodeViewEventBus.$off('saveWorkflow', this.saveCurrentWorkflowExternal);
|
||||
this.instance.unbind();
|
||||
},
|
||||
destroyed() {
|
||||
this.resetWorkspace();
|
||||
this.instance.unbind();
|
||||
this.instance.destroy();
|
||||
this.uiStore.stateIsDirty = false;
|
||||
window.removeEventListener('message', this.onPostMessageReceived);
|
||||
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||
|
@ -4016,40 +4033,6 @@ export default mixins(
|
|||
</style>
|
||||
|
||||
<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 {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: 600;
|
||||
|
@ -4058,30 +4041,12 @@ export default mixins(
|
|||
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 {
|
||||
&:hover {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
> div {
|
||||
> button {
|
||||
color: var(--color-foreground-xdark);
|
||||
border: 2px solid var(--color-foreground-xdark);
|
||||
background-color: var(--color-background-xlight);
|
||||
|
|
|
@ -28,6 +28,7 @@ importers:
|
|||
'@types/node': ^16.11.22
|
||||
cross-env: ^7.0.3
|
||||
cypress: ^10.0.3
|
||||
cypress-real-events: ^1.7.6
|
||||
jest: ^29.3.1
|
||||
jest-environment-jsdom: ^29.3.1
|
||||
jest-mock: ^29.3.1
|
||||
|
@ -52,6 +53,7 @@ importers:
|
|||
'@types/node': 16.11.65
|
||||
cross-env: 7.0.3
|
||||
cypress: 10.11.0
|
||||
cypress-real-events: 1.7.6_cypress@10.11.0
|
||||
jest: 29.3.1_@types+node@16.11.65
|
||||
jest-environment-jsdom: 29.3.1
|
||||
jest-mock: 29.3.1
|
||||
|
@ -532,6 +534,11 @@ importers:
|
|||
'@fortawesome/free-regular-svg-icons': ^6.1.1
|
||||
'@fortawesome/free-solid-svg-icons': ^5.15.3
|
||||
'@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
|
||||
'@testing-library/jest-dom': ^5.16.5
|
||||
'@testing-library/vue': ^5.8.3
|
||||
|
@ -561,7 +568,6 @@ importers:
|
|||
jquery: ^3.4.1
|
||||
jshint: ^2.9.7
|
||||
jsonpath: ^1.1.1
|
||||
jsplumb: 2.15.4
|
||||
lodash-es: ^4.17.21
|
||||
lodash.camelcase: ^4.3.0
|
||||
lodash.debounce: ^4.0.8
|
||||
|
@ -614,6 +620,11 @@ importers:
|
|||
'@fortawesome/free-regular-svg-icons': 6.2.0
|
||||
'@fortawesome/free-solid-svg-icons': 5.15.4
|
||||
'@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
|
||||
codemirror-lang-html-n8n: 1.0.0
|
||||
codemirror-lang-n8n-expression: 0.1.0_zyklskjzaprvz25ee7sq7godcq
|
||||
|
@ -625,7 +636,6 @@ importers:
|
|||
humanize-duration: 3.27.3
|
||||
jquery: 3.6.1
|
||||
jsonpath: 1.1.1
|
||||
jsplumb: 2.15.4
|
||||
lodash-es: 4.17.21
|
||||
lodash.camelcase: 4.3.0
|
||||
lodash.debounce: 4.0.8
|
||||
|
@ -2071,7 +2081,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.20.12
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
'@babel/helper-plugin-utils': 7.19.0
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.19.3:
|
||||
|
@ -3711,6 +3721,34 @@ packages:
|
|||
/@jsdevtools/ono/7.1.3:
|
||||
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:
|
||||
resolution: {integrity: sha512-NrZL1peOIlmlLKvheQcJAx9PHdnc4kaW+9+Yt4jXUfbbYR9EFNCZt6yApI4SwlFilaiZieReM6XslWy1LZAvoQ==}
|
||||
dependencies:
|
||||
|
@ -10253,6 +10291,14 @@ packages:
|
|||
resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
@ -15088,10 +15134,6 @@ packages:
|
|||
semver: 7.3.8
|
||||
dev: false
|
||||
|
||||
/jsplumb/2.15.4:
|
||||
resolution: {integrity: sha512-QssfhXe0YRxY4V2WHPmKwsE3bPHNj4Vts9oinys66ci+4m9lJvFDcEMDygqueiSFL8Jb8CnFyQC9fvL+YHJS7g==}
|
||||
dev: false
|
||||
|
||||
/jsprim/1.4.2:
|
||||
resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
|
Loading…
Reference in a new issue