Add plus endpoint (#2450)

* fix

* refactor

* clean up css

* refactor setzoom level

* refactor

* refactor

* refactor func

* remove scope

* remove localstorage caching

* clean up imports

* update zero case

* add delete connection

* update selected state

* add base type, remove straight line

* add stub offset back

* rename param

* add label offset

* update font size of items

* move up label

* fix error state while executing

* disrespect stubs

* check for errors

* refactor position

* clean up extra space

* make entire node connectable

* Revert "make entire node connectable"

e304f7c5b8ff1b41268450c60ca4bc3b2ada5d4a

* always show border

* add border to zoom buttons

* update spacing

* update colors

* allow connecting to entire node

* fix pull conn active

* two line names

* apply select to all lines

* increase input margin

* override target pos

* reset conn after pull

* fix types

* update orientation

* fix up connectors snapping

* hide arrow on pull

* update overrides for connectors

* change text

* update pull colors

* set to 1 line when selected

* fix executions bug

* build

* refactor node component

* remove comment

* refactor more

* remove prop

* fix build issue

* fix input drag bug in executions

* reset offset

* update select background

* handle issue when endpoints are not set

* fix connection aborted issue

* add try catch to help show errors

* wrap bind with try/catch

* set default styles

* reset pos despite zoom

* add more checks

* clean up impl

* update icon

* handle unknown types

* hide items on init

* fix importing unknown types with credentials

* change opacity

* push up item label

* update color

* update label class and colors

* add to drop distance

* fix z-index to match node

* disable eslint

* fix lasso tool selection

* update background color

* update waiting state

* update tooltip positions

* update wait node border

* fix selection bug mostly

* if selected, move above other nodes

* add line through disabled nodes

* remove node color option

* move label above connection

* success color for line through

* update options index

* hide waiting icon when disabled

* fix gmail icon

* refactor icons

* clear execution data on disable/delete

* fix selected node

* fix executing behavior

* optional __meta

* set grid size

* remove default color

* remove node color

* add comments

* comments

* add comments

* remove empty space

* update comment

* refactor uuids

* fix type issue

* Revert "fix type issue"

9523b34f9604f75253ae0631f29fc27267a99d78

* Revert "fix type issue"

9523b34f9604f75253ae0631f29fc27267a99d78

* Revert "refactor uuids"

07f6848065cb9a98475fddb8330846106f9e70ad

* fix build issues

* refactor

* update uuid

* child nodes

* skip nodes behind when pushing in loop

* shift output icon for switch node

* don't show output if waiting

* waiting on init

* build

* change to bezier

* implement plus

* revert connector change

* add bezier type

* fix snapping

* clean up impl

* refactor func

* make const

* rename type

* refactor to simplify

* Revert "refactor to simplify"

2db0ed504c752c33de975370d86a83a04ffcda14

* enable flowchart mode

* clean up flowchart type

* refactor type

* merge types

* configure curviness

* set in localstorage

* fix straight line arrow bug

* show arrow when pulling

* refactor / simplify

* fix target gap in bezier

* refactor target gap

* add comments

* add comment

* fix dragging connections

* fix bug when moving connection

* update comment

* rename file

* update values

* update minor

* update straight line box

* clean up conn types

* clean up z-indexes

* move color filters to node icon

* update background color

* update to use grid size value

* fix endpoint offsets

* set yspan range lower

* remove overlays when moving conn

* prevent unwanted connections

* fix messed up connections

* remove console log

* clear execution issues on workflow run

* update corner radius

* fix drag/delete bug

* increase offset

* update disabled state

* hide if endpoint has conn

* node insert on click

* simplify impl

* fix z-indexes

* add drop hover message

* address comments

* refactor

* refactor func

* remove drop conn overlay

* update messagE

* update colors

* remove console log

* rebuild impl

* add stalk

* fix disabled state

* fix dragging bug

* remove node selection

* disable plus in executions

* add success output to talk

* add success output

* set output

* fix bug

* fix switch size

* update size for 3 output nodes

* update loops

* fix bug when node is renamed

* set final values

* update loop behavior

* update comment

* hide output labels on multi outputs

* center output

* fix flicker when deleting nodes

* fix dragging space

* increase stalk if if

* more type checks

* rename event source

* rename event source

* support touch when dragging

* offset center

* center plus

* fix repaint behavior

* remove extending output

* add offset to switch

* fix merge node bug

* rename endpoint

* refactor css, fix merge bug

* fix unrelated issues

* space out

* remove flowchart type

* rename constant key

* clean up css

* rename var

* fix more type issues

* update types

* fix spacing

* simplify vertical offset

* refactor css

* add license

* update css

* clean up scss

* update to use css var

* clean up code

* update params

* show message

* fix plus position

* fix merge node bugs

* hide box when not dragging and hidden

* fix potential issue

* address comments
This commit is contained in:
Mutasem Aldmour
2021-12-03 18:53:55 +01:00
committed by GitHub
parent d62c7a7f3c
commit 76c63790b4
8 changed files with 757 additions and 67 deletions

View File

@@ -66,11 +66,22 @@ declare module 'jsplumb' {
}
interface Endpoint {
endpoint: any; // tslint:disable-line:no-any
elementId: string;
__meta?: {
nodeName: string,
nodeId: string,
index: number,
totalEndpoints: number;
};
getUuid(): string;
getOverlay(name: string): any; // tslint:disable-line:no-any
repaint(params?: object): void;
}
interface N8nPlusEndpoint extends Endpoint {
setSuccessOutput(message: string): void;
clearSuccessOutput(): void;
}
interface Overlay {
@@ -103,6 +114,7 @@ export interface IEndpointOptions {
parameters?: any; // tslint:disable-line:no-any
uuid?: string;
enabled?: boolean;
cssClass?: string;
}
export interface IUpdateInformation {

View File

@@ -1,5 +1,5 @@
<template>
<div :class="{'node-wrapper': true, selected: isSelected}" :style="nodePosition">
<div class="node-wrapper" :style="nodePosition">
<div class="select-background" v-show="isSelected"></div>
<div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name">
<div :class="nodeClass" :style="nodeStyle" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
@@ -519,8 +519,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
</style>
<style lang="scss">
/** node */
.node-wrapper.selected {
.jtk-endpoint {
z-index: 2;
}
@@ -551,32 +550,31 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
z-index: 4;
}
/** node endpoints */
.jtk-endpoint {
z-index:5;
}
.jtk-connector.jtk-hover {
z-index: 6;
}
.disabled-linethrough {
.jtk-endpoint.plus-endpoint {
z-index: 6;
}
.jtk-endpoint.jtk-hover {
.jtk-endpoint.dot-output-endpoint {
z-index: 7;
}
.jtk-overlay {
z-index:7;
z-index: 7;
}
.disabled-linethrough {
z-index: 8;
}
.jtk-connector.jtk-dragging {
z-index: 8;
}
.jtk-endpoint.jtk-drag-active {
.jtk-drag-active.dot-output-endpoint, .jtk-drag-active.rect-input-endpoint {
z-index: 9;
}
@@ -593,3 +591,87 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
}
</style>
<style lang="scss">
$--stalklength: 40px;
$--box-size-medium: 24px;
$--box-size-small: 18px;
.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);
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;
&.small {
height: $--box-size-small;
width: $--box-size-small;
font-size: 8px;
}
.fa-plus {
width: 1em;
}
}
.drop-hover-message {
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;
}
}
}
</style>

View File

@@ -11,6 +11,7 @@ import { Endpoint } from 'jsplumb';
import {
INodeTypeDescription,
} from 'n8n-workflow';
import { getStyleTokenValue } from '../helpers';
export const nodeBase = mixins(
deviceSupportHelpers,
@@ -68,13 +69,14 @@ export const nodeBase = mixins(
endpointStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
endpointHoverStyle: CanvasHelpers.getInputEndpointStyle(nodeTypeData, '--color-primary'),
isSource: false,
isTarget: !this.isReadOnly,
isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
parameters: {
nodeIndex: this.nodeIndex,
type: inputName,
index,
},
enabled: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView
enabled: !this.isReadOnly, // enabled in default case to allow dragging
cssClass: 'rect-input-endpoint',
dragAllowedWhenFull: true,
dropOptions: {
tolerance: 'touch',
@@ -92,7 +94,9 @@ export const nodeBase = mixins(
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
endpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.inputs.length,
};
// TODO: Activate again if it makes sense. Currently makes problems when removing
@@ -140,6 +144,7 @@ export const nodeBase = mixins(
type: inputName,
index,
},
cssClass: 'dot-output-endpoint',
dragAllowedWhenFull: false,
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
};
@@ -151,11 +156,53 @@ export const nodeBase = mixins(
];
}
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, {...newEndpointData});
endpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.outputs.length,
};
if (!this.isReadOnly) {
const plusEndpointData: IEndpointOptions = {
uuid: CanvasHelpers.getOutputEndpointUUID(this.nodeIndex, index),
anchor: anchorPosition,
maxConnections: -1,
endpoint: 'N8nPlus',
isSource: true,
isTarget: false,
enabled: !this.isReadOnly,
endpointStyle: {
fill: getStyleTokenValue('--color-xdark'),
outlineStroke: 'none',
hover: false,
showOutputLabel: nodeTypeData.outputs.length === 1,
size: nodeTypeData.outputs.length >= 3 ? 'small' : 'medium',
},
endpointHoverStyle: {
fill: getStyleTokenValue('--color-primary'),
outlineStroke: 'none',
hover: true, // hack to distinguish hover state
},
parameters: {
nodeIndex: this.nodeIndex,
type: inputName,
index,
},
cssClass: 'plus-draggable-endpoint',
dragAllowedWhenFull: false,
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
};
const plusEndpoint: Endpoint = this.instance.addEndpoint(this.nodeId, plusEndpointData);
plusEndpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.outputs.length,
};
}
});
},
__makeInstanceDraggable(node: INodeUi) {

View File

@@ -11,6 +11,7 @@ export const DEFAULT_NEW_WORKFLOW_NAME = 'My workflow';
export const MIN_WORKFLOW_NAME_LENGTH = 1;
export const MAX_WORKFLOW_NAME_LENGTH = 128;
export const DUPLICATE_POSTFFIX = ' copy';
export const NODE_OUTPUT_DEFAULT_KEY = '_NODE_OUTPUT_DEFAULT_KEY_';
// tags
export const MAX_TAG_NAME_LENGTH = 24;

View File

@@ -0,0 +1,495 @@
/**
* 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 () {
var root = this, _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.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;
})();

View File

@@ -862,7 +862,7 @@ export const store = new Vuex.Store({
return state.workflowExecutionData;
},
getWorkflowRunData: (state): IRunData | null => {
if (state.workflowExecutionData === null) {
if (!state.workflowExecutionData || !state.workflowExecutionData.data || !state.workflowExecutionData.data.resultData) {
return null;
}

View File

@@ -108,11 +108,11 @@
<script lang="ts">
import Vue from 'vue';
import {
Connection, Endpoint,
Connection, Endpoint, N8nPlusEndpoint,
} from 'jsplumb';
import { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
import { NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers';
@@ -168,6 +168,7 @@ import {
} from '../Interface';
import { mapGetters } from 'vuex';
import '../plugins/N8nCustomConnectorType';
import '../plugins/PlusEndpointType';
export default mixins(
copyPaste,
@@ -360,7 +361,7 @@ export default mixins(
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
this.$telemetry.track('User opened read-only execution', { workflow_id: data.workflowData.id, execution_mode: data.mode, execution_finished: data.finished });
if (data.finished !== true && data.data.resultData.error) {
if (data.finished !== true && data && data.data && data.data.resultData && data.data.resultData.error) {
// Check if any node contains an error
let nodeErrorFound = false;
if (data.data.resultData.runData) {
@@ -1519,7 +1520,6 @@ export default mixins(
this.pullConnActive = true;
this.newNodeInsertPosition = null;
CanvasHelpers.resetConnection(connection);
CanvasHelpers.addOverlays(connection, CanvasHelpers.CONNECTOR_DROP_NODE_OVERLAY);
const nodes = [...document.querySelectorAll('.node-default')];
const onMouseMove = (e: MouseEvent | TouchEvent) => {
@@ -1579,6 +1579,17 @@ export default mixins(
console.error(e); // eslint-disable-line no-console
}
});
// @ts-ignore
this.instance.bind(('plusEndpointClick'), (endpoint: Endpoint) => {
if (endpoint && endpoint.__meta) {
insertNodeAfterSelected({
sourceId: endpoint.__meta.nodeId,
index: endpoint.__meta.index,
eventSource: 'plus_endpoint',
});
}
});
},
async newWorkflow (): Promise<void> {
await this.resetWorkspace();
@@ -1715,8 +1726,13 @@ 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
}
},
__removeConnectionByConnectionInfo (info: OnConnectionBindInfo, removeVisualConnection = false) {
// @ts-ignore
@@ -1808,6 +1824,16 @@ export default mixins(
return uuids[0] === sourceEndpoint && uuids[1] === targetEndpoint;
});
},
getJSPlumbEndpoints (nodeName: string): Endpoint[] {
const nodeIndex = this.getNodeIndex(nodeName);
const nodeId = `${NODE_NAME_PREFIX}${nodeIndex}`;
return this.instance.getEndpoints(nodeId);
},
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);
},
getIncomingOutgoingConnections(nodeName: string): {incoming: Connection[], outgoing: Connection[]} {
const name = `${NODE_NAME_PREFIX}${this.$store.getters.getNodeIndex(nodeName)}`;
// @ts-ignore
@@ -1847,33 +1873,47 @@ export default mixins(
outgoing.forEach((connection: Connection) => {
CanvasHelpers.resetConnection(connection);
});
const endpoints = this.getJSPlumbEndpoints(sourceNodeName);
endpoints.forEach((endpoint: Endpoint) => {
// @ts-ignore
if (endpoint.type === 'N8nPlus') {
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
}
});
return;
}
const nodeConnections = (this.$store.getters.outgoingConnectionsByNodeName(sourceNodeName) as INodeConnections).main;
if (!nodeConnections) {
return;
}
const outputMap = CanvasHelpers.getOutputSummary(data, nodeConnections);
const outputMap = CanvasHelpers.getOutputSummary(data, nodeConnections || []);
Object.keys(outputMap).forEach((sourceOutputIndex: string) => {
Object.keys(outputMap[sourceOutputIndex]).forEach((targetNodeName: string) => {
Object.keys(outputMap[sourceOutputIndex][targetNodeName]).forEach((targetInputIndex: string) => {
if (targetNodeName) {
const connection = this.getJSPlumbConnection(sourceNodeName, parseInt(sourceOutputIndex, 10), targetNodeName, parseInt(targetInputIndex, 10));
if (!connection) {
return;
}
if (connection) {
const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex];
if (!output || !output.total) {
CanvasHelpers.resetConnection(connection);
return;
}
else {
CanvasHelpers.addConnectionOutputSuccess(connection, output);
}
}
}
CanvasHelpers.addConnectionOutputSuccess(connection, output);
const endpoint = this.getPlusEndpoint(sourceNodeName, parseInt(sourceOutputIndex, 10));
if (endpoint && endpoint.endpoint) {
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
if (output && output.total > 0) {
(endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(CanvasHelpers.getRunItemsLabel(output));
}
else {
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
}
}
});
});
});
@@ -1911,6 +1951,7 @@ export default mixins(
}
}
let waitForNewConnection = false;
// connect nodes before/after deleted node
const nodeType: INodeTypeDescription | null = this.$store.getters.nodeType(node.type, node.typeVersion);
if (nodeType && nodeType.outputs.length === 1
@@ -1920,6 +1961,7 @@ export default mixins(
const conn1 = incoming[0];
const conn2 = outgoing[0];
if (conn1.__meta && conn2.__meta) {
waitForNewConnection = true;
const sourceNodeName = conn1.__meta.sourceNodeName;
const sourceNodeOutputIndex = conn1.__meta.sourceOutputIndex;
const targetNodeName = conn2.__meta.targetNodeName;
@@ -1927,7 +1969,12 @@ export default mixins(
setTimeout(() => {
this.connectTwoNodes(sourceNodeName, sourceNodeOutputIndex, targetNodeName, targetNodeOuputIndex);
}, 100);
if (waitForNewConnection) {
this.instance.setSuspendDrawing(false, true);
waitForNewConnection = false;
}
}, 100); // just to make it clear to users that this is a new connection
}
}
}
@@ -1952,8 +1999,10 @@ export default mixins(
this.$store.commit('removeNode', node);
this.$store.commit('clearNodeExecutionData', node.name);
if (!waitForNewConnection) {
// Now it can draw again
this.instance.setSuspendDrawing(false, true);
}
// Remove node from selected index if found in it
this.$store.commit('removeNodeFromSelection', node);
@@ -2603,7 +2652,7 @@ export default mixins(
color: var(--color-success);
}
> span.floating {
.floating {
position: absolute;
top: -22px;
transform: translateX(-50%);

View File

@@ -1,5 +1,5 @@
import { getStyleTokenValue } from "@/components/helpers";
import { START_NODE_TYPE } from "@/constants";
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE } from "@/constants";
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
import {
@@ -57,11 +57,13 @@ export const CONNECTOR_FLOWCHART_TYPE = ['N8nCustom', {
getEndpointOffset(endpoint: Endpoint) {
const indexOffset = 10; // stub offset between different endpoints of same node
const index = endpoint && endpoint.__meta ? endpoint.__meta.index : 0;
const totalEndpoints = endpoint && endpoint.__meta ? endpoint.__meta.totalEndpoints : 0;
const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL);
const labelOffset = outputOverlay && outputOverlay.label && outputOverlay.label.length > 1 ? 10 : 0;
const outputsOffset = totalEndpoints > 3 ? 24 : 0; // avoid intersecting plus
return index * indexOffset + labelOffset;
return index * indexOffset + labelOffset + outputsOffset;
},
}];
@@ -112,23 +114,9 @@ export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
],
];
export const CONNECTOR_DROP_NODE_OVERLAY: OverlaySpec[] = [
[
'Label',
{
id: OVERLAY_DROP_NODE_ID,
label: 'Drop connection<br />to add node',
cssClass: 'drop-add-node-label',
location: 0.5,
visible: true,
},
],
];
export const ANCHOR_POSITIONS: {
[key: string]: {
[key: number]: string[] | number[][];
[key: number]: number[][];
}
} = {
input: {
@@ -511,20 +499,33 @@ export const getOutputSummary = (data: ITaskData[], nodeConnections: NodeInputCo
}
run.data.main.forEach((output: INodeExecutionData[] | null, i: number) => {
if (!nodeConnections[i]) {
return;
}
nodeConnections[i]
.map((connection: IConnection) => {
const sourceOutputIndex = i;
const targetNodeName = connection.node;
const targetInputIndex = connection.index;
if (!outputMap[sourceOutputIndex]) {
outputMap[sourceOutputIndex] = {};
}
if (!outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY]) {
outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY] = {};
outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0] = {
total: 0,
iterations: 0,
};
}
const defaultOutput = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
defaultOutput.total += output ? output.length : 0;
defaultOutput.iterations += output ? 1 : 0;
if (!nodeConnections[sourceOutputIndex]) {
return;
}
nodeConnections[sourceOutputIndex]
.map((connection: IConnection) => {
const targetNodeName = connection.node;
const targetInputIndex = connection.index;
if (!outputMap[sourceOutputIndex][targetNodeName]) {
outputMap[sourceOutputIndex][targetNodeName] = {};
}
@@ -554,6 +555,13 @@ export const resetConnection = (connection: Connection) => {
}
};
export const getRunItemsLabel = (output: {total: number, iterations: number}): string => {
let label = `${output.total}`;
label = output.total > 1 ? `${label} items` : `${label} item`;
label = output.iterations > 1 ? `${label} total` : label;
return label;
};
export const addConnectionOutputSuccess = (connection: Connection, output: {total: number, iterations: number}) => {
connection.setPaintStyle(CONNECTOR_PAINT_STYLE_SUCCESS);
if (connection.canvas) {
@@ -564,15 +572,11 @@ export const addConnectionOutputSuccess = (connection: Connection, output: {tota
connection.removeOverlay(OVERLAY_RUN_ITEMS_ID);
}
let label = `${output.total}`;
label = output.total > 1 ? `${label} items` : `${label} item`;
label = output.iterations > 1 ? `${label} total` : label;
connection.addOverlay([
'Label',
{
id: OVERLAY_RUN_ITEMS_ID,
label: `<span>${label}</span>`,
label: `<span>${getRunItemsLabel(output)}</span>`,
cssClass: 'connection-run-items-label',
location: .5,
},