refactor(editor): Fix NodeView/Canvas related TS errors (#9581)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
oleg
2024-06-03 16:33:20 +02:00
committed by GitHub
parent 3298914bc4
commit 68420ca6be
23 changed files with 587 additions and 389 deletions

View File

@@ -3,7 +3,7 @@ import {
mapLegacyEndpointsToCanvasConnectionPort,
getUniqueNodeName,
} from '@/utils/canvasUtilsV2';
import type { IConnections, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType, type IConnections, type INodeTypeDescription } from 'n8n-workflow';
import type { CanvasConnection } from '@/types';
import type { INodeUi } from '@/Interface';
@@ -15,7 +15,7 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
it('should map legacy connections to canvas connections', () => {
const legacyConnections: IConnections = {
'Node A': {
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
main: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]],
},
};
const nodes: INodeUi[] = [
@@ -53,11 +53,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node A',
source: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
@@ -67,7 +67,7 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
it('should return empty array when no matching nodes found', () => {
const legacyConnections: IConnections = {
'Node A': {
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
main: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]],
},
};
const nodes: INodeUi[] = [];
@@ -113,8 +113,8 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
const legacyConnections: IConnections = {
'Node A': {
main: [
[{ node: 'Node B', type: 'main', index: 0 }],
[{ node: 'Node B', type: 'main', index: 1 }],
[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }],
[{ node: 'Node B', type: NodeConnectionType.Main, index: 1 }],
],
},
};
@@ -153,11 +153,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node A',
source: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
@@ -171,11 +171,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node A',
source: {
index: 1,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 1,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
@@ -186,8 +186,8 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
const legacyConnections: IConnections = {
'Node A': {
main: [
[{ node: 'Node B', type: 'main', index: 0 }],
[{ node: 'Node C', type: 'main', index: 0 }],
[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }],
[{ node: 'Node C', type: NodeConnectionType.Main, index: 0 }],
],
},
};
@@ -234,11 +234,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node A',
source: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
@@ -252,11 +252,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node A',
source: {
index: 1,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
@@ -266,11 +266,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
it('should map complex node setup with mixed inputs and outputs', () => {
const legacyConnections: IConnections = {
'Node A': {
main: [[{ node: 'Node B', type: 'main', index: 0 }]],
other: [[{ node: 'Node C', type: 'other', index: 1 }]],
main: [[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }]],
[NodeConnectionType.AiMemory]: [
[{ node: 'Node C', type: NodeConnectionType.AiMemory, index: 1 }],
],
},
'Node B': {
main: [[{ node: 'Node C', type: 'main', index: 0 }]],
main: [[{ node: 'Node C', type: NodeConnectionType.Main, index: 0 }]],
},
};
const nodes: INodeUi[] = [
@@ -316,29 +318,29 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node A',
source: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
{
id: '[1/other/0][3/other/1]',
id: `[1/${NodeConnectionType.AiMemory}/0][3/${NodeConnectionType.AiMemory}/1]`,
source: '1',
target: '3',
sourceHandle: 'outputs/other/0',
targetHandle: 'inputs/other/1',
sourceHandle: `outputs/${NodeConnectionType.AiMemory}/0`,
targetHandle: `inputs/${NodeConnectionType.AiMemory}/1`,
data: {
fromNodeName: 'Node A',
source: {
index: 0,
type: 'other',
type: NodeConnectionType.AiMemory,
},
target: {
index: 1,
type: 'other',
type: NodeConnectionType.AiMemory,
},
},
},
@@ -352,11 +354,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node B',
source: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
@@ -367,8 +369,8 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
const legacyConnections: IConnections = {
'Node A': {
main: [
[{ node: 'Nonexistent Node', type: 'main', index: 0 }],
[{ node: 'Node B', type: 'main', index: 0 }],
[{ node: 'Nonexistent Node', type: NodeConnectionType.Main, index: 0 }],
[{ node: 'Node B', type: NodeConnectionType.Main, index: 0 }],
],
},
};
@@ -407,11 +409,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
fromNodeName: 'Node A',
source: {
index: 1,
type: 'main',
type: NodeConnectionType.Main,
},
target: {
index: 0,
type: 'main',
type: NodeConnectionType.Main,
},
},
},
@@ -435,66 +437,69 @@ describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
});
it('should map string endpoints correctly', () => {
const endpoints: INodeTypeDescription['inputs'] = ['main', 'ai_tool'];
const endpoints: INodeTypeDescription['inputs'] = [
NodeConnectionType.Main,
NodeConnectionType.AiTool,
];
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
expect(result).toEqual([
{ type: 'main', index: 0, label: undefined },
{ type: 'ai_tool', index: 0, label: undefined },
{ type: NodeConnectionType.Main, index: 0, label: undefined },
{ type: NodeConnectionType.AiTool, index: 0, label: undefined },
]);
});
it('should map object endpoints correctly', () => {
const endpoints: INodeTypeDescription['inputs'] = [
{ type: 'main', displayName: 'Main Input' },
{ type: 'ai_tool', displayName: 'AI Tool', required: true },
{ type: NodeConnectionType.Main, displayName: 'Main Input' },
{ type: NodeConnectionType.AiTool, displayName: 'AI Tool', required: true },
];
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
expect(result).toEqual([
{ type: 'main', index: 0, label: 'Main Input' },
{ type: 'ai_tool', index: 0, label: 'AI Tool', required: true },
{ type: NodeConnectionType.Main, index: 0, label: 'Main Input' },
{ type: NodeConnectionType.AiTool, index: 0, label: 'AI Tool', required: true },
]);
});
it('should map mixed string and object endpoints correctly', () => {
const endpoints: INodeTypeDescription['inputs'] = [
'main',
{ type: 'ai_tool', displayName: 'AI Tool' },
'main',
NodeConnectionType.Main,
{ type: NodeConnectionType.AiTool, displayName: 'AI Tool' },
NodeConnectionType.Main,
];
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
expect(result).toEqual([
{ type: 'main', index: 0, label: undefined },
{ type: 'ai_tool', index: 0, label: 'AI Tool' },
{ type: 'main', index: 1, label: undefined },
{ type: NodeConnectionType.Main, index: 0, label: undefined },
{ type: NodeConnectionType.AiTool, index: 0, label: 'AI Tool' },
{ type: NodeConnectionType.Main, index: 1, label: undefined },
]);
});
it('should handle multiple same type object endpoints', () => {
const endpoints: INodeTypeDescription['inputs'] = [
{ type: 'main', displayName: 'Main Input' },
{ type: 'main', displayName: 'Secondary Main Input' },
{ type: NodeConnectionType.Main, displayName: 'Main Input' },
{ type: NodeConnectionType.Main, displayName: 'Secondary Main Input' },
];
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
expect(result).toEqual([
{ type: 'main', index: 0, label: 'Main Input' },
{ type: 'main', index: 1, label: 'Secondary Main Input' },
{ type: NodeConnectionType.Main, index: 0, label: 'Main Input' },
{ type: NodeConnectionType.Main, index: 1, label: 'Secondary Main Input' },
]);
});
it('should map required and non-required endpoints correctly', () => {
const endpoints: INodeTypeDescription['inputs'] = [
{ type: 'main', displayName: 'Main Input', required: true },
{ type: 'ai_tool', displayName: 'Optional Tool', required: false },
{ type: NodeConnectionType.Main, displayName: 'Main Input', required: true },
{ type: NodeConnectionType.AiTool, displayName: 'Optional Tool', required: false },
];
const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints);
expect(result).toEqual([
{ type: 'main', index: 0, label: 'Main Input', required: true },
{ type: 'ai_tool', index: 0, label: 'Optional Tool' },
{ type: NodeConnectionType.Main, index: 0, label: 'Main Input', required: true },
{ type: NodeConnectionType.AiTool, index: 0, label: 'Optional Tool' },
]);
});
});

