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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 587 additions and 389 deletions

View file

@ -136,6 +136,8 @@ export type EndpointStyle = {
export type EndpointMeta = { export type EndpointMeta = {
__meta?: { __meta?: {
nodeName: string;
nodeId: string;
index: number; index: number;
totalEndpoints: number; totalEndpoints: number;
endpointLabelLength: number; endpointLabelLength: number;
@ -247,7 +249,7 @@ export interface IWorkflowData {
export interface IWorkflowDataUpdate { export interface IWorkflowDataUpdate {
id?: string; id?: string;
name?: string; name?: string;
nodes?: Array<INode | IWorkflowTemplateNode>; nodes?: INode[];
connections?: IConnections; connections?: IConnections;
settings?: IWorkflowSettings; settings?: IWorkflowSettings;
active?: boolean; active?: boolean;
@ -268,7 +270,10 @@ export interface NewWorkflowResponse {
} }
export interface IWorkflowTemplateNode export interface IWorkflowTemplateNode
extends Pick<INodeUi, 'name' | 'type' | 'position' | 'parameters' | 'typeVersion' | 'webhookId'> { extends Pick<
INodeUi,
'name' | 'type' | 'position' | 'parameters' | 'typeVersion' | 'webhookId' | 'id' | 'disabled'
> {
// The credentials in a template workflow have a different type than in a regular workflow // The credentials in a template workflow have a different type than in a regular workflow
credentials?: IWorkflowTemplateNodeCredentials; credentials?: IWorkflowTemplateNodeCredentials;
} }
@ -1926,13 +1931,13 @@ export type NewConnectionInfo = {
index: number; index: number;
eventSource: NodeCreatorOpenSource; eventSource: NodeCreatorOpenSource;
connection?: Connection; connection?: Connection;
nodeCreatorView?: string; nodeCreatorView?: NodeFilterType;
outputType?: NodeConnectionType; outputType?: NodeConnectionType;
endpointUuid?: string; endpointUuid?: string;
}; };
export type AIAssistantConnectionInfo = NewConnectionInfo & { export type AIAssistantConnectionInfo = NewConnectionInfo & {
stepName: string; stepName?: string;
}; };
export type EnterpriseEditionFeatureKey = export type EnterpriseEditionFeatureKey =

View file

@ -5,12 +5,11 @@ import ChatComponent from '@n8n/chat/components/Chat.vue';
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants'; import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types'; import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { computed, provide, ref } from 'vue'; import { computed, provide, ref, onMounted, onBeforeUnmount } from 'vue';
import QuickReplies from './QuickReplies.vue'; import QuickReplies from './QuickReplies.vue';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useAIStore } from '@/stores/ai.store'; import { useAIStore } from '@/stores/ai.store';
import { chatEventBus } from '@n8n/chat/event-buses'; import { chatEventBus } from '@n8n/chat/event-buses';
import { onMounted } from 'vue';
import { import {
AI_ASSISTANT_EXPERIMENT_URLS, AI_ASSISTANT_EXPERIMENT_URLS,
AI_ASSISTANT_LOCAL_STORAGE_KEY, AI_ASSISTANT_LOCAL_STORAGE_KEY,
@ -19,7 +18,6 @@ import {
import { useStorage } from '@/composables/useStorage'; import { useStorage } from '@/composables/useStorage';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { onBeforeUnmount } from 'vue';
const locale = useI18n(); const locale = useI18n();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@ -93,7 +91,7 @@ const thanksResponses: ChatMessage[] = [
]; ];
const initialMessageText = computed(() => { const initialMessageText = computed(() => {
if (latestConnectionInfo.value) { if (latestConnectionInfo.value?.stepName) {
return locale.baseText('aiAssistantChat.initialMessage.nextStep', { return locale.baseText('aiAssistantChat.initialMessage.nextStep', {
interpolate: { currentAction: latestConnectionInfo.value.stepName }, interpolate: { currentAction: latestConnectionInfo.value.stepName },
}); });

View file

@ -54,7 +54,6 @@ import type {
} from '@/Interface'; } from '@/Interface';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import type { MessageBoxInputData } from 'element-plus';
import type { BaseTextKey } from '../../plugins/i18n'; import type { BaseTextKey } from '../../plugins/i18n';
const props = defineProps<{ const props = defineProps<{
@ -385,7 +384,7 @@ async function handleFileImport(): Promise<void> {
} }
} }
async function onWorkflowMenuSelect(action: string): Promise<void> { async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void> {
switch (action) { switch (action) {
case WORKFLOW_MENU_ACTIONS.DUPLICATE: { case WORKFLOW_MENU_ACTIONS.DUPLICATE: {
uiStore.openModalWithData({ uiStore.openModalWithData({
@ -427,7 +426,7 @@ async function onWorkflowMenuSelect(action: string): Promise<void> {
} }
case WORKFLOW_MENU_ACTIONS.IMPORT_FROM_URL: { case WORKFLOW_MENU_ACTIONS.IMPORT_FROM_URL: {
try { try {
const promptResponse = (await message.prompt( const promptResponse = await message.prompt(
locale.baseText('mainSidebar.prompt.workflowUrl') + ':', locale.baseText('mainSidebar.prompt.workflowUrl') + ':',
locale.baseText('mainSidebar.prompt.importWorkflowFromUrl') + ':', locale.baseText('mainSidebar.prompt.importWorkflowFromUrl') + ':',
{ {
@ -436,9 +435,9 @@ async function onWorkflowMenuSelect(action: string): Promise<void> {
inputErrorMessage: locale.baseText('mainSidebar.prompt.invalidUrl'), inputErrorMessage: locale.baseText('mainSidebar.prompt.invalidUrl'),
inputPattern: /^http[s]?:\/\/.*\.json$/i, inputPattern: /^http[s]?:\/\/.*\.json$/i,
}, },
)) as MessageBoxInputData; );
if ((promptResponse as unknown as string) === 'cancel') { if (promptResponse.action === 'cancel') {
return; return;
} }

View file

@ -204,8 +204,8 @@ export default function useCanvasMouseSelect() {
uiStore.lastSelectedNode = null; uiStore.lastSelectedNode = null;
uiStore.lastSelectedNodeOutputIndex = null; uiStore.lastSelectedNodeOutputIndex = null;
canvasStore.lastSelectedConnection = null;
canvasStore.newNodeInsertPosition = null; canvasStore.newNodeInsertPosition = null;
canvasStore.setLastSelectedConnection(undefined);
} }
const instance = computed(() => canvasStore.jsPlumbInstance); const instance = computed(() => canvasStore.jsPlumbInstance);

View file

@ -70,7 +70,7 @@ export function useCanvasPanning(
/** /**
* Ends the panning process and removes the mousemove event listener * Ends the panning process and removes the mousemove event listener
*/ */
function onMouseUp(_: MouseEvent) { function onMouseUp() {
if (!uiStore.nodeViewMoveInProgress) { if (!uiStore.nodeViewMoveInProgress) {
// If it is not active return directly. // If it is not active return directly.
// Else normal node dragging will not work. // Else normal node dragging will not work.
@ -89,7 +89,7 @@ export function useCanvasPanning(
* Handles the actual movement of the canvas during a mouse drag, * Handles the actual movement of the canvas during a mouse drag,
* updating the position based on the current mouse position * updating the position based on the current mouse position
*/ */
function onMouseMove(e: MouseEvent) { function onMouseMove(e: MouseEvent | TouchEvent) {
const element = unref(elementRef); const element = unref(elementRef);
if (e.target && !(element === e.target || element?.contains(e.target as Node))) { if (e.target && !(element === e.target || element?.contains(e.target as Node))) {
return; return;
@ -100,11 +100,11 @@ export function useCanvasPanning(
} }
// Signal that moving canvas is active if middle button is pressed and mouse is moved // Signal that moving canvas is active if middle button is pressed and mouse is moved
if (e.buttons === MOUSE_EVENT_BUTTONS.MIDDLE) { if (e instanceof MouseEvent && e.buttons === MOUSE_EVENT_BUTTONS.MIDDLE) {
uiStore.nodeViewMoveInProgress = true; uiStore.nodeViewMoveInProgress = true;
} }
if (e.buttons === MOUSE_EVENT_BUTTONS.NONE) { if (e instanceof MouseEvent && e.buttons === MOUSE_EVENT_BUTTONS.NONE) {
// Mouse button is not pressed anymore so stop selection mode // Mouse button is not pressed anymore so stop selection mode
// Happens normally when mouse leave the view pressed and then // Happens normally when mouse leave the view pressed and then
// comes back unpressed. // comes back unpressed.

View file

@ -1,12 +1,19 @@
import type { ElMessageBoxOptions } from 'element-plus'; import type { ElMessageBoxOptions, Action, MessageBoxInputData } from 'element-plus';
import { ElMessageBox as MessageBox } from 'element-plus'; import { ElMessageBox as MessageBox } from 'element-plus';
export type MessageBoxConfirmResult = 'confirm' | 'cancel'; export type MessageBoxConfirmResult = 'confirm' | 'cancel';
export function useMessage() { export function useMessage() {
const handleCancelOrClose = (e: unknown) => { const handleCancelOrClose = (e: Action | Error): Action => {
if (e instanceof Error) throw e; if (e instanceof Error) throw e;
else return e;
return e;
};
const handleCancelOrClosePrompt = (e: Error | Action): MessageBoxInputData => {
if (e instanceof Error) throw e;
return { value: '', action: e };
}; };
async function alert( async function alert(
@ -15,7 +22,7 @@ export function useMessage() {
config?: ElMessageBoxOptions, config?: ElMessageBoxOptions,
) { ) {
const resolvedConfig = { const resolvedConfig = {
...(config || (typeof configOrTitle === 'object' ? configOrTitle : {})), ...(config ?? (typeof configOrTitle === 'object' ? configOrTitle : {})),
cancelButtonClass: 'btn--cancel', cancelButtonClass: 'btn--cancel',
confirmButtonClass: 'btn--confirm', confirmButtonClass: 'btn--confirm',
}; };
@ -32,24 +39,23 @@ export function useMessage() {
message: ElMessageBoxOptions['message'], message: ElMessageBoxOptions['message'],
configOrTitle?: string | ElMessageBoxOptions, configOrTitle?: string | ElMessageBoxOptions,
config?: ElMessageBoxOptions, config?: ElMessageBoxOptions,
): Promise<MessageBoxConfirmResult> { ) {
const resolvedConfig = { const resolvedConfig = {
...(config || (typeof configOrTitle === 'object' ? configOrTitle : {})), ...(config ?? (typeof configOrTitle === 'object' ? configOrTitle : {})),
cancelButtonClass: 'btn--cancel', cancelButtonClass: 'btn--cancel',
confirmButtonClass: 'btn--confirm', confirmButtonClass: 'btn--confirm',
distinguishCancelAndClose: true, distinguishCancelAndClose: true,
showClose: config?.showClose || false, showClose: config?.showClose ?? false,
closeOnClickModal: false, closeOnClickModal: false,
}; };
if (typeof configOrTitle === 'string') { if (typeof configOrTitle === 'string') {
return await (MessageBox.confirm(message, configOrTitle, resolvedConfig).catch( return await MessageBox.confirm(message, configOrTitle, resolvedConfig).catch(
handleCancelOrClose, handleCancelOrClose,
) as unknown as Promise<MessageBoxConfirmResult>); );
} }
return await (MessageBox.confirm(message, resolvedConfig).catch(
handleCancelOrClose, return await MessageBox.confirm(message, resolvedConfig).catch(handleCancelOrClose);
) as unknown as Promise<MessageBoxConfirmResult>);
} }
async function prompt( async function prompt(
@ -58,17 +64,17 @@ export function useMessage() {
config?: ElMessageBoxOptions, config?: ElMessageBoxOptions,
) { ) {
const resolvedConfig = { const resolvedConfig = {
...(config || (typeof configOrTitle === 'object' ? configOrTitle : {})), ...(config ?? (typeof configOrTitle === 'object' ? configOrTitle : {})),
cancelButtonClass: 'btn--cancel', cancelButtonClass: 'btn--cancel',
confirmButtonClass: 'btn--confirm', confirmButtonClass: 'btn--confirm',
}; };
if (typeof configOrTitle === 'string') { if (typeof configOrTitle === 'string') {
return await MessageBox.prompt(message, configOrTitle, resolvedConfig).catch( return await MessageBox.prompt(message, configOrTitle, resolvedConfig).catch(
handleCancelOrClose, handleCancelOrClosePrompt,
); );
} }
return await MessageBox.prompt(message, resolvedConfig).catch(handleCancelOrClose); return await MessageBox.prompt(message, resolvedConfig).catch(handleCancelOrClosePrompt);
} }
return { return {

View file

@ -39,6 +39,7 @@ import type {
IWorkflowData, IWorkflowData,
IWorkflowDataUpdate, IWorkflowDataUpdate,
IWorkflowDb, IWorkflowDb,
IWorkflowTemplateNode,
TargetItem, TargetItem,
XYPosition, XYPosition,
} from '@/Interface'; } from '@/Interface';
@ -307,7 +308,11 @@ function getNodes(): INodeUi[] {
} }
// Returns a workflow instance. // Returns a workflow instance.
function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { function getWorkflow(
nodes: Array<INodeUi | IWorkflowTemplateNode>,
connections: IConnections,
copyData?: boolean,
): Workflow {
return useWorkflowsStore().getWorkflow(nodes, connections, copyData); return useWorkflowsStore().getWorkflow(nodes, connections, copyData);
} }

View file

@ -398,7 +398,6 @@ export const ROLE_OTHER = 'other';
/** END OF PERSONALIZATION SURVEY */ /** END OF PERSONALIZATION SURVEY */
export const MODAL_CANCEL = 'cancel'; export const MODAL_CANCEL = 'cancel';
export const MODAL_CLOSE = 'close';
export const MODAL_CONFIRM = 'confirm'; export const MODAL_CONFIRM = 'confirm';
export const VALID_EMAIL_REGEX = export const VALID_EMAIL_REGEX =

34
packages/editor-ui/src/jsplumb.d.ts vendored Normal file
View file

@ -0,0 +1,34 @@
import type { Connection, Endpoint, EndpointRepresentation, AbstractConnector, Overlay } from '@jsplumb/core';
import type { NodeConnectionType } from 'n8n-workflow';
declare module '@jsplumb/core' {
interface EndpointRepresentation {
canvas: HTMLElement;
scope: NodeConnectionType;
}
interface AbstractConnector {
canvas: HTMLElement;
overrideTargetEndpoint: Endpoint;
}
interface Overlay {
canvas: HTMLElement;
}
interface Connection {
__meta: {
sourceOutputIndex: number;
targetNodeName: string;
targetOutputIndex: number;
sourceNodeName: string;
};
}
interface Endpoint {
scope: NodeConnectionType;
__meta: {
nodeName: string;
nodeId: string;
index: number;
totalEndpoints: number;
endpointLabelLength: number;
};
};
}

View file

@ -165,7 +165,7 @@ export const routes = [
// Templates view remembers it's scroll position on back // Templates view remembers it's scroll position on back
scrollOffset: 0, scrollOffset: 0,
telemetry: { telemetry: {
getProperties(route: RouteLocation) { getProperties() {
const templatesStore = useTemplatesStore(); const templatesStore = useTemplatesStore();
return { return {
wf_template_repo_session_id: templatesStore.currentSessionId, wf_template_repo_session_id: templatesStore.currentSessionId,
@ -474,7 +474,7 @@ export const routes = [
}, },
telemetry: { telemetry: {
pageCategory: 'settings', pageCategory: 'settings',
getProperties(route: RouteLocation) { getProperties() {
return { return {
feature: 'usage', feature: 'usage',
}; };
@ -492,7 +492,7 @@ export const routes = [
middleware: ['authenticated'], middleware: ['authenticated'],
telemetry: { telemetry: {
pageCategory: 'settings', pageCategory: 'settings',
getProperties(route: RouteLocation) { getProperties() {
return { return {
feature: 'personal', feature: 'personal',
}; };
@ -515,7 +515,7 @@ export const routes = [
}, },
telemetry: { telemetry: {
pageCategory: 'settings', pageCategory: 'settings',
getProperties(route: RouteLocation) { getProperties() {
return { return {
feature: 'users', feature: 'users',
}; };
@ -533,7 +533,7 @@ export const routes = [
middleware: ['authenticated'], middleware: ['authenticated'],
telemetry: { telemetry: {
pageCategory: 'settings', pageCategory: 'settings',
getProperties(route: RouteLocation) { getProperties() {
return { return {
feature: 'api', feature: 'api',
}; };
@ -556,7 +556,7 @@ export const routes = [
}, },
telemetry: { telemetry: {
pageCategory: 'settings', pageCategory: 'settings',
getProperties(route: RouteLocation) { getProperties() {
return { return {
feature: 'environments', feature: 'environments',
}; };
@ -579,7 +579,7 @@ export const routes = [
}, },
telemetry: { telemetry: {
pageCategory: 'settings', pageCategory: 'settings',
getProperties(route: RouteLocation) { getProperties() {
return { return {
feature: 'external-secrets', feature: 'external-secrets',
}; };
@ -606,7 +606,7 @@ export const routes = [
}, },
telemetry: { telemetry: {
pageCategory: 'settings', pageCategory: 'settings',
getProperties(route: RouteLocation) { getProperties() {
return { return {
feature: 'sso', feature: 'sso',
}; };

View file

@ -22,6 +22,7 @@ declare global {
BASE_PATH: string; BASE_PATH: string;
REST_ENDPOINT: string; REST_ENDPOINT: string;
n8nExternalHooks?: PartialDeep<ExternalHooks>; n8nExternalHooks?: PartialDeep<ExternalHooks>;
preventNodeViewBeforeUnload?: boolean;
} }
namespace JSX { namespace JSX {

View file

@ -52,7 +52,8 @@ export const useCanvasStore = defineStore('canvas', () => {
const jsPlumbInstanceRef = ref<BrowserJsPlumbInstance>(); const jsPlumbInstanceRef = ref<BrowserJsPlumbInstance>();
const isDragging = ref<boolean>(false); const isDragging = ref<boolean>(false);
const lastSelectedConnection = ref<Connection | null>(null); const lastSelectedConnection = ref<Connection>();
const newNodeInsertPosition = ref<XYPosition | null>(null); const newNodeInsertPosition = ref<XYPosition | null>(null);
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes); const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
@ -68,6 +69,9 @@ export const useCanvasStore = defineStore('canvas', () => {
const nodeViewScale = ref<number>(1); const nodeViewScale = ref<number>(1);
const canvasAddButtonPosition = ref<XYPosition>([1, 1]); const canvasAddButtonPosition = ref<XYPosition>([1, 1]);
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly); const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
const lastSelectedConnectionComputed = computed<Connection | undefined>(
() => lastSelectedConnection.value,
);
watch(readOnlyEnv, (readOnly) => { watch(readOnlyEnv, (readOnly) => {
if (jsPlumbInstanceRef.value) { if (jsPlumbInstanceRef.value) {
@ -75,6 +79,10 @@ export const useCanvasStore = defineStore('canvas', () => {
} }
}); });
const setLastSelectedConnection = (connection: Connection | undefined) => {
lastSelectedConnection.value = connection;
};
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => { const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
const position = getMidCanvasPosition(nodeViewScale.value, offset ?? [0, 0]); const position = getMidCanvasPosition(nodeViewScale.value, offset ?? [0, 0]);
@ -314,11 +322,12 @@ export const useCanvasStore = defineStore('canvas', () => {
isDemo, isDemo,
nodeViewScale, nodeViewScale,
canvasAddButtonPosition, canvasAddButtonPosition,
lastSelectedConnection,
newNodeInsertPosition, newNodeInsertPosition,
jsPlumbInstance, jsPlumbInstance,
isLoading: loadingService.isLoading, isLoading: loadingService.isLoading,
aiNodes, aiNodes,
lastSelectedConnection: lastSelectedConnectionComputed,
setLastSelectedConnection,
startLoading: loadingService.startLoading, startLoading: loadingService.startLoading,
setLoadingText: loadingService.setLoadingText, setLoadingText: loadingService.setLoadingText,
stopLoading: loadingService.stopLoading, stopLoading: loadingService.stopLoading,

View file

@ -30,6 +30,7 @@ import type {
NodeMetadataMap, NodeMetadataMap,
WorkflowMetadata, WorkflowMetadata,
IExecutionFlattedResponse, IExecutionFlattedResponse,
IWorkflowTemplateNode,
} from '@/Interface'; } from '@/Interface';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import type { import type {
@ -312,9 +313,31 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
setNodeValue({ name: node.name, key: 'position', value: position }); setNodeValue({ name: node.name, key: 'position', value: position });
} }
function convertTemplateNodeToNodeUi(node: IWorkflowTemplateNode): INodeUi {
const filteredCredentials = Object.keys(node.credentials ?? {}).reduce<INodeCredentials>(
(credentials, curr) => {
const credential = node?.credentials?.[curr];
if (!credential || typeof credential === 'string') {
return credentials;
}
credentials[curr] = credential;
return credentials;
},
{},
);
return {
...node,
credentials: filteredCredentials,
};
}
function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow {
const nodeTypes = getNodeTypes(); const nodeTypes = getNodeTypes();
let cachedWorkflowId: string | undefined = workflowId.value; let cachedWorkflowId: string | undefined = workflowId.value;
if (cachedWorkflowId && cachedWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { if (cachedWorkflowId && cachedWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
cachedWorkflowId = undefined; cachedWorkflowId = undefined;
} }
@ -327,7 +350,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
active: false, active: false,
nodeTypes, nodeTypes,
settings: workflowSettings.value, settings: workflowSettings.value,
// @ts-ignore
pinData: pinnedWorkflowData.value, pinData: pinnedWorkflowData.value,
}); });
@ -1520,6 +1542,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
getPinDataSize, getPinDataSize,
getNodeTypes, getNodeTypes,
getNodes, getNodes,
convertTemplateNodeToNodeUi,
getWorkflow, getWorkflow,
getCurrentWorkflow, getCurrentWorkflow,
getWorkflowFromUrl, getWorkflowFromUrl,

View file

@ -25,6 +25,7 @@ import type {
INodeUpdatePropertiesInformation, INodeUpdatePropertiesInformation,
IPersonalizationLatestVersion, IPersonalizationLatestVersion,
IWorkflowDb, IWorkflowDb,
IWorkflowTemplateNode,
NodeFilterType, NodeFilterType,
} from '@/Interface'; } from '@/Interface';
import type { ComponentPublicInstance } from 'vue/dist/vue'; import type { ComponentPublicInstance } from 'vue/dist/vue';
@ -68,6 +69,7 @@ export interface ExternalHooks {
addNodeButton: Array<ExternalHooksMethod<{ nodeTypeName: string }>>; addNodeButton: Array<ExternalHooksMethod<{ nodeTypeName: string }>>;
onRunNode: Array<ExternalHooksMethod<ITelemetryTrackProperties>>; onRunNode: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
onRunWorkflow: Array<ExternalHooksMethod<ITelemetryTrackProperties>>; onRunWorkflow: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
onOpenChat: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
}; };
main: { main: {
routeChange: Array<ExternalHooksMethod<{ to: RouteLocation; from: RouteLocation }>>; routeChange: Array<ExternalHooksMethod<{ to: RouteLocation; from: RouteLocation }>>;
@ -255,7 +257,7 @@ export interface ExternalHooks {
ExternalHooksMethod<{ ExternalHooksMethod<{
templateId: string; templateId: string;
templateName: string; templateName: string;
workflow: { nodes: INodeUi[]; connections: IConnections }; workflow: { nodes: INodeUi[] | IWorkflowTemplateNode[]; connections: IConnections };
}> }>
>; >;
}; };

View file

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

View file

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

View file

@ -6,6 +6,8 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { nodeConnectionTypes } from 'n8n-workflow'; import { nodeConnectionTypes } from 'n8n-workflow';
import type { ICredentialsResponse, NewCredentialsModal } from '@/Interface'; 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 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); 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); 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 { export function isDateObject(date: unknown): date is Date {
return ( return (
!!date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as number) !!date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as number)

View file

@ -42,6 +42,7 @@ import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { useCollaborationStore } from '@/stores/collaboration.store'; import { useCollaborationStore } from '@/stores/collaboration.store';
import { getUniqueNodeName } from '@/utils/canvasUtilsV2'; import { getUniqueNodeName } from '@/utils/canvasUtilsV2';
import { isValidNodeConnectionType } from '@/utils/typeGuards';
const NodeCreation = defineAsyncComponent( const NodeCreation = defineAsyncComponent(
async () => await import('@/components/Node/NodeCreation.vue'), async () => await import('@/components/Node/NodeCreation.vue'),
@ -217,13 +218,17 @@ function onCreateNodeConnection(connection: Connection) {
const sourceNodeId = connection.source; const sourceNodeId = connection.source;
const sourceNode = workflowsStore.getNodeById(sourceNodeId); const sourceNode = workflowsStore.getNodeById(sourceNodeId);
const sourceNodeName = sourceNode?.name ?? ''; const sourceNodeName = sourceNode?.name ?? '';
const [, sourceType, sourceIndex] = (connection.sourceHandle ?? '').split('/'); const [, sourceType, sourceIndex] = (connection.sourceHandle ?? '')
.split('/')
.filter(isValidNodeConnectionType);
// Input // Input
const targetNodeId = connection.target; const targetNodeId = connection.target;
const targetNode = workflowsStore.getNodeById(targetNodeId); const targetNode = workflowsStore.getNodeById(targetNodeId);
const targetNodeName = targetNode?.name ?? ''; const targetNodeName = targetNode?.name ?? '';
const [, targetType, targetIndex] = (connection.targetHandle ?? '').split('/'); const [, targetType, targetIndex] = (connection.targetHandle ?? '')
.split('/')
.filter(isValidNodeConnectionType);
if (sourceNode && targetNode && !checkIfNodeConnectionIsAllowed(sourceNode, targetNode)) { if (sourceNode && targetNode && !checkIfNodeConnectionIsAllowed(sourceNode, targetNode)) {
return; return;
@ -248,7 +253,7 @@ function onCreateNodeConnection(connection: Connection) {
} }
// @TODO Figure out a way to improve this // @TODO Figure out a way to improve this
function checkIfNodeConnectionIsAllowed(sourceNode: INodeUi, targetNode: INodeUi): boolean { function checkIfNodeConnectionIsAllowed(_sourceNode: INodeUi, _targetNode: INodeUi): boolean {
// const targetNodeType = nodeTypesStore.getNodeType( // const targetNodeType = nodeTypesStore.getNodeType(
// targetNode.type, // targetNode.type,
// targetNode.typeVersion, // targetNode.typeVersion,
@ -341,7 +346,7 @@ async function onAddNodes(
) { ) {
let currentPosition = position; let currentPosition = position;
for (const { type, name, position: nodePosition, isAutoAdd, openDetail } of nodes) { for (const { type, name, position: nodePosition, isAutoAdd, openDetail } of nodes) {
const node = await addNode( const _node = await addNode(
{ {
name, name,
type, type,
@ -407,7 +412,7 @@ type AddNodeOptions = {
isAutoAdd?: boolean; isAutoAdd?: boolean;
}; };
async function addNode(node: AddNodeData, options: AddNodeOptions): Promise<INodeUi | undefined> { async function addNode(node: AddNodeData, _options: AddNodeOptions): Promise<INodeUi | undefined> {
if (!checkIfEditingIsAllowed()) { if (!checkIfEditingIsAllowed()) {
return; return;
} }
@ -663,11 +668,11 @@ async function createNodeWithDefaultCredentials(node: Partial<INodeUi>) {
* @TODO Probably not needed and can be merged into addNode * @TODO Probably not needed and can be merged into addNode
*/ */
async function injectNode( async function injectNode(
nodeTypeName: string, _nodeTypeName: string,
options: AddNodeOptions = {}, _options: AddNodeOptions = {},
showDetail = true, _showDetail = true,
trackHistory = false, _trackHistory = false,
isAutoAdd = false, _isAutoAdd = false,
) { ) {
// const nodeTypeData: INodeTypeDescription | null = // const nodeTypeData: INodeTypeDescription | null =
// this.nodeTypesStore.getNodeType(nodeTypeName); // this.nodeTypesStore.getNodeType(nodeTypeName);

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,7 @@ const SettingsView = defineComponent({
components: { components: {
SettingsSidebar, SettingsSidebar,
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(_to, from, next) {
next((vm) => { next((vm) => {
(vm as unknown as InstanceType<typeof SettingsView>).previousRoute = from; (vm as unknown as InstanceType<typeof SettingsView>).previousRoute = from;
}); });

View file

@ -29,7 +29,7 @@ const openWorkflowTemplate = async (templateId: string) => {
const workflow = await workflowsStore.createNewWorkflow({ const workflow = await workflowsStore.createNewWorkflow({
name, name,
connections: template.workflow.connections, connections: template.workflow.connections,
nodes: template.workflow.nodes, nodes: template.workflow.nodes.map(workflowsStore.convertTemplateNodeToNodeUi),
pinData: template.workflow.pinData, pinData: template.workflow.pinData,
settings: template.workflow.settings, settings: template.workflow.settings,
meta: { meta: {

View file

@ -68,7 +68,7 @@ export interface IConnection {
node: string; node: string;
// The type of the input on destination node (for example "main") // The type of the input on destination node (for example "main")
type: string; type: NodeConnectionType;
// The output/input-index of destination node (if node has multiple inputs/outputs of the same type) // The output/input-index of destination node (if node has multiple inputs/outputs of the same type)
index: number; index: number;

View file

@ -168,7 +168,8 @@ export class Workflow {
if (!connections.hasOwnProperty(sourceNode)) { if (!connections.hasOwnProperty(sourceNode)) {
continue; continue;
} }
for (const type in connections[sourceNode]) {
for (const type of Object.keys(connections[sourceNode]) as NodeConnectionType[]) {
if (!connections[sourceNode].hasOwnProperty(type)) { if (!connections[sourceNode].hasOwnProperty(type)) {
continue; continue;
} }