From 571140deee8f1b296769f0701d649f1a53e0a7f2 Mon Sep 17 00:00:00 2001 From: Mutasem Date: Mon, 18 Oct 2021 14:53:56 +0200 Subject: [PATCH] support item labels and success path --- packages/editor-ui/src/components/Node.vue | 18 ++- packages/editor-ui/src/views/NodeView.vue | 148 +++++++++++++++++++-- packages/editor-ui/src/views/helpers.ts | 74 +++++------ 3 files changed, 188 insertions(+), 52 deletions(-) diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index f7da98475e..d952659836 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -61,6 +61,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { INodeTypeDescription, + ITaskData, NodeHelpers, } from 'n8n-workflow'; @@ -76,8 +77,11 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext NodeIcon, }, computed: { - workflowDataItems () { - const workflowResultDataNode = this.$store.getters.getWorkflowResultDataByNodeName(this.data.name); + nodeRunData(): ITaskData[] { + return this.$store.getters.getWorkflowResultDataByNodeName(this.data.name); + }, + workflowDataItems (): Number { + const workflowResultDataNode = this.nodeRunData; if (workflowResultDataNode === null) { return 0; } @@ -161,9 +165,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext this.setSubtitle(); } }, + nodeRunData(newValue) { + this.$emit('run', {name: this.data.name, data: newValue}); + }, }, mounted() { this.setSubtitle(); + setTimeout(() => { + this.$emit('run', {name: this.data.name, data: this.nodeRunData}); + }, 0); }, data () { return { @@ -402,6 +412,10 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext z-index: 5; } +.jtk-connector.jtk-success { + z-index: 5; +} + .jtk-endpoint { z-index:5; } diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 25ee44d30e..d45410937d 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -22,6 +22,7 @@ @removeNode="removeNode" @runWorkflow="runWorkflow" @moved="onNodeMoved" + @run="onNodeRun" :id="'node-' + getNodeIndex(nodeData.name)" :key="getNodeIndex(nodeData.name)" :name="nodeData.name" @@ -132,7 +133,7 @@ import NodeCreator from '@/components/NodeCreator/NodeCreator.vue'; import NodeSettings from '@/components/NodeSettings.vue'; import RunData from '@/components/RunData.vue'; -import { getLeftmostTopNode, getWorkflowCorners, scaleSmaller, scaleBigger, scaleReset, addOrRemoveMidpointArrow, addEndpointArrow, getDefaultOverlays, getIcon, getNewNodePosition } from './helpers'; +import { getLeftmostTopNode, getWorkflowCorners, scaleSmaller, scaleBigger, scaleReset, showOrHideMidpointArrow, addEndpointArrow, getDefaultOverlays, getIcon, getNewNodePosition, hideMidpointArrow } from './helpers'; import mixins from 'vue-typed-mixins'; import { v4 as uuidv4} from 'uuid'; @@ -145,14 +146,14 @@ import { INodeIssues, INodeTypeDescription, INodeTypeNameVersion, - NodeInputConnections, NodeHelpers, Workflow, IRun, + ITaskData, INodeCredentialsDetails, + INodeExecutionData, } from 'n8n-workflow'; import { - IConnectionsUi, ICredentialsResponse, IExecutionResponse, IN8nUISettings, @@ -1329,7 +1330,7 @@ export default mixins( info.connection.setConnector(['Flowchart', { cornerRadius: 8, stub: JSPLUMB_FLOWCHART_STUB, gap: 5, alwaysRespectStubs: false}]); addEndpointArrow(info.connection); - addOrRemoveMidpointArrow(info.connection); + showOrHideMidpointArrow(info.connection); // @ts-ignore const sourceInfo = info.sourceEndpoint.getParameters(); @@ -1355,9 +1356,11 @@ export default mixins( const overlay = info.connection.getOverlay('connection-actions'); overlay.setVisible(true); - const arrow = info.connection.getOverlay('midpoint-arrow'); - if (arrow) { - arrow.setVisible(false); + hideMidpointArrow(info.connection); + + const itemsOverlay = info.connection.getOverlay('output-items-label'); + if (itemsOverlay) { + itemsOverlay.setVisible(false); } }); info.connection.bind('mouseout', (connection: IConnection) => { @@ -1366,9 +1369,11 @@ export default mixins( overlay.setVisible(false); timer = undefined; - const arrow = info.connection.getOverlay('midpoint-arrow'); - if (arrow) { - arrow.setVisible(true); + showOrHideMidpointArrow(info.connection); + + const itemsOverlay = info.connection.getOverlay('output-items-label'); + if (itemsOverlay) { + itemsOverlay.setVisible(true); } }, 500); }); @@ -1722,7 +1727,128 @@ export default mixins( }) as Connection[]; [...incoming, ...outgoing].forEach((connection: Connection) => { - addOrRemoveMidpointArrow(connection); + showOrHideMidpointArrow(connection); + }); + }, + onNodeRun ({name, data}: {name: string, data: ITaskData[] | null}) { + const sourceIndex = this.$store.getters.getNodeIndex(name); + const sourceId = `${NODE_NAME_PREFIX}${sourceIndex}`; + + if (data === null || data.length === 0) { + // @ts-ignore + const outgoing = this.instance.getConnections({ + source: sourceId, + }) as Connection[]; + + outgoing.forEach((connection: Connection) => { + const arrow = connection.getOverlay('midpoint-arrow'); + if (arrow) { + // @ts-ignore + arrow.setLocation(0.5); + } + + connection.removeOverlay('output-items-label'); + connection.setPaintStyle({stroke: getStyleTokenValue('--color-foreground-dark')}); + }); + + return; + } + + const nodeConnections = (this.$store.getters.outgoingConnectionsByNodeName(name) as INodeConnections).main; + if (!nodeConnections) { + return; + } + + const outputMap: {[sourceEndpoint: string]: {[targetId: string]: {[targetEndpoint: string]: {total: number, iterations: number}}}} = {}; + + data.forEach((run: ITaskData) => { + if (!run.data) { + return; + } + + run.data.main.forEach((output: INodeExecutionData[] | null, i: number) => { + nodeConnections[i] + .map((conn: IConnection) => { + const targetIndex = this.getNodeIndex(conn.node); + const targetId = `${NODE_NAME_PREFIX}${targetIndex}`; + + const sourceEndpoint = `${sourceIndex}-output${i}`; + const targetEndpoint = `${targetIndex}-input${conn.index}`; + + if (!outputMap[sourceEndpoint]) { + outputMap[sourceEndpoint] = {}; + } + + if (!outputMap[sourceEndpoint][targetId]) { + outputMap[sourceEndpoint][targetId] = {}; + } + + if (!outputMap[sourceEndpoint][targetId][targetEndpoint]) { + outputMap[sourceEndpoint][targetId][targetEndpoint] = { + total: 0, + iterations: 0, + }; + } + + outputMap[sourceEndpoint][targetId][targetEndpoint].total += output ? output.length : 0; + outputMap[sourceEndpoint][targetId][targetEndpoint].iterations += output ? 1 : 0; + }); + }); + }); + + Object.keys(outputMap).forEach((sourceEndpoint: string) => { + Object.keys(outputMap[sourceEndpoint]).forEach((targetId: string) => { + Object.keys(outputMap[sourceEndpoint][targetId]).forEach((targetEndpoint: string) => { + // @ts-ignore + const connections = this.instance.getConnections({ + source: sourceId, + target: targetId, + }) as Connection[]; + + const conn = connections.find((connection: Connection) => { + // @ts-ignore + const uuids = connection.getUuids(); + return uuids[0] === sourceEndpoint && uuids[1] === targetEndpoint; + }); + + if (!conn) { + return; + } + + const output = outputMap[sourceEndpoint][targetId][targetEndpoint]; + if (!output || !output.total) { + conn.setPaintStyle({stroke: getStyleTokenValue('--color-foreground-dark')}); + conn.removeOverlay('output-items-label'); + return; + } + + conn.setPaintStyle({stroke: getStyleTokenValue('--color-success')}); + + if (conn.getOverlay('output-items-label')) { + conn.removeOverlay('output-items-label'); + } + + let label = `${output.total}`; + label = output.total > 1 ? `${label} items` : `${label} item`; + label = output.iterations > 1 ? `${label} total` : label; + + conn.addOverlay([ + 'Label', + { + id: 'output-items-label', + label, + cssClass: 'connection-output-name-label', + location: .5, + }, + ]); + + const arrow = connections[0].getOverlay('midpoint-arrow'); + if (arrow) { + // @ts-ignore + arrow.setLocation(0.6); + } + }); + }); }); }, removeNode (nodeName: string) { diff --git a/packages/editor-ui/src/views/helpers.ts b/packages/editor-ui/src/views/helpers.ts index fe034a3c2d..5d20570aa6 100644 --- a/packages/editor-ui/src/views/helpers.ts +++ b/packages/editor-ui/src/views/helpers.ts @@ -1,4 +1,3 @@ -import { JSPLUMB_FLOWCHART_STUB } from "@/constants"; import { INodeUi, IZoomConfig, XYPositon } from "@/Interface"; import { Connection, OverlaySpec } from "jsplumb"; @@ -95,6 +94,7 @@ export const getDefaultOverlays = (): OverlaySpec[] => ([ width: 12, foldback: 1, length: 10, + visible: true, }, ], [ @@ -104,46 +104,10 @@ export const getDefaultOverlays = (): OverlaySpec[] => ([ label: 'Drop connection
to create node', cssClass: 'drop-add-node-label', location: 0.5, + visible: false, }, ], -]); - -export const addEndpointArrow = (connection: Connection) => { - const hasArrow = !!connection.getOverlay('midpoint-arrow'); - if (!hasArrow) { - connection.addOverlay([ - 'Arrow', - { - id: 'endpoint-arrow', - location: 1, - width: 12, - foldback: 1, - length: 10, - }, - ]); - } -}; - -export const addOrRemoveMidpointArrow = (connection: Connection) => { - const sourceEndpoint = connection.endpoints[0]; - const targetEndpoint = connection.endpoints[1]; - const requiresArrow = sourceEndpoint.anchor.lastReturnValue[0] >= targetEndpoint.anchor.lastReturnValue[0]; - - const hasArrow = !!connection.getOverlay('midpoint-arrow'); - - if (!requiresArrow) { - if (hasArrow) { - connection.removeOverlay('midpoint-arrow'); - } - - return; - } - - if (hasArrow) { - return; - } - - connection.addOverlay([ + [ 'Arrow', { id: 'midpoint-arrow', @@ -151,10 +115,42 @@ export const addOrRemoveMidpointArrow = (connection: Connection) => { width: 12, foldback: 1, length: 10, + visible: false, + }, + ], +]); + +export const addEndpointArrow = (connection: Connection) => { + connection.addOverlay([ + 'Arrow', + { + id: 'endpoint-arrow', + location: 1, + width: 12, + foldback: 1, + length: 10, }, ]); }; +export const hideMidpointArrow = (connection: Connection) => { + const arrow = connection.getOverlay('midpoint-arrow'); + if (arrow) { + arrow.setVisible(false); + } +}; + +export const showOrHideMidpointArrow = (connection: Connection) => { + const sourceEndpoint = connection.endpoints[0]; + const targetEndpoint = connection.endpoints[1]; + const requiresArrow = sourceEndpoint.anchor.lastReturnValue[0] >= targetEndpoint.anchor.lastReturnValue[0]; + + const arrow = connection.getOverlay('midpoint-arrow'); + if (arrow) { + arrow.setVisible(requiresArrow); + } +}; + export const getIcon = (name: string): string => { if (name === 'trash') { return ``;