View File

@@ -1,6 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker/locale/en';
import type { ITemplatesWorkflowFull, IWorkflowTemplateNode } from '@/Interface';
import { NodeConnectionType } from 'n8n-workflow';
export const newWorkflowTemplateNode = ({
type,
@@ -26,6 +27,7 @@ export const fullShopifyTelegramTwitterTemplate = {
workflow: {
nodes: [
{
id: 'd65f8060-0196-430a-923c-57f838991cc1',
name: 'Twitter',
type: 'n8n-nodes-base.twitter',
position: [720, -220],
@@ -39,6 +41,7 @@ export const fullShopifyTelegramTwitterTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991dd3',
name: 'Telegram',
type: 'n8n-nodes-base.telegram',
position: [720, -20],
@@ -53,6 +56,7 @@ export const fullShopifyTelegramTwitterTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991dd2',
name: 'product created',
type: 'n8n-nodes-base.shopifyTrigger',
position: [540, -110],
@@ -72,12 +76,12 @@ export const fullShopifyTelegramTwitterTemplate = {
[
{
node: 'Twitter',
type: 'main',
type: NodeConnectionType.Main,
index: 0,
},
{
node: 'Telegram',
type: 'main',
type: NodeConnectionType.Main,
index: 0,
},
],
@@ -195,6 +199,7 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = {
workflow: {
nodes: [
{
id: 'd65f8060-0196-430a-923c-57f8389911f3',
name: 'IMAP Email',
type: 'n8n-nodes-base.emailReadImap',
position: [240, 420],
@@ -206,6 +211,7 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991gg2',
name: 'Nextcloud',
type: 'n8n-nodes-base.nextCloud',
position: [940, 420],
@@ -217,6 +223,7 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991ddh',
name: 'Map each attachment',
type: 'n8n-nodes-base.function',
position: [620, 420],
@@ -228,8 +235,12 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = {
},
],
connections: {
'IMAP Email': { main: [[{ node: 'Map each attachment', type: 'main', index: 0 }]] },
'Map each attachment': { main: [[{ node: 'Nextcloud', type: 'main', index: 0 }]] },
'IMAP Email': {
main: [[{ node: 'Map each attachment', type: NodeConnectionType.Main, index: 0 }]],
},
'Map each attachment': {
main: [[{ node: 'Nextcloud', type: NodeConnectionType.Main, index: 0 }]],
},
},
},
workflowInfo: {
@@ -303,6 +314,7 @@ export const fullCreateApiEndpointTemplate = {
workflow: {
nodes: [
{
id: 'd65f8060-0196-430a-923c-57f838991dd1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
position: [375, 115],
@@ -315,6 +327,7 @@ export const fullCreateApiEndpointTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991dd9',
name: 'Note1',
type: 'n8n-nodes-base.stickyNote',
position: [355, -25],
@@ -327,6 +340,7 @@ export const fullCreateApiEndpointTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991dd5',
name: 'Respond to Webhook',
type: 'n8n-nodes-base.respondToWebhook',
position: [815, 115],
@@ -339,6 +353,7 @@ export const fullCreateApiEndpointTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991df1',
name: 'Create URL string',
type: 'n8n-nodes-base.set',
position: [595, 115],
@@ -358,6 +373,7 @@ export const fullCreateApiEndpointTemplate = {
typeVersion: 1,
},
{
id: 'd65f8060-0196-430a-923c-57f838991dbb',
name: 'Note3',
type: 'n8n-nodes-base.stickyNote',
position: [355, 275],
@@ -371,8 +387,10 @@ export const fullCreateApiEndpointTemplate = {
},
],
connections: {
Webhook: { main: [[{ node: 'Create URL string', type: 'main', index: 0 }]] },
'Create URL string': { main: [[{ node: 'Respond to Webhook', type: 'main', index: 0 }]] },
Webhook: { main: [[{ node: 'Create URL string', type: NodeConnectionType.Main, index: 0 }]] },
'Create URL string': {
main: [[{ node: 'Respond to Webhook', type: NodeConnectionType.Main, index: 0 }]],
},
},
},
lastUpdatedBy: 1,

View File

@@ -6,6 +6,8 @@ import type {
} from 'n8n-workflow';
import { nodeConnectionTypes } from 'n8n-workflow';
import type { ICredentialsResponse, NewCredentialsModal } from '@/Interface';
import type { jsPlumbDOMElement } from '@jsplumb/browser-ui';
import type { Connection } from '@jsplumb/core';
/*
Type guards used in editor-ui project
@@ -46,10 +48,14 @@ export const isResourceMapperValue = (value: unknown): value is string | number
return ['string', 'number', 'boolean'].includes(typeof value);
};
export const isJSPlumbEndpointElement = (element: Node): element is HTMLElement => {
export const isJSPlumbEndpointElement = (element: Node): element is jsPlumbDOMElement => {
return 'jtk' in element && 'endpoint' in (element.jtk as object);
};
export const isJSPlumbConnection = (connection: unknown): connection is Connection => {
return connection !== null && typeof connection === 'object' && 'connector' in connection;
};
export function isDateObject(date: unknown): date is Date {
return (
!!date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as number)