mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(core): Update LLM applications building support (no-changelog) (#7710)
extracted out of #7336 --------- Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com> Co-authored-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
parent
4a89504d54
commit
117962d473
|
@ -51,6 +51,10 @@ describe('Execution', () => {
|
||||||
.canvasNodeByName('Manual')
|
.canvasNodeByName('Manual')
|
||||||
.within(() => cy.get('.fa-check'))
|
.within(() => cy.get('.fa-check'))
|
||||||
.should('exist');
|
.should('exist');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('exist');
|
||||||
workflowPage.getters
|
workflowPage.getters
|
||||||
.canvasNodeByName('Set')
|
.canvasNodeByName('Set')
|
||||||
.within(() => cy.get('.fa-check'))
|
.within(() => cy.get('.fa-check'))
|
||||||
|
@ -120,8 +124,8 @@ describe('Execution', () => {
|
||||||
workflowPage.getters.clearExecutionDataButton().click();
|
workflowPage.getters.clearExecutionDataButton().click();
|
||||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||||
|
|
||||||
// Check warning toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||||
workflowPage.getters.warningToast().should('be.visible');
|
workflowPage.getters.successToast().should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should test webhook workflow', () => {
|
it('should test webhook workflow', () => {
|
||||||
|
@ -267,7 +271,7 @@ describe('Execution', () => {
|
||||||
workflowPage.getters.clearExecutionDataButton().click();
|
workflowPage.getters.clearExecutionDataButton().click();
|
||||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||||
|
|
||||||
// Check warning toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||||
workflowPage.getters.warningToast().should('be.visible');
|
workflowPage.getters.successToast().should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'cypress-real-events';
|
import 'cypress-real-events';
|
||||||
import { WorkflowPage } from '../pages';
|
import { WorkflowPage } from '../pages';
|
||||||
import { BACKEND_BASE_URL, N8N_AUTH_COOKIE } from '../constants';
|
import { BACKEND_BASE_URL, INSTANCE_MEMBERS, INSTANCE_OWNER, N8N_AUTH_COOKIE } from '../constants';
|
||||||
|
|
||||||
Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
||||||
return cy.get(`[data-test-id="${selector}"]`, ...args);
|
return cy.get(`[data-test-id="${selector}"]`, ...args);
|
||||||
|
@ -169,6 +169,13 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('push', (type, data) => {
|
||||||
|
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/push`, {
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('shouldNotHaveConsoleErrors', () => {
|
Cypress.Commands.add('shouldNotHaveConsoleErrors', () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
const spy = cy.spy(win.console, 'error');
|
const spy = cy.spy(win.console, 'error');
|
||||||
|
|
|
@ -39,6 +39,7 @@ declare global {
|
||||||
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
|
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
|
||||||
): void;
|
): void;
|
||||||
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
||||||
|
push(type: string, data: unknown): void;
|
||||||
shouldNotHaveConsoleErrors(): void;
|
shouldNotHaveConsoleErrors(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,8 @@
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@vitest/coverage-v8": "^0.33.0",
|
"@vitest/coverage-v8": "^0.33.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress-otp": "^1.0.3",
|
|
||||||
"cypress": "^12.17.2",
|
"cypress": "^12.17.2",
|
||||||
|
"cypress-otp": "^1.0.3",
|
||||||
"cypress-real-events": "^1.9.1",
|
"cypress-real-events": "^1.9.1",
|
||||||
"jest": "^29.6.2",
|
"jest": "^29.6.2",
|
||||||
"jest-environment-jsdom": "^29.6.2",
|
"jest-environment-jsdom": "^29.6.2",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
|
@ -13,8 +13,9 @@ import { License } from '@/License';
|
||||||
import { LICENSE_FEATURES, inE2ETests } from '@/constants';
|
import { LICENSE_FEATURES, inE2ETests } from '@/constants';
|
||||||
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
|
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
|
||||||
import type { UserSetupPayload } from '@/requests';
|
import type { UserSetupPayload } from '@/requests';
|
||||||
import type { BooleanLicenseFeature } from '@/Interfaces';
|
import type { BooleanLicenseFeature, IPushDataType } from '@/Interfaces';
|
||||||
import { MfaService } from '@/Mfa/mfa.service';
|
import { MfaService } from '@/Mfa/mfa.service';
|
||||||
|
import { Push } from '@/push';
|
||||||
|
|
||||||
if (!inE2ETests) {
|
if (!inE2ETests) {
|
||||||
console.error('E2E endpoints only allowed during E2E tests');
|
console.error('E2E endpoints only allowed during E2E tests');
|
||||||
|
@ -51,6 +52,16 @@ type ResetRequest = Request<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type PushRequest = Request<
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
type: IPushDataType;
|
||||||
|
sessionId: string;
|
||||||
|
data: object;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@NoAuthRequired()
|
@NoAuthRequired()
|
||||||
@RestController('/e2e')
|
@RestController('/e2e')
|
||||||
|
@ -95,6 +106,17 @@ export class E2EController {
|
||||||
await this.setupUserManagement(req.body.owner, req.body.members);
|
await this.setupUserManagement(req.body.owner, req.body.members);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/push')
|
||||||
|
async push(req: PushRequest) {
|
||||||
|
const pushInstance = Container.get(Push);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const sessionId = Object.keys(pushInstance.getBackend().connections as object)[0];
|
||||||
|
|
||||||
|
pushInstance.send(req.body.type, req.body.data, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch('/feature')
|
@Patch('/feature')
|
||||||
setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) {
|
setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) {
|
||||||
const { enabled, feature } = req.body;
|
const { enabled, feature } = req.body;
|
||||||
|
|
|
@ -63,6 +63,10 @@ export class Push extends EventEmitter {
|
||||||
this.backend.send(type, data, sessionId);
|
this.backend.send(type, data, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBackend() {
|
||||||
|
return this.backend;
|
||||||
|
}
|
||||||
|
|
||||||
sendToUsers<D>(type: IPushDataType, data: D, userIds: Array<User['id']>) {
|
sendToUsers<D>(type: IPushDataType, data: D, userIds: Array<User['id']>) {
|
||||||
this.backend.sendToUsers(type, data, userIds);
|
this.backend.sendToUsers(type, data, userIds);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3301,9 +3301,11 @@ export function getExecuteFunctions(
|
||||||
// Display on the calling node which node has the error
|
// Display on the calling node which node has the error
|
||||||
throw new NodeOperationError(
|
throw new NodeOperationError(
|
||||||
connectedNode,
|
connectedNode,
|
||||||
`Error on node "${connectedNode.name}" which is connected via input "${inputName}"`,
|
`Error in sub-node ${connectedNode.name}`,
|
||||||
{
|
{
|
||||||
itemIndex,
|
itemIndex,
|
||||||
|
functionality: 'configuration-node',
|
||||||
|
description: error.message,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
:style="iconStyleData"
|
:style="iconStyleData"
|
||||||
>
|
>
|
||||||
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
||||||
<n8n-tooltip placement="top" :disabled="!showTooltip" v-if="showTooltip">
|
<n8n-tooltip :placement="tooltipPosition" :disabled="!showTooltip" v-if="showTooltip">
|
||||||
<template #content>{{ nodeTypeName }}</template>
|
<template #content>{{ nodeTypeName }}</template>
|
||||||
<div v-if="type !== 'unknown'" :class="$style.icon">
|
<div v-if="type !== 'unknown'" :class="$style.icon">
|
||||||
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
||||||
|
@ -78,6 +78,10 @@ export default defineComponent({
|
||||||
showTooltip: {
|
showTooltip: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
tooltipPosition: {
|
||||||
|
type: String,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
badge: { type: Object as PropType<{ src: string; type: string }> },
|
badge: { type: Object as PropType<{ src: string; type: string }> },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -66,7 +66,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
sanitizeHtml(text: string): string {
|
sanitizeHtml(text: string): string {
|
||||||
return sanitizeHtml(text, {
|
return sanitizeHtml(text, {
|
||||||
allowedAttributes: { a: ['data-key', 'href', 'target'] },
|
allowedAttributes: {
|
||||||
|
a: ['data-key', 'href', 'target', 'data-action', 'data-action-parameter-connectiontype'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onClick(event: MouseEvent) {
|
onClick(event: MouseEvent) {
|
||||||
|
|
|
@ -31,6 +31,13 @@
|
||||||
|
|
||||||
// Secondary tokens
|
// Secondary tokens
|
||||||
|
|
||||||
|
// LangChain
|
||||||
|
--color-lm-chat-messages-background: var(--prim-gray-820);
|
||||||
|
--color-lm-chat-bot-background: var(--prim-gray-540);
|
||||||
|
--color-lm-chat-bot-border: var(--prim-gray-490);
|
||||||
|
--color-lm-chat-user-background: var(--prim-color-alt-a-shade-100);
|
||||||
|
--color-lm-chat-user-border: var(--prim-color-alt-a);
|
||||||
|
|
||||||
// Canvas
|
// Canvas
|
||||||
--color-canvas-background: var(--prim-gray-820);
|
--color-canvas-background: var(--prim-gray-820);
|
||||||
--color-canvas-dot: var(--prim-gray-670);
|
--color-canvas-dot: var(--prim-gray-670);
|
||||||
|
@ -174,6 +181,11 @@
|
||||||
--border-color-light: var(--color-foreground-light);
|
--border-color-light: var(--color-foreground-light);
|
||||||
--border-base: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
--border-base: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
||||||
--node-type-supplemental-label-color-l: 100%;
|
--node-type-supplemental-label-color-l: 100%;
|
||||||
|
--node-type-supplemental-label-color: hsl(
|
||||||
|
var(--node-type-supplemental-label-color-h),
|
||||||
|
var(--node-type-supplemental-label-color-s),
|
||||||
|
var(--node-type-supplemental-label-color-l)
|
||||||
|
);
|
||||||
--color-configurable-node-name: var(--color-text-lighter);
|
--color-configurable-node-name: var(--color-text-lighter);
|
||||||
--color-secondary-link: var(--prim-color-secondary-tint-200);
|
--color-secondary-link: var(--prim-color-secondary-tint-200);
|
||||||
--color-secondary-link-hover: var(--prim-color-secondary-tint-100);
|
--color-secondary-link-hover: var(--prim-color-secondary-tint-100);
|
||||||
|
|
|
@ -64,6 +64,13 @@
|
||||||
|
|
||||||
// Secondary tokens
|
// Secondary tokens
|
||||||
|
|
||||||
|
// LangChain
|
||||||
|
--color-lm-chat-messages-background: var(--color-background-base);
|
||||||
|
--color-lm-chat-bot-background: var(--prim-gray-120);
|
||||||
|
--color-lm-chat-bot-border: var(--prim-gray-200);
|
||||||
|
--color-lm-chat-user-background: var(--prim-color-alt-a-tint-400);
|
||||||
|
--color-lm-chat-user-border: var(--prim-color-alt-a-tint-300);
|
||||||
|
|
||||||
// Canvas
|
// Canvas
|
||||||
--color-canvas-background: var(--prim-gray-10);
|
--color-canvas-background: var(--prim-gray-10);
|
||||||
--color-canvas-dot: var(--prim-gray-120);
|
--color-canvas-dot: var(--prim-gray-120);
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
"@faker-js/faker": "^8.0.2",
|
"@faker-js/faker": "^8.0.2",
|
||||||
"@pinia/testing": "^0.1.3",
|
"@pinia/testing": "^0.1.3",
|
||||||
"@sentry/vite-plugin": "^2.5.0",
|
"@sentry/vite-plugin": "^2.5.0",
|
||||||
|
"@testing-library/vue": "^7.0.0",
|
||||||
"@types/dateformat": "^3.0.0",
|
"@types/dateformat": "^3.0.0",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/humanize-duration": "^3.27.1",
|
"@types/humanize-duration": "^3.27.1",
|
||||||
|
|
|
@ -1269,6 +1269,7 @@ export type NodeCreatorOpenSource =
|
||||||
| 'tab'
|
| 'tab'
|
||||||
| 'node_connection_action'
|
| 'node_connection_action'
|
||||||
| 'node_connection_drop'
|
| 'node_connection_drop'
|
||||||
|
| 'notice_error_message'
|
||||||
| 'add_node_button';
|
| 'add_node_button';
|
||||||
|
|
||||||
export interface INodeCreatorState {
|
export interface INodeCreatorState {
|
||||||
|
|
47
packages/editor-ui/src/__tests__/defaults.ts
Normal file
47
packages/editor-ui/src/__tests__/defaults.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import type { INodeTypeData, INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
AGENT_NODE_TYPE,
|
||||||
|
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||||
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
} from '@/constants';
|
||||||
|
import nodeTypesJson from '../../../nodes-base/dist/types/nodes.json';
|
||||||
|
|
||||||
|
const allNodeTypes = [...nodeTypesJson];
|
||||||
|
|
||||||
|
function findNodeWithName(name: string): INodeTypeDescription {
|
||||||
|
return allNodeTypes.find((node) => node.name === name) as INodeTypeDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testingNodeTypes: INodeTypeData = {
|
||||||
|
[MANUAL_TRIGGER_NODE_TYPE]: {
|
||||||
|
sourcePath: '',
|
||||||
|
type: {
|
||||||
|
description: findNodeWithName(MANUAL_TRIGGER_NODE_TYPE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[MANUAL_CHAT_TRIGGER_NODE_TYPE]: {
|
||||||
|
sourcePath: '',
|
||||||
|
type: {
|
||||||
|
description: findNodeWithName(MANUAL_CHAT_TRIGGER_NODE_TYPE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[AGENT_NODE_TYPE]: {
|
||||||
|
sourcePath: '',
|
||||||
|
type: {
|
||||||
|
description: findNodeWithName(AGENT_NODE_TYPE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultMockNodeTypes: INodeTypeData = {
|
||||||
|
[MANUAL_TRIGGER_NODE_TYPE]: testingNodeTypes[MANUAL_TRIGGER_NODE_TYPE],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mockNodeTypesToArray(nodeTypes: INodeTypeData): INodeTypeDescription[] {
|
||||||
|
return Object.values(nodeTypes).map(
|
||||||
|
(nodeType) => nodeType.type.description as INodeTypeDescription,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultMockNodeTypesArray: INodeTypeDescription[] =
|
||||||
|
mockNodeTypesToArray(defaultMockNodeTypes);
|
111
packages/editor-ui/src/__tests__/mocks.ts
Normal file
111
packages/editor-ui/src/__tests__/mocks.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import type {
|
||||||
|
INodeType,
|
||||||
|
INodeTypeData,
|
||||||
|
INodeTypes,
|
||||||
|
IVersionedNodeType,
|
||||||
|
IConnections,
|
||||||
|
IDataObject,
|
||||||
|
INode,
|
||||||
|
IPinData,
|
||||||
|
IWorkflowSettings,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { NodeHelpers, Workflow } from 'n8n-workflow';
|
||||||
|
import { uuid } from '@jsplumb/util';
|
||||||
|
import { defaultMockNodeTypes } from '@/__tests__/defaults';
|
||||||
|
import type {
|
||||||
|
INodeUi,
|
||||||
|
ITag,
|
||||||
|
IUsedCredential,
|
||||||
|
IUser,
|
||||||
|
IWorkflowDb,
|
||||||
|
WorkflowMetadata,
|
||||||
|
} from '@/Interface';
|
||||||
|
|
||||||
|
export function createTestNodeTypes(data: INodeTypeData = {}): INodeTypes {
|
||||||
|
const getResolvedKey = (key: string) => {
|
||||||
|
const resolvedKeyParts = key.split(/[\/.]/);
|
||||||
|
return resolvedKeyParts[resolvedKeyParts.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
...defaultMockNodeTypes,
|
||||||
|
...Object.keys(data).reduce<INodeTypeData>((acc, key) => {
|
||||||
|
acc[getResolvedKey(key)] = data[key];
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
function getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||||
|
return nodeTypes[getResolvedKey(nodeType)].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||||
|
return NodeHelpers.getVersionedNodeType(getByName(nodeType), version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getByName,
|
||||||
|
getByNameAndVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTestWorkflowObject(options: {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
nodes: INode[];
|
||||||
|
connections: IConnections;
|
||||||
|
active?: boolean;
|
||||||
|
nodeTypes?: INodeTypeData;
|
||||||
|
staticData?: IDataObject;
|
||||||
|
settings?: IWorkflowSettings;
|
||||||
|
pinData?: IPinData;
|
||||||
|
}) {
|
||||||
|
return new Workflow({
|
||||||
|
...options,
|
||||||
|
id: options.id ?? uuid(),
|
||||||
|
active: options.active ?? false,
|
||||||
|
nodeTypes: createTestNodeTypes(options.nodeTypes),
|
||||||
|
connections: options.connections ?? {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTestWorkflow(options: {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
active?: boolean;
|
||||||
|
createdAt?: number | string;
|
||||||
|
updatedAt?: number | string;
|
||||||
|
nodes?: INodeUi[];
|
||||||
|
connections?: IConnections;
|
||||||
|
settings?: IWorkflowSettings;
|
||||||
|
tags?: ITag[] | string[];
|
||||||
|
pinData?: IPinData;
|
||||||
|
sharedWith?: Array<Partial<IUser>>;
|
||||||
|
ownedBy?: Partial<IUser>;
|
||||||
|
versionId?: string;
|
||||||
|
usedCredentials?: IUsedCredential[];
|
||||||
|
meta?: WorkflowMetadata;
|
||||||
|
}): IWorkflowDb {
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
createdAt: options.createdAt ?? '',
|
||||||
|
updatedAt: options.updatedAt ?? '',
|
||||||
|
versionId: options.versionId ?? '',
|
||||||
|
id: options.id ?? uuid(),
|
||||||
|
active: options.active ?? false,
|
||||||
|
connections: options.connections ?? {},
|
||||||
|
} as IWorkflowDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTestNode(
|
||||||
|
node: Partial<INode> & { name: INode['name']; type: INode['type'] },
|
||||||
|
): INode {
|
||||||
|
return {
|
||||||
|
id: uuid(),
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 0] as [number, number],
|
||||||
|
parameters: {},
|
||||||
|
...node,
|
||||||
|
};
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ const defaultSettings: IN8nUISettings = {
|
||||||
oauthCallbackUrls: { oauth1: '', oauth2: '' },
|
oauthCallbackUrls: { oauth1: '', oauth2: '' },
|
||||||
onboardingCallPromptEnabled: false,
|
onboardingCallPromptEnabled: false,
|
||||||
personalizationSurveyEnabled: false,
|
personalizationSurveyEnabled: false,
|
||||||
|
releaseChannel: 'stable',
|
||||||
posthog: {
|
posthog: {
|
||||||
apiHost: '',
|
apiHost: '',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
|
|
|
@ -5,7 +5,6 @@ import type { EventBus } from 'n8n-design-system/utils';
|
||||||
import { createEventBus } from 'n8n-design-system/utils';
|
import { createEventBus } from 'n8n-design-system/utils';
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
import { CHAT_EMBED_MODAL_KEY, WEBHOOK_NODE_TYPE } from '../constants';
|
import { CHAT_EMBED_MODAL_KEY, WEBHOOK_NODE_TYPE } from '../constants';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
||||||
|
@ -22,7 +21,6 @@ const props = defineProps({
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
|
|
||||||
const tabs = ref([
|
const tabs = ref([
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import Vue from 'vue';
|
|
||||||
import { addVarType } from '../utils';
|
import { addVarType } from '../utils';
|
||||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||||
import type { CodeNodeEditorMixin } from '../types';
|
|
||||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
const escape = (str: string) => str.replace('$', '\\$');
|
const escape = (str: string) => str.replace('$', '\\$');
|
||||||
|
|
||||||
export const secretsCompletions = (Vue as CodeNodeEditorMixin).extend({
|
export const secretsCompletions = defineComponent({
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
* Complete `$secrets.` to `$secrets.providerName` and `$secrets.providerName.secretName`.
|
* Complete `$secrets.` to `$secrets.providerName` and `$secrets.providerName.secretName`.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="error-header">
|
<div class="error-header">
|
||||||
<div class="error-message">{{ getErrorMessage() }}</div>
|
<div class="error-message" v-text="getErrorMessage()" />
|
||||||
<div class="error-description" v-if="error.description" v-html="getErrorDescription()"></div>
|
<div class="error-description" v-if="error.description" v-html="getErrorDescription()"></div>
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
|
@ -125,7 +125,12 @@ import { copyPaste } from '@/mixins/copyPaste';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { MAX_DISPLAY_DATA_SIZE } from '@/constants';
|
import { MAX_DISPLAY_DATA_SIZE } from '@/constants';
|
||||||
|
|
||||||
import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow';
|
import type {
|
||||||
|
INodeProperties,
|
||||||
|
INodePropertyCollection,
|
||||||
|
INodePropertyOptions,
|
||||||
|
NodeOperationError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
@ -170,6 +175,18 @@ export default defineComponent({
|
||||||
.replace(/%%PARAMETER_FULL%%/g, parameterFullName);
|
.replace(/%%PARAMETER_FULL%%/g, parameterFullName);
|
||||||
},
|
},
|
||||||
getErrorDescription(): string {
|
getErrorDescription(): string {
|
||||||
|
const isSubNodeError =
|
||||||
|
this.error.name === 'NodeOperationError' &&
|
||||||
|
(this.error as NodeOperationError).functionality === 'configuration-node';
|
||||||
|
|
||||||
|
if (isSubNodeError) {
|
||||||
|
return sanitizeHtml(
|
||||||
|
this.error.description +
|
||||||
|
this.$locale.baseText('pushConnection.executionError.openNode', {
|
||||||
|
interpolate: { node: this.error.node.name },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!this.error.context?.descriptionTemplate) {
|
if (!this.error.context?.descriptionTemplate) {
|
||||||
return sanitizeHtml(this.error.description);
|
return sanitizeHtml(this.error.description);
|
||||||
}
|
}
|
||||||
|
@ -182,6 +199,20 @@ export default defineComponent({
|
||||||
getErrorMessage(): string {
|
getErrorMessage(): string {
|
||||||
const baseErrorMessage = this.$locale.baseText('nodeErrorView.error') + ': ';
|
const baseErrorMessage = this.$locale.baseText('nodeErrorView.error') + ': ';
|
||||||
|
|
||||||
|
const isSubNodeError =
|
||||||
|
this.error.name === 'NodeOperationError' &&
|
||||||
|
(this.error as NodeOperationError).functionality === 'configuration-node';
|
||||||
|
|
||||||
|
if (isSubNodeError) {
|
||||||
|
const baseErrorMessageSubNode = this.$locale.baseText('nodeErrorView.errorSubNode', {
|
||||||
|
interpolate: { node: this.error.node.name },
|
||||||
|
});
|
||||||
|
return baseErrorMessageSubNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.error.message === this.error.description) {
|
||||||
|
return baseErrorMessage;
|
||||||
|
}
|
||||||
if (!this.error.context?.messageTemplate) {
|
if (!this.error.context?.messageTemplate) {
|
||||||
return baseErrorMessage + this.error.message;
|
return baseErrorMessage + this.error.message;
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,7 +166,13 @@ import { defineComponent } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
|
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
|
||||||
import type { ConnectionTypes, IConnectedNode, INodeTypeDescription, Workflow } from 'n8n-workflow';
|
import type {
|
||||||
|
ConnectionTypes,
|
||||||
|
IConnectedNode,
|
||||||
|
INodeOutputConfiguration,
|
||||||
|
INodeTypeDescription,
|
||||||
|
Workflow,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import RunData from './RunData.vue';
|
import RunData from './RunData.vue';
|
||||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||||
|
@ -271,9 +277,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(inputs.length === 0 ||
|
inputs.length === 0 ||
|
||||||
inputs.find((inputName) => inputName !== NodeConnectionType.Main)) &&
|
(inputs.every((input) => this.filterOutConnectionType(input, NodeConnectionType.Main)) &&
|
||||||
outputs.find((outputName) => outputName !== NodeConnectionType.Main)
|
outputs.find((output) => this.filterOutConnectionType(output, NodeConnectionType.Main)))
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -384,6 +390,14 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
filterOutConnectionType(
|
||||||
|
item: ConnectionTypes | INodeOutputConfiguration,
|
||||||
|
type: ConnectionTypes,
|
||||||
|
) {
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
|
return typeof item === 'string' ? item !== type : item.type !== type;
|
||||||
|
},
|
||||||
onInputModeChange(val: MappingMode) {
|
onInputModeChange(val: MappingMode) {
|
||||||
this.inputMode = val;
|
this.inputMode = val;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<NDVFloatingNodes
|
||||||
|
v-if="activeNode"
|
||||||
|
@switchSelectedNode="onSwitchSelectedNode"
|
||||||
|
:root-node="activeNode"
|
||||||
|
type="input"
|
||||||
|
/>
|
||||||
<div :class="$style.inputPanel" v-if="!hideInputAndOutput" :style="inputPanelStyles">
|
<div :class="$style.inputPanel" v-if="!hideInputAndOutput" :style="inputPanelStyles">
|
||||||
<slot name="input"></slot>
|
<slot name="input"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,6 +56,7 @@ import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '
|
||||||
import { debounceHelper } from '@/mixins/debounce';
|
import { debounceHelper } from '@/mixins/debounce';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { ndvEventBus } from '@/event-bus';
|
import { ndvEventBus } from '@/event-bus';
|
||||||
|
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||||
|
|
||||||
const SIDE_MARGIN = 24;
|
const SIDE_MARGIN = 24;
|
||||||
const SIDE_PANELS_MARGIN = 80;
|
const SIDE_PANELS_MARGIN = 80;
|
||||||
|
@ -70,6 +77,7 @@ export default defineComponent({
|
||||||
mixins: [debounceHelper],
|
mixins: [debounceHelper],
|
||||||
components: {
|
components: {
|
||||||
PanelDragButton,
|
PanelDragButton,
|
||||||
|
NDVFloatingNodes,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
isDraggable: {
|
isDraggable: {
|
||||||
|
@ -136,6 +144,9 @@ export default defineComponent({
|
||||||
} {
|
} {
|
||||||
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType);
|
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType);
|
||||||
},
|
},
|
||||||
|
activeNode() {
|
||||||
|
return this.ndvStore.activeNode;
|
||||||
|
},
|
||||||
supportedResizeDirections(): string[] {
|
supportedResizeDirections(): string[] {
|
||||||
const supportedDirections = ['right'];
|
const supportedDirections = ['right'];
|
||||||
|
|
||||||
|
@ -249,6 +260,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onSwitchSelectedNode(node: string) {
|
||||||
|
this.$emit('switchSelectedNode', node);
|
||||||
|
},
|
||||||
getInitialLeftPosition(width: number) {
|
getInitialLeftPosition(width: number) {
|
||||||
if (this.currentNodePaneType === 'dragless')
|
if (this.currentNodePaneType === 'dragless')
|
||||||
return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
|
return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
|
||||||
|
|
262
packages/editor-ui/src/components/NDVFloatingNodes.vue
Normal file
262
packages/editor-ui/src/components/NDVFloatingNodes.vue
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
<template>
|
||||||
|
<aside :class="$style.floatingNodes">
|
||||||
|
<ul
|
||||||
|
v-for="connectionGroup in connectionGroups"
|
||||||
|
:class="[$style.nodesList, $style[connectionGroup]]"
|
||||||
|
:key="connectionGroup"
|
||||||
|
>
|
||||||
|
<template v-for="{ node, nodeType } in connectedNodes[connectionGroup]">
|
||||||
|
<n8n-tooltip
|
||||||
|
:placement="tooltipPositionMapper[connectionGroup]"
|
||||||
|
v-if="node && nodeType"
|
||||||
|
:teleported="false"
|
||||||
|
:key="node.name"
|
||||||
|
:offset="60"
|
||||||
|
>
|
||||||
|
<template #content>{{ node.name }}</template>
|
||||||
|
|
||||||
|
<li
|
||||||
|
:class="$style.connectedNode"
|
||||||
|
@click="$emit('switchSelectedNode', node.name)"
|
||||||
|
data-test-id="floating-node"
|
||||||
|
:data-node-name="node.name"
|
||||||
|
:data-node-placement="connectionGroup"
|
||||||
|
>
|
||||||
|
<node-icon
|
||||||
|
:nodeType="nodeType"
|
||||||
|
:nodeName="node.name"
|
||||||
|
:tooltip-position="tooltipPositionMapper[connectionGroup]"
|
||||||
|
:size="35"
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</n8n-tooltip>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { computed, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
rootNode: INodeUi;
|
||||||
|
type: 'input' | 'sub-input' | 'sub-output' | 'output';
|
||||||
|
}
|
||||||
|
const enum FloatingNodePosition {
|
||||||
|
top = 'outputSub',
|
||||||
|
right = 'outputMain',
|
||||||
|
bottom = 'inputSub',
|
||||||
|
left = 'inputMain',
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const workflow = workflowsStore.getCurrentWorkflow();
|
||||||
|
const emit = defineEmits(['switchSelectedNode']);
|
||||||
|
|
||||||
|
interface NodeConfig {
|
||||||
|
node: INodeUi;
|
||||||
|
nodeType: INodeTypeDescription;
|
||||||
|
}
|
||||||
|
function moveNodeDirection(direction: FloatingNodePosition) {
|
||||||
|
const matchedDirectionNode = connectedNodes.value[direction][0];
|
||||||
|
if (matchedDirectionNode) {
|
||||||
|
emit('switchSelectedNode', matchedDirectionNode.node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.shiftKey && e.altKey && (e.ctrlKey || e.metaKey)) {
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
const mapper = {
|
||||||
|
ArrowUp: FloatingNodePosition.top,
|
||||||
|
ArrowRight: FloatingNodePosition.right,
|
||||||
|
ArrowDown: FloatingNodePosition.bottom,
|
||||||
|
ArrowLeft: FloatingNodePosition.left,
|
||||||
|
};
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
const matchingDirection = mapper[e.key as keyof typeof mapper] || null;
|
||||||
|
if (matchingDirection) {
|
||||||
|
moveNodeDirection(matchingDirection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getINodesFromNames(names: string[]): NodeConfig[] {
|
||||||
|
return names
|
||||||
|
.map((name) => {
|
||||||
|
const node = workflowsStore.getNodeByName(name);
|
||||||
|
if (node) {
|
||||||
|
const nodeType = nodeTypesStore.getNodeType(node.type);
|
||||||
|
if (nodeType) {
|
||||||
|
return { node, nodeType };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((n): n is NodeConfig => n !== null);
|
||||||
|
}
|
||||||
|
const connectedNodes = computed<
|
||||||
|
Record<FloatingNodePosition, Array<{ node: INodeUi; nodeType: INodeTypeDescription }>>
|
||||||
|
>(() => {
|
||||||
|
const rootName = props.rootNode.name;
|
||||||
|
return {
|
||||||
|
[FloatingNodePosition.top]: getINodesFromNames(
|
||||||
|
workflow.getChildNodes(rootName, 'ALL_NON_MAIN'),
|
||||||
|
),
|
||||||
|
[FloatingNodePosition.right]: getINodesFromNames(workflow.getChildNodes(rootName, 'main', 1)),
|
||||||
|
[FloatingNodePosition.bottom]: getINodesFromNames(
|
||||||
|
workflow.getParentNodes(rootName, 'ALL_NON_MAIN'),
|
||||||
|
),
|
||||||
|
[FloatingNodePosition.left]: getINodesFromNames(workflow.getParentNodes(rootName, 'main', 1)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionGroups = [
|
||||||
|
FloatingNodePosition.top,
|
||||||
|
FloatingNodePosition.right,
|
||||||
|
FloatingNodePosition.bottom,
|
||||||
|
FloatingNodePosition.left,
|
||||||
|
];
|
||||||
|
const tooltipPositionMapper = {
|
||||||
|
[FloatingNodePosition.top]: 'bottom',
|
||||||
|
[FloatingNodePosition.right]: 'left',
|
||||||
|
[FloatingNodePosition.bottom]: 'top',
|
||||||
|
[FloatingNodePosition.left]: 'right',
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', onKeyDown, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('keydown', onKeyDown, true);
|
||||||
|
});
|
||||||
|
defineExpose({
|
||||||
|
moveNodeDirection,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.floatingNodes {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.floatingNodes {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodesList {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: min-content;
|
||||||
|
margin: auto;
|
||||||
|
transform-origin: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&.inputSub,
|
||||||
|
&.outputSub {
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
&.outputSub {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
&.inputSub {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
&.outputMain,
|
||||||
|
&.inputMain {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
&.outputMain {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
&.inputMain {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.outputMain {
|
||||||
|
transform: translateX(50%);
|
||||||
|
}
|
||||||
|
&.outputSub {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
&.inputMain {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
&.inputSub {
|
||||||
|
transform: translateY(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.connectedNode {
|
||||||
|
border: var(--border-base);
|
||||||
|
background-color: var(--color-canvas-node-background);
|
||||||
|
border-radius: 100%;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
position: relative;
|
||||||
|
transform: scale(0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-self: center;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -35%;
|
||||||
|
right: -30%;
|
||||||
|
bottom: -35%;
|
||||||
|
left: -30%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.outputMain &,
|
||||||
|
.inputMain & {
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.outputMain & {
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2) translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.outputSub & {
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2) translateY(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.inputMain & {
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2) translateX(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.inputSub & {
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2) translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -169,6 +169,7 @@ import type {
|
||||||
INodeOutputConfiguration,
|
INodeOutputConfiguration,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -392,7 +393,7 @@ export default defineComponent({
|
||||||
nodeExecutionStatus(): string {
|
nodeExecutionStatus(): string {
|
||||||
const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name];
|
const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name];
|
||||||
if (nodeExecutionRunData) {
|
if (nodeExecutionRunData) {
|
||||||
return nodeExecutionRunData[0].executionStatus ?? '';
|
return nodeExecutionRunData.filter(Boolean)[0].executionStatus ?? '';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
@ -401,7 +402,7 @@ export default defineComponent({
|
||||||
const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name];
|
const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name];
|
||||||
if (nodeExecutionRunData) {
|
if (nodeExecutionRunData) {
|
||||||
nodeExecutionRunData.forEach((executionRunData) => {
|
nodeExecutionRunData.forEach((executionRunData) => {
|
||||||
if (executionRunData.error) {
|
if (executionRunData?.error) {
|
||||||
issues.push(
|
issues.push(
|
||||||
`${executionRunData.error.message}${
|
`${executionRunData.error.message}${
|
||||||
executionRunData.error.description ? ` (${executionRunData.error.description})` : ''
|
executionRunData.error.description ? ` (${executionRunData.error.description})` : ''
|
||||||
|
@ -426,7 +427,10 @@ export default defineComponent({
|
||||||
return this.node ? this.node.position : [0, 0];
|
return this.node ? this.node.position : [0, 0];
|
||||||
},
|
},
|
||||||
showDisabledLinethrough(): boolean {
|
showDisabledLinethrough(): boolean {
|
||||||
return !!(this.data.disabled && this.inputs.length === 1 && this.outputs.length === 1);
|
return (
|
||||||
|
!this.isConfigurableNode &&
|
||||||
|
!!(this.data.disabled && this.inputs.length === 1 && this.outputs.length === 1)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
shortNodeType(): string {
|
shortNodeType(): string {
|
||||||
return this.$locale.shortNodeType(this.data.type);
|
return this.$locale.shortNodeType(this.data.type);
|
||||||
|
@ -482,9 +486,15 @@ export default defineComponent({
|
||||||
borderColor = '--color-foreground-base';
|
borderColor = '--color-foreground-base';
|
||||||
} else if (!this.isExecuting) {
|
} else if (!this.isExecuting) {
|
||||||
if (this.hasIssues) {
|
if (this.hasIssues) {
|
||||||
borderColor = '--color-danger';
|
// Do not set red border if there is an issue with the configuration node
|
||||||
returnStyles['border-width'] = '2px';
|
if (
|
||||||
returnStyles['border-style'] = 'solid';
|
(this.nodeRunData?.[0]?.error as NodeOperationError)?.functionality !==
|
||||||
|
'configuration-node'
|
||||||
|
) {
|
||||||
|
borderColor = '--color-danger';
|
||||||
|
returnStyles['border-width'] = '2px';
|
||||||
|
returnStyles['border-style'] = 'solid';
|
||||||
|
}
|
||||||
} else if (this.waiting || this.showPinnedDataInfo) {
|
} else if (this.waiting || this.showPinnedDataInfo) {
|
||||||
borderColor = '--color-canvas-node-pinned-border';
|
borderColor = '--color-canvas-node-pinned-border';
|
||||||
} else if (this.nodeExecutionStatus === 'unknown') {
|
} else if (this.nodeExecutionStatus === 'unknown') {
|
||||||
|
@ -608,6 +618,7 @@ export default defineComponent({
|
||||||
!this.isTriggerNode ||
|
!this.isTriggerNode ||
|
||||||
this.isManualTypeNode ||
|
this.isManualTypeNode ||
|
||||||
this.isScheduledGroup ||
|
this.isScheduledGroup ||
|
||||||
|
this.uiStore.isModalActive ||
|
||||||
dataItemsCount === 0
|
dataItemsCount === 0
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
@ -1333,6 +1344,10 @@ export default defineComponent({
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.add-input-endpoint-error {
|
||||||
|
--endpoint-svg-color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
.add-input-endpoint-default {
|
.add-input-endpoint-default {
|
||||||
transition: transform var(--add-input-endpoint--transition-duration) ease;
|
transition: transform var(--add-input-endpoint--transition-duration) ease;
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,7 +162,7 @@ function onBackButton() {
|
||||||
v-if="activeViewStack.info && !activeViewStack.search"
|
v-if="activeViewStack.info && !activeViewStack.search"
|
||||||
:class="$style.info"
|
:class="$style.info"
|
||||||
:content="activeViewStack.info"
|
:content="activeViewStack.info"
|
||||||
theme="info"
|
theme="warning"
|
||||||
/>
|
/>
|
||||||
<!-- Actions mode -->
|
<!-- Actions mode -->
|
||||||
<ActionsRenderer v-if="isActionsMode && activeViewStack.subcategory" v-bind="$attrs" />
|
<ActionsRenderer v-if="isActionsMode && activeViewStack.subcategory" v-bind="$attrs" />
|
||||||
|
|
|
@ -8,6 +8,7 @@ import type {
|
||||||
SimplifiedNodeType,
|
SimplifiedNodeType,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import {
|
import {
|
||||||
|
AI_CODE_NODE_TYPE,
|
||||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||||
DEFAULT_SUBCATEGORY,
|
DEFAULT_SUBCATEGORY,
|
||||||
TRIGGER_NODE_CREATOR_VIEW,
|
TRIGGER_NODE_CREATOR_VIEW,
|
||||||
|
@ -152,6 +153,9 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
||||||
},
|
},
|
||||||
panelClass: relatedAIView?.properties.panelClass,
|
panelClass: relatedAIView?.properties.panelClass,
|
||||||
baseFilter: (i: INodeCreateElement) => {
|
baseFilter: (i: INodeCreateElement) => {
|
||||||
|
// AI Code node could have any connection type so we don't want to display it
|
||||||
|
// in the compatible connection view as it would be displayed in all of them
|
||||||
|
if (i.key === AI_CODE_NODE_TYPE) return false;
|
||||||
const displayNode = nodesByConnectionType[connectionType].includes(i.key);
|
const displayNode = nodesByConnectionType[connectionType].includes(i.key);
|
||||||
|
|
||||||
// TODO: Filtering works currently fine for displaying compatible node when dropping
|
// TODO: Filtering works currently fine for displaying compatible node when dropping
|
||||||
|
|
|
@ -317,18 +317,6 @@ export function TriggerView(nodes: SimplifiedNodeType[]) {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAINodes = (nodes ?? []).some((node) => node.codex?.categories?.includes(AI_SUBCATEGORY));
|
|
||||||
if (hasAINodes)
|
|
||||||
view.items.push({
|
|
||||||
key: AI_NODE_CREATOR_VIEW,
|
|
||||||
type: 'view',
|
|
||||||
properties: {
|
|
||||||
title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'),
|
|
||||||
icon: 'robot',
|
|
||||||
description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
width="auto"
|
width="auto"
|
||||||
append-to-body
|
append-to-body
|
||||||
data-test-id="ndv"
|
data-test-id="ndv"
|
||||||
|
:data-has-output-connection="hasOutputConnection"
|
||||||
>
|
>
|
||||||
<n8n-tooltip
|
<n8n-tooltip
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
|
@ -42,6 +43,8 @@
|
||||||
:isDraggable="!isTriggerNode"
|
:isDraggable="!isTriggerNode"
|
||||||
:hasDoubleWidth="activeNodeType?.parameterPane === 'wide'"
|
:hasDoubleWidth="activeNodeType?.parameterPane === 'wide'"
|
||||||
:nodeType="activeNodeType"
|
:nodeType="activeNodeType"
|
||||||
|
:key="activeNode.name"
|
||||||
|
@switchSelectedNode="onSwitchSelectedNode"
|
||||||
@close="close"
|
@close="close"
|
||||||
@init="onPanelsInit"
|
@init="onPanelsInit"
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
|
@ -275,6 +278,15 @@ export default defineComponent({
|
||||||
workflow(): Workflow {
|
workflow(): Workflow {
|
||||||
return this.getCurrentWorkflow();
|
return this.getCurrentWorkflow();
|
||||||
},
|
},
|
||||||
|
hasOutputConnection() {
|
||||||
|
if (!this.activeNode) return false;
|
||||||
|
const outgoingConnections = this.workflowsStore.outgoingConnectionsByNodeName(
|
||||||
|
this.activeNode.name,
|
||||||
|
) as INodeConnections;
|
||||||
|
|
||||||
|
// Check if there's at-least one output connection
|
||||||
|
return (Object.values(outgoingConnections)?.[0]?.[0] ?? []).length > 0;
|
||||||
|
},
|
||||||
parentNodes(): string[] {
|
parentNodes(): string[] {
|
||||||
if (this.activeNode) {
|
if (this.activeNode) {
|
||||||
return (
|
return (
|
||||||
|
@ -634,6 +646,9 @@ export default defineComponent({
|
||||||
nodeTypeSelected(nodeTypeName: string) {
|
nodeTypeSelected(nodeTypeName: string) {
|
||||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||||
},
|
},
|
||||||
|
async onSwitchSelectedNode(nodeTypeName: string) {
|
||||||
|
this.$emit('switchSelectedNode', nodeTypeName);
|
||||||
|
},
|
||||||
async close() {
|
async close() {
|
||||||
if (this.isDragging) {
|
if (this.isDragging) {
|
||||||
return;
|
return;
|
||||||
|
@ -739,6 +754,10 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
// Hide notice(.ndv-connection-hint-notice) warning when node has output connection
|
||||||
|
[data-has-output-connection='true'] .ndv-connection-hint-notice {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.ndv-wrapper {
|
.ndv-wrapper {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:size="size"
|
:size="size"
|
||||||
:circle="circle"
|
:circle="circle"
|
||||||
:nodeTypeName="nodeType ? nodeType.displayName : ''"
|
:nodeTypeName="nodeName ?? nodeType?.displayName ?? ''"
|
||||||
:showTooltip="showTooltip"
|
:showTooltip="showTooltip"
|
||||||
|
:tooltipPosition="tooltipPosition"
|
||||||
:badge="badge"
|
:badge="badge"
|
||||||
@click="(e) => $emit('click')"
|
@click="(e) => $emit('click')"
|
||||||
></n8n-node-icon>
|
></n8n-node-icon>
|
||||||
|
@ -53,6 +54,14 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
tooltipPosition: {
|
||||||
|
type: String,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
|
nodeName: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useRootStore),
|
...mapStores(useRootStore),
|
||||||
|
|
|
@ -65,14 +65,19 @@
|
||||||
$locale.baseText('ndv.output.waitingToRun')
|
$locale.baseText('ndv.output.waitingToRun')
|
||||||
}}</n8n-text>
|
}}</n8n-text>
|
||||||
<n8n-text v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
|
<n8n-text v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
|
||||||
{{ $locale.baseText('ndv.output.runNodeHint') }}
|
<template v-if="isSubNode">
|
||||||
<span @click="insertTestData" v-if="canPinData">
|
{{ $locale.baseText('ndv.output.runNodeHintSubNode') }}
|
||||||
<br />
|
</template>
|
||||||
{{ $locale.baseText('generic.or') }}
|
<template v-else>
|
||||||
<n8n-text tag="a" size="medium" color="primary">
|
{{ $locale.baseText('ndv.output.runNodeHint') }}
|
||||||
{{ $locale.baseText('ndv.output.insertTestData') }}
|
<span @click="insertTestData" v-if="canPinData">
|
||||||
</n8n-text>
|
<br />
|
||||||
</span>
|
{{ $locale.baseText('generic.or') }}
|
||||||
|
<n8n-text tag="a" size="medium" color="primary">
|
||||||
|
{{ $locale.baseText('ndv.output.insertTestData') }}
|
||||||
|
</n8n-text>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
<n8n-notice
|
<n8n-notice
|
||||||
v-else-if="parameter.type === 'notice'"
|
v-else-if="parameter.type === 'notice'"
|
||||||
class="parameter-item"
|
:class="['parameter-item', parameter.typeOptions?.containerClass ?? '']"
|
||||||
:content="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
:content="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||||
@action="onNoticeAction"
|
@action="onNoticeAction"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1325,7 +1325,7 @@ export default defineComponent({
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.workflowRunData?.[this.node.name][runIndex].hasOwnProperty('error')) {
|
if (this.workflowRunData?.[this.node.name]?.[runIndex]?.hasOwnProperty('error')) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -265,5 +265,6 @@ onMounted(() => {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
:indent="12"
|
:indent="12"
|
||||||
@node-click="onItemClick"
|
@node-click="onItemClick"
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
|
data-test-id="lm-chat-logs-tree"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<div
|
<div
|
||||||
|
@ -50,7 +51,11 @@
|
||||||
}}
|
}}
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(data, index) in selectedRun" :key="`${data.node}__${data.runIndex}__index`">
|
<div
|
||||||
|
v-for="(data, index) in selectedRun"
|
||||||
|
:key="`${data.node}__${data.runIndex}__index`"
|
||||||
|
data-test-id="lm-chat-logs-entry"
|
||||||
|
>
|
||||||
<RunDataAiContent :inputData="data" :contentIndex="index" />
|
<RunDataAiContent :inputData="data" :contentIndex="index" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ interface MemoryMessage {
|
||||||
type: string;
|
type: string;
|
||||||
id: string[];
|
id: string[];
|
||||||
kwargs: {
|
kwargs: {
|
||||||
content: string;
|
content: unknown;
|
||||||
additional_kwargs: Record<string, unknown>;
|
additional_kwargs: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,7 @@ const outputTypeParsers: {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[NodeConnectionType.AiTool]: fallbackParser,
|
[NodeConnectionType.AiTool]: fallbackParser,
|
||||||
|
[NodeConnectionType.AiAgent]: fallbackParser,
|
||||||
[NodeConnectionType.AiMemory](execData: IDataObject) {
|
[NodeConnectionType.AiMemory](execData: IDataObject) {
|
||||||
const chatHistory =
|
const chatHistory =
|
||||||
execData.chatHistory ?? execData.messages ?? execData?.response?.chat_history;
|
execData.chatHistory ?? execData.messages ?? execData?.response?.chat_history;
|
||||||
|
@ -88,7 +89,23 @@ const outputTypeParsers: {
|
||||||
const responseText = chatHistory
|
const responseText = chatHistory
|
||||||
.map((content: MemoryMessage) => {
|
.map((content: MemoryMessage) => {
|
||||||
if (content.type === 'constructor' && content.id?.includes('schema') && content.kwargs) {
|
if (content.type === 'constructor' && content.id?.includes('schema') && content.kwargs) {
|
||||||
|
interface MessageContent {
|
||||||
|
type: string;
|
||||||
|
image_url?: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
let message = content.kwargs.content;
|
let message = content.kwargs.content;
|
||||||
|
if (Array.isArray(message)) {
|
||||||
|
const messageContent = message[0] as {
|
||||||
|
type?: string;
|
||||||
|
image_url?: { url: string };
|
||||||
|
};
|
||||||
|
if (messageContent?.type === 'image_url') {
|
||||||
|
message = `![Input image](${messageContent.image_url?.url})`;
|
||||||
|
}
|
||||||
|
message = message as MessageContent[];
|
||||||
|
}
|
||||||
if (Object.keys(content.kwargs.additional_kwargs).length) {
|
if (Object.keys(content.kwargs.additional_kwargs).length) {
|
||||||
message += ` (${JSON.stringify(content.kwargs.additional_kwargs)})`;
|
message += ` (${JSON.stringify(content.kwargs.additional_kwargs)})`;
|
||||||
}
|
}
|
||||||
|
@ -120,7 +137,6 @@ const outputTypeParsers: {
|
||||||
},
|
},
|
||||||
[NodeConnectionType.AiOutputParser]: fallbackParser,
|
[NodeConnectionType.AiOutputParser]: fallbackParser,
|
||||||
[NodeConnectionType.AiRetriever]: fallbackParser,
|
[NodeConnectionType.AiRetriever]: fallbackParser,
|
||||||
[NodeConnectionType.AiVectorRetriever]: fallbackParser,
|
|
||||||
[NodeConnectionType.AiVectorStore](execData: IDataObject) {
|
[NodeConnectionType.AiVectorStore](execData: IDataObject) {
|
||||||
if (execData.documents) {
|
if (execData.documents) {
|
||||||
return {
|
return {
|
||||||
|
@ -189,9 +205,17 @@ export const useAiContentParsers = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const parser = outputTypeParsers[endpointType as AllowedEndpointType];
|
const parser = outputTypeParsers[endpointType as AllowedEndpointType];
|
||||||
if (!parser) return [{ raw: contentJson, parsedContent: null }];
|
if (!parser)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
raw: contentJson.filter((item): item is IDataObject => item !== undefined),
|
||||||
|
parsedContent: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const parsedOutput = contentJson.map((c) => ({ raw: c, parsedContent: parser(c) }));
|
const parsedOutput = contentJson
|
||||||
|
.filter((c): c is IDataObject => c !== undefined)
|
||||||
|
.map((c) => ({ raw: c, parsedContent: parser(c) }));
|
||||||
return parsedOutput;
|
return parsedOutput;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { within } from '@testing-library/dom';
|
import { within } from '@testing-library/vue';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
|
|
@ -64,17 +64,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="logs-wrapper">
|
<div v-if="node" class="logs-wrapper" data-test-id="lm-chat-logs">
|
||||||
<n8n-text class="logs-title" tag="p" size="large">{{
|
<n8n-text class="logs-title" tag="p" size="large">{{
|
||||||
$locale.baseText('chat.window.logs')
|
$locale.baseText('chat.window.logs')
|
||||||
}}</n8n-text>
|
}}</n8n-text>
|
||||||
<div class="logs">
|
<div class="logs">
|
||||||
<run-data-ai v-if="node" :node="node" hide-title slim :key="messages.length" />
|
<run-data-ai :node="node" hide-title slim :key="messages.length" />
|
||||||
<div v-else class="no-node-connected">
|
|
||||||
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
|
|
||||||
$locale.baseText('chat.window.noExecution')
|
|
||||||
}}</n8n-text>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,6 +82,7 @@
|
||||||
type="textarea"
|
type="textarea"
|
||||||
ref="inputField"
|
ref="inputField"
|
||||||
:placeholder="$locale.baseText('chat.window.chat.placeholder')"
|
:placeholder="$locale.baseText('chat.window.chat.placeholder')"
|
||||||
|
data-test-id="workflow-chat-input"
|
||||||
@keydown.stop="updated"
|
@keydown.stop="updated"
|
||||||
/>
|
/>
|
||||||
<n8n-button
|
<n8n-button
|
||||||
|
@ -97,7 +93,7 @@
|
||||||
size="large"
|
size="large"
|
||||||
icon="comment"
|
icon="comment"
|
||||||
type="primary"
|
type="primary"
|
||||||
data-test-id="workflow-chat-button"
|
data-test-id="workflow-chat-send-button"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<n8n-info-tip class="mt-s">
|
<n8n-info-tip class="mt-s">
|
||||||
|
@ -290,8 +286,10 @@ export default defineComponent({
|
||||||
|
|
||||||
if (!chatNode) {
|
if (!chatNode) {
|
||||||
this.showError(
|
this.showError(
|
||||||
new Error('Chat viable node(Agent or Chain) could not be found!'),
|
new Error(
|
||||||
'Chat node not found',
|
'Chat only works when an AI agent or chain is connected to the chat trigger node',
|
||||||
|
),
|
||||||
|
'Missing AI node',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -395,6 +393,8 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputKey = triggerNode.typeVersion < 1.1 ? 'input' : 'chat_input';
|
||||||
|
|
||||||
const nodeData: ITaskData = {
|
const nodeData: ITaskData = {
|
||||||
startTime: new Date().getTime(),
|
startTime: new Date().getTime(),
|
||||||
executionTime: 0,
|
executionTime: 0,
|
||||||
|
@ -404,7 +404,7 @@ export default defineComponent({
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
json: {
|
json: {
|
||||||
input: message,
|
[inputKey]: message,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -504,9 +504,11 @@ export default defineComponent({
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
.logs-wrapper {
|
.logs-wrapper {
|
||||||
border: 1px solid #e0e0e0;
|
--node-icon-color: var(--color-text-base);
|
||||||
|
border: 1px solid var(--color-foreground-base);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
@ -518,8 +520,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.messages {
|
.messages {
|
||||||
background-color: var(--color-background-base);
|
background-color: var(--color-lm-chat-messages-background);
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--color-foreground-base);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -533,16 +535,17 @@ export default defineComponent({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
border-radius: 10px;
|
border-radius: var(--border-radius-large);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin: 0.5em 1em;
|
margin: var(--spacing-2xs) var(--spacing-s);
|
||||||
max-width: 75%;
|
max-width: 75%;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
&.bot {
|
&.bot {
|
||||||
background-color: #e0d0d0;
|
background-color: var(--color-lm-chat-bot-background);
|
||||||
|
border: 1px solid var(--color-lm-chat-bot-border);
|
||||||
float: left;
|
float: left;
|
||||||
|
|
||||||
.message-options {
|
.message-options {
|
||||||
|
@ -551,7 +554,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
&.user {
|
&.user {
|
||||||
background-color: #d0e0d0;
|
background-color: var(--color-lm-chat-user-background);
|
||||||
|
border: 1px solid var(--color-lm-chat-user-border);
|
||||||
float: right;
|
float: right;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import ChatEmbedModal from '@/components/ChatEmbedModal.vue';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { CHAT_EMBED_MODAL_KEY, STORES, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(ChatEmbedModal, {
|
||||||
|
props: {
|
||||||
|
teleported: false,
|
||||||
|
appendToBody: false,
|
||||||
|
},
|
||||||
|
pinia: createTestingPinia({
|
||||||
|
initialState: {
|
||||||
|
[STORES.UI]: {
|
||||||
|
modals: {
|
||||||
|
[CHAT_EMBED_MODAL_KEY]: { open: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[STORES.WORKFLOWS]: {
|
||||||
|
workflow: {
|
||||||
|
nodes: [{ type: WEBHOOK_NODE_TYPE }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ChatEmbedModal', () => {
|
||||||
|
it('should render correctly', async () => {
|
||||||
|
const wrapper = renderComponent();
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(wrapper.container.querySelector('.modal-content')).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabs = wrapper.container.querySelectorAll('.n8n-tabs .tab');
|
||||||
|
const activeTab = wrapper.container.querySelector('.n8n-tabs .tab.activeTab');
|
||||||
|
const editor = wrapper.container.querySelector('.cm-editor');
|
||||||
|
|
||||||
|
expect(tabs).toHaveLength(4);
|
||||||
|
expect(activeTab).toBeVisible();
|
||||||
|
expect(activeTab).toHaveTextContent('CDN Embed');
|
||||||
|
expect(editor).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,87 @@
|
||||||
|
import NodeDetailsView from '@/components/NodeDetailsView.vue';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import { uuid } from '@jsplumb/util';
|
||||||
|
import type { INode } from 'n8n-workflow';
|
||||||
|
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import { defaultMockNodeTypesArray } from '@/__tests__/defaults';
|
||||||
|
import { setupServer } from '@/__tests__/server';
|
||||||
|
|
||||||
|
async function createPiniaWithActiveNode(node: INode) {
|
||||||
|
const workflowId = uuid();
|
||||||
|
const workflow = createTestWorkflow({
|
||||||
|
id: workflowId,
|
||||||
|
name: 'Test Workflow',
|
||||||
|
connections: {},
|
||||||
|
active: true,
|
||||||
|
nodes: [node],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes(defaultMockNodeTypesArray);
|
||||||
|
workflowsStore.workflow = workflow;
|
||||||
|
ndvStore.activeNodeName = node.name;
|
||||||
|
|
||||||
|
await useSettingsStore().getSettings();
|
||||||
|
await useUsersStore().loginWithCookie();
|
||||||
|
|
||||||
|
return pinia;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(NodeDetailsView, {
|
||||||
|
props: {
|
||||||
|
teleported: false,
|
||||||
|
appendToBody: false,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: {
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('NodeDetailsView', () => {
|
||||||
|
let server: ReturnType<typeof setupServer>;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
server = setupServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly', async () => {
|
||||||
|
const wrapper = renderComponent({
|
||||||
|
pinia: await createPiniaWithActiveNode(
|
||||||
|
createTestNode({
|
||||||
|
name: 'Manual Trigger',
|
||||||
|
type: 'manualTrigger',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(wrapper.container.querySelector('.ndv-wrapper')).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -17,6 +17,18 @@ export default () => {
|
||||||
const { [key]: _, ...rest } = state.customActions;
|
const { [key]: _, ...rest } = state.customActions;
|
||||||
state.customActions = rest;
|
state.customActions = rest;
|
||||||
}
|
}
|
||||||
|
function getElementAttributes(element: Element) {
|
||||||
|
const attributesObject: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < element.attributes.length; i++) {
|
||||||
|
const attr = element.attributes[i];
|
||||||
|
if (attr.name.startsWith('data-action-parameter-')) {
|
||||||
|
attributesObject[attr.name.replace('data-action-parameter-', '')] = attr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attributesObject;
|
||||||
|
}
|
||||||
|
|
||||||
function delegateClick(e: MouseEvent) {
|
function delegateClick(e: MouseEvent) {
|
||||||
const clickedElement = e.target;
|
const clickedElement = e.target;
|
||||||
if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return;
|
if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return;
|
||||||
|
@ -24,7 +36,9 @@ export default () => {
|
||||||
const actionAttribute = clickedElement.getAttribute('data-action');
|
const actionAttribute = clickedElement.getAttribute('data-action');
|
||||||
if (actionAttribute && typeof availableActions.value[actionAttribute] === 'function') {
|
if (actionAttribute && typeof availableActions.value[actionAttribute] === 'function') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
availableActions.value[actionAttribute]();
|
// Extract and parse `data-action-parameter-` attributes and pass them to the action
|
||||||
|
const elementAttributes = getElementAttributes(clickedElement);
|
||||||
|
availableActions.value[actionAttribute](elementAttributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,7 @@ export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
||||||
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||||
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
||||||
export const MANUAL_CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualChatTrigger';
|
export const MANUAL_CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualChatTrigger';
|
||||||
|
export const AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
|
||||||
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
||||||
export const N8N_NODE_TYPE = 'n8n-nodes-base.n8n';
|
export const N8N_NODE_TYPE = 'n8n-nodes-base.n8n';
|
||||||
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
||||||
|
@ -194,6 +195,7 @@ export const NODE_CREATOR_OPEN_SOURCES: Record<
|
||||||
TAB: 'tab',
|
TAB: 'tab',
|
||||||
NODE_CONNECTION_ACTION: 'node_connection_action',
|
NODE_CONNECTION_ACTION: 'node_connection_action',
|
||||||
NODE_CONNECTION_DROP: 'node_connection_drop',
|
NODE_CONNECTION_DROP: 'node_connection_drop',
|
||||||
|
NOTICE_ERROR_MESSAGE: 'notice_error_message',
|
||||||
CONTEXT_MENU: 'context_menu',
|
CONTEXT_MENU: 'context_menu',
|
||||||
'': '',
|
'': '',
|
||||||
};
|
};
|
||||||
|
|
|
@ -176,6 +176,8 @@ export const nodeHelpers = defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const taskData of workflowResultData[node.name]) {
|
for (const taskData of workflowResultData[node.name]) {
|
||||||
|
if (!taskData) return false;
|
||||||
|
|
||||||
if (taskData.error !== undefined) {
|
if (taskData.error !== undefined) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -313,11 +315,24 @@ export const nodeHelpers = defineComponent({
|
||||||
const parentNodes = workflow.getParentNodes(node.name, input.type, 1);
|
const parentNodes = workflow.getParentNodes(node.name, input.type, 1);
|
||||||
|
|
||||||
if (parentNodes.length === 0) {
|
if (parentNodes.length === 0) {
|
||||||
foundIssues[input.type] = [
|
// We want to show different error for missing AI subnodes
|
||||||
this.$locale.baseText('nodeIssues.input.missing', {
|
if (input.type.startsWith('ai_')) {
|
||||||
interpolate: { inputName: input.displayName || input.type },
|
foundIssues[input.type] = [
|
||||||
}),
|
this.$locale.baseText('nodeIssues.input.missingSubNode', {
|
||||||
];
|
interpolate: {
|
||||||
|
inputName: input.displayName?.toLocaleLowerCase() ?? input.type,
|
||||||
|
inputType: input.type,
|
||||||
|
node: node.name,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
foundIssues[input.type] = [
|
||||||
|
this.$locale.baseText('nodeIssues.input.missing', {
|
||||||
|
interpolate: { inputName: input.displayName ?? input.type },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ import {
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { NodeConnectionType, NodeHelpers, jsonParse, jsonStringify } from 'n8n-workflow';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { jsonParse, jsonStringify } from 'n8n-workflow';
|
|
||||||
|
|
||||||
export type PinDataSource =
|
export type PinDataSource =
|
||||||
| 'pin-icon-click'
|
| 'pin-icon-click'
|
||||||
|
@ -39,9 +39,19 @@ export const pinData = defineComponent({
|
||||||
hasPinData(): boolean {
|
hasPinData(): boolean {
|
||||||
return !!this.node && typeof this.pinData !== 'undefined';
|
return !!this.node && typeof this.pinData !== 'undefined';
|
||||||
},
|
},
|
||||||
|
isSubNode() {
|
||||||
|
if (!this.nodeType.outputs || typeof this.nodeType.outputs === 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const outputTypes = NodeHelpers.getConnectionTypes(this.nodeType.outputs);
|
||||||
|
return outputTypes
|
||||||
|
? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0
|
||||||
|
: false;
|
||||||
|
},
|
||||||
isPinDataNodeType(): boolean {
|
isPinDataNodeType(): boolean {
|
||||||
return (
|
return (
|
||||||
!!this.node &&
|
!!this.node &&
|
||||||
|
!this.isSubNode &&
|
||||||
!this.isMultipleOutputsNodeType &&
|
!this.isMultipleOutputsNodeType &&
|
||||||
!PIN_DATA_NODE_TYPES_DENYLIST.includes(this.node.type)
|
!PIN_DATA_NODE_TYPES_DENYLIST.includes(this.node.type)
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import type {
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
SubworkflowOperationError,
|
SubworkflowOperationError,
|
||||||
IExecuteContextData,
|
IExecuteContextData,
|
||||||
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { TelemetryHelpers } from 'n8n-workflow';
|
import { TelemetryHelpers } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -396,25 +397,59 @@ export const pushConnection = defineComponent({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (
|
||||||
|
runDataExecuted.data.resultData.error?.name === 'NodeOperationError' &&
|
||||||
|
(runDataExecuted.data.resultData.error as NodeOperationError).functionality ===
|
||||||
|
'configuration-node'
|
||||||
|
) {
|
||||||
|
// If the error is a configuration error of the node itself doesn't get executed so we can't use lastNodeExecuted for the title
|
||||||
let title: string;
|
let title: string;
|
||||||
let type = 'error';
|
const nodeError = runDataExecuted.data.resultData.error as NodeOperationError;
|
||||||
if (runDataExecuted.status === 'canceled') {
|
if (nodeError.node.name) {
|
||||||
title = this.$locale.baseText('nodeView.showMessage.stopExecutionTry.title');
|
title = `Error in sub-node ‘${nodeError.node.name}‘`;
|
||||||
type = 'warning';
|
|
||||||
} else if (runDataExecuted.data.resultData.lastNodeExecuted) {
|
|
||||||
title = `Problem in node ‘${runDataExecuted.data.resultData.lastNodeExecuted}‘`;
|
|
||||||
} else {
|
} else {
|
||||||
title = 'Problem executing workflow';
|
title = 'Problem executing workflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showMessage({
|
this.showMessage({
|
||||||
title,
|
title,
|
||||||
message: runDataExecutedErrorMessage,
|
message:
|
||||||
type,
|
(nodeError?.description ?? runDataExecutedErrorMessage) +
|
||||||
|
this.$locale.baseText('pushConnection.executionError.openNode', {
|
||||||
|
interpolate: {
|
||||||
|
node: nodeError.node.name,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
type: 'error',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let title: string;
|
||||||
|
const isManualExecutionCancelled =
|
||||||
|
runDataExecuted.mode === 'manual' && runDataExecuted.status === 'canceled';
|
||||||
|
|
||||||
|
// Do not show the error message if the workflow got canceled manually
|
||||||
|
if (isManualExecutionCancelled) {
|
||||||
|
this.showMessage({
|
||||||
|
title: this.$locale.baseText('nodeView.showMessage.stopExecutionTry.title'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (runDataExecuted.data.resultData.lastNodeExecuted) {
|
||||||
|
title = `Problem in node ‘${runDataExecuted.data.resultData.lastNodeExecuted}‘`;
|
||||||
|
} else {
|
||||||
|
title = 'Problem executing workflow';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showMessage({
|
||||||
|
title,
|
||||||
|
message: runDataExecutedErrorMessage,
|
||||||
|
type: 'error',
|
||||||
|
duration: 0,
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Workflow did execute without a problem
|
// Workflow did execute without a problem
|
||||||
|
|
|
@ -4,57 +4,45 @@ import type {
|
||||||
IConnections,
|
IConnections,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
INodeType,
|
|
||||||
INodeTypeData,
|
INodeTypeData,
|
||||||
INodeTypes,
|
|
||||||
IVersionedNodeType,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Workflow, WorkflowDataProxy, NodeHelpers } from 'n8n-workflow';
|
import { WorkflowDataProxy } from 'n8n-workflow';
|
||||||
|
import { createTestWorkflowObject } from '@/__tests__/mocks';
|
||||||
|
|
||||||
class NodeTypesClass implements INodeTypes {
|
const nodeTypes: INodeTypeData = {
|
||||||
nodeTypes: INodeTypeData = {
|
'test.set': {
|
||||||
'test.set': {
|
sourcePath: '',
|
||||||
sourcePath: '',
|
type: {
|
||||||
type: {
|
description: {
|
||||||
description: {
|
displayName: 'Set',
|
||||||
displayName: 'Set',
|
name: 'set',
|
||||||
name: 'set',
|
group: ['input'],
|
||||||
group: ['input'],
|
version: 1,
|
||||||
version: 1,
|
description: 'Sets a value',
|
||||||
description: 'Sets a value',
|
defaults: {
|
||||||
defaults: {
|
name: 'Set',
|
||||||
name: 'Set',
|
color: '#0000FF',
|
||||||
color: '#0000FF',
|
|
||||||
},
|
|
||||||
inputs: ['main'],
|
|
||||||
outputs: ['main'],
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
displayName: 'Value1',
|
|
||||||
name: 'value1',
|
|
||||||
type: 'string',
|
|
||||||
default: 'default-value1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Value2',
|
|
||||||
name: 'value2',
|
|
||||||
type: 'string',
|
|
||||||
default: 'default-value2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Value1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'string',
|
||||||
|
default: 'default-value1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'string',
|
||||||
|
default: 'default-value2',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
|
||||||
return this.nodeTypes[nodeType].type;
|
|
||||||
}
|
|
||||||
|
|
||||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
|
||||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes: INode[] = [
|
const nodes: INode[] = [
|
||||||
{
|
{
|
||||||
|
@ -273,13 +261,13 @@ const runExecutionData: IRunExecutionData = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflow = new Workflow({
|
const workflow = createTestWorkflowObject({
|
||||||
id: '123',
|
id: '123',
|
||||||
name: 'test workflow',
|
name: 'test workflow',
|
||||||
nodes,
|
nodes,
|
||||||
connections,
|
connections,
|
||||||
active: false,
|
active: false,
|
||||||
nodeTypes: new NodeTypesClass(),
|
nodeTypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastNodeName = 'End';
|
const lastNodeName = 'End';
|
||||||
|
|
|
@ -791,6 +791,7 @@
|
||||||
"ndv.output.pageSize": "Page Size",
|
"ndv.output.pageSize": "Page Size",
|
||||||
"ndv.output.run": "Run",
|
"ndv.output.run": "Run",
|
||||||
"ndv.output.runNodeHint": "Execute this node to output data",
|
"ndv.output.runNodeHint": "Execute this node to output data",
|
||||||
|
"ndv.output.runNodeHintSubNode": "Output will appear here once the parent node is run",
|
||||||
"ndv.output.insertTestData": "insert test data",
|
"ndv.output.insertTestData": "insert test data",
|
||||||
"ndv.output.staleDataWarning.regular": "Node parameters have changed.<br>Execute node again to refresh output.",
|
"ndv.output.staleDataWarning.regular": "Node parameters have changed.<br>Execute node again to refresh output.",
|
||||||
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
|
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
|
||||||
|
@ -948,6 +949,7 @@
|
||||||
"nodeErrorView.dataBelowMayContain": "Data below may contain sensitive information. Proceed with caution when sharing.",
|
"nodeErrorView.dataBelowMayContain": "Data below may contain sensitive information. Proceed with caution when sharing.",
|
||||||
"nodeErrorView.details": "Details",
|
"nodeErrorView.details": "Details",
|
||||||
"nodeErrorView.error": "ERROR",
|
"nodeErrorView.error": "ERROR",
|
||||||
|
"nodeErrorView.errorSubNode": "Error in sub-node ‘{node}’",
|
||||||
"nodeErrorView.httpCode": "HTTP Code",
|
"nodeErrorView.httpCode": "HTTP Code",
|
||||||
"nodeErrorView.inParameter": "In or underneath Parameter",
|
"nodeErrorView.inParameter": "In or underneath Parameter",
|
||||||
"nodeErrorView.itemIndex": "Item Index",
|
"nodeErrorView.itemIndex": "Item Index",
|
||||||
|
@ -1240,6 +1242,7 @@
|
||||||
"pushConnection.executionFailed": "Execution failed",
|
"pushConnection.executionFailed": "Execution failed",
|
||||||
"pushConnection.executionFailed.message": "There might not be enough memory to finish the execution. Tips for avoiding this <a target=\"_blank\" href=\"https://docs.n8n.io/flow-logic/error-handling/memory-errors/\">here</a>",
|
"pushConnection.executionFailed.message": "There might not be enough memory to finish the execution. Tips for avoiding this <a target=\"_blank\" href=\"https://docs.n8n.io/flow-logic/error-handling/memory-errors/\">here</a>",
|
||||||
"pushConnection.executionError": "There was a problem executing the workflow{error}",
|
"pushConnection.executionError": "There was a problem executing the workflow{error}",
|
||||||
|
"pushConnection.executionError.openNode": " <a data-action='openNodeDetail' data-action-parameter-node='{node}'>Open node</a>",
|
||||||
"pushConnection.executionError.details": "<br /><strong>{details}</strong>",
|
"pushConnection.executionError.details": "<br /><strong>{details}</strong>",
|
||||||
"resourceLocator.id.placeholder": "Enter ID...",
|
"resourceLocator.id.placeholder": "Enter ID...",
|
||||||
"resourceLocator.mode.id": "By ID",
|
"resourceLocator.mode.id": "By ID",
|
||||||
|
@ -1762,6 +1765,7 @@
|
||||||
"nodeIssues.credentials.notIdentified": "Credentials with name {name} exist for {type}.",
|
"nodeIssues.credentials.notIdentified": "Credentials with name {name} exist for {type}.",
|
||||||
"nodeIssues.credentials.notIdentified.hint": "Credentials are not clearly identified. Please select the correct credentials.",
|
"nodeIssues.credentials.notIdentified.hint": "Credentials are not clearly identified. Please select the correct credentials.",
|
||||||
"nodeIssues.input.missing": "No node connected to required input \"{inputName}\"",
|
"nodeIssues.input.missing": "No node connected to required input \"{inputName}\"",
|
||||||
|
"nodeIssues.input.missingSubNode": "On the canvas, <a data-action='openSelectiveNodeCreator' data-action-parameter-connectiontype='{inputType}' data-action-parameter-node='{node}'>add a ‘{inputName}’</a> connected to the ‘{node}’ node ",
|
||||||
"ndv.trigger.moreInfo": "More info",
|
"ndv.trigger.moreInfo": "More info",
|
||||||
"ndv.trigger.copiedTestUrl": "Test URL copied to clipboard",
|
"ndv.trigger.copiedTestUrl": "Test URL copied to clipboard",
|
||||||
"ndv.trigger.webhookBasedNode.executionsHelp.inactive": "<b>While building your workflow</b>, click the 'listen' button, then go to {service} and make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /> <b>Once you're happy with your workflow</b>, <a data-key=\"activate\">activate</a> it. Then every time there's a matching event in {service}, the workflow will execute. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor.",
|
"ndv.trigger.webhookBasedNode.executionsHelp.inactive": "<b>While building your workflow</b>, click the 'listen' button, then go to {service} and make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /> <b>Once you're happy with your workflow</b>, <a data-key=\"activate\">activate</a> it. Then every time there's a matching event in {service}, the workflow will execute. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor.",
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const register = () => {
|
||||||
const sizeDifference = (unconnectedPlusSize - unconnectedDiamondWidth) / 2;
|
const sizeDifference = (unconnectedPlusSize - unconnectedDiamondWidth) / 2;
|
||||||
|
|
||||||
const container = svg.node('g', {
|
const container = svg.node('g', {
|
||||||
style: `--svg-color: var(${endpointInstance.params.color})`,
|
style: `--svg-color: var(--endpoint-svg-color, var(${endpointInstance.params.color}))`,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,6 +46,14 @@ export class N8nAddInputEndpoint extends EndpointRepresentation<ComputedN8nAddIn
|
||||||
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setError() {
|
||||||
|
this.endpoint.addClass('add-input-endpoint-error');
|
||||||
|
}
|
||||||
|
|
||||||
|
resetError() {
|
||||||
|
this.endpoint.removeClass('add-input-endpoint-error');
|
||||||
|
}
|
||||||
|
|
||||||
fireClickEvent = (endpoint: Endpoint) => {
|
fireClickEvent = (endpoint: Endpoint) => {
|
||||||
if (endpoint === this.endpoint) {
|
if (endpoint === this.endpoint) {
|
||||||
this.instance.fire(EVENT_ADD_INPUT_ENDPOINT_CLICK, this.endpoint);
|
this.instance.fire(EVENT_ADD_INPUT_ENDPOINT_CLICK, this.endpoint);
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useHistoryStore } from '@/stores/history.store';
|
||||||
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import type { INodeUi, XYPosition } from '@/Interface';
|
import type { INodeUi, XYPosition } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
applyScale,
|
applyScale,
|
||||||
|
@ -37,12 +41,6 @@ import {
|
||||||
} from '@/utils/nodeViewUtils';
|
} from '@/utils/nodeViewUtils';
|
||||||
import type { PointXY } from '@jsplumb/util';
|
import type { PointXY } from '@jsplumb/util';
|
||||||
|
|
||||||
import { useWorkflowsStore } from './workflows.store';
|
|
||||||
import { useNodeTypesStore } from './nodeTypes.store';
|
|
||||||
import { useUIStore } from './ui.store';
|
|
||||||
import { useHistoryStore } from './history.store';
|
|
||||||
import { useSourceControlStore } from './sourceControl.store';
|
|
||||||
|
|
||||||
export const useCanvasStore = defineStore('canvas', () => {
|
export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const workflowStore = useWorkflowsStore();
|
const workflowStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
|
@ -157,6 +157,26 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
||||||
}
|
}
|
||||||
acc[outputType].push(node.name);
|
acc[outputType].push(node.name);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// If outputs is not an array, it must be a string expression
|
||||||
|
// in which case we'll try to match all possible non-main output types that are supported
|
||||||
|
const connectorTypes: ConnectionTypes[] = [
|
||||||
|
NodeConnectionType.AiVectorStore,
|
||||||
|
NodeConnectionType.AiChain,
|
||||||
|
NodeConnectionType.AiDocument,
|
||||||
|
NodeConnectionType.AiEmbedding,
|
||||||
|
NodeConnectionType.AiLanguageModel,
|
||||||
|
NodeConnectionType.AiMemory,
|
||||||
|
NodeConnectionType.AiOutputParser,
|
||||||
|
NodeConnectionType.AiTextSplitter,
|
||||||
|
NodeConnectionType.AiTool,
|
||||||
|
];
|
||||||
|
connectorTypes.forEach((outputType: ConnectionTypes) => {
|
||||||
|
if (outputTypes.includes(outputType)) {
|
||||||
|
acc[outputType] = acc[outputType] || [];
|
||||||
|
acc[outputType].push(node.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
@ -663,7 +663,7 @@ export const getOutputSummary = (
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
data.forEach((run: ITaskData) => {
|
data.forEach((run: ITaskData) => {
|
||||||
if (!run.data?.[connectionType]) {
|
if (!run?.data?.[connectionType]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
:renaming="renamingActive"
|
:renaming="renamingActive"
|
||||||
:isProductionExecutionPreview="isProductionExecutionPreview"
|
:isProductionExecutionPreview="isProductionExecutionPreview"
|
||||||
@redrawNode="redrawNode"
|
@redrawNode="redrawNode"
|
||||||
|
@switchSelectedNode="onSwitchSelectedNode"
|
||||||
@valueChanged="valueChanged"
|
@valueChanged="valueChanged"
|
||||||
@stopExecution="stopExecution"
|
@stopExecution="stopExecution"
|
||||||
@saveKeyboardShortcut="onSaveKeyboardShortcut"
|
@saveKeyboardShortcut="onSaveKeyboardShortcut"
|
||||||
|
@ -356,7 +357,11 @@ import {
|
||||||
N8nPlusEndpointType,
|
N8nPlusEndpointType,
|
||||||
EVENT_PLUS_ENDPOINT_CLICK,
|
EVENT_PLUS_ENDPOINT_CLICK,
|
||||||
} from '@/plugins/jsplumb/N8nPlusEndpointType';
|
} from '@/plugins/jsplumb/N8nPlusEndpointType';
|
||||||
import { EVENT_ADD_INPUT_ENDPOINT_CLICK } from '@/plugins/jsplumb/N8nAddInputEndpointType';
|
import type { N8nAddInputEndpoint } from '@/plugins/jsplumb/N8nAddInputEndpointType';
|
||||||
|
import {
|
||||||
|
EVENT_ADD_INPUT_ENDPOINT_CLICK,
|
||||||
|
N8nAddInputEndpointType,
|
||||||
|
} from '@/plugins/jsplumb/N8nAddInputEndpointType';
|
||||||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||||
import { getConnectorPaintStyleData, OVERLAY_ENDPOINT_ARROW_ID } from '@/utils/nodeViewUtils';
|
import { getConnectorPaintStyleData, OVERLAY_ENDPOINT_ARROW_ID } from '@/utils/nodeViewUtils';
|
||||||
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
|
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
|
||||||
|
@ -788,6 +793,42 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.runWorkflow({});
|
await this.runWorkflow({});
|
||||||
|
this.refreshEndpointsErrorsState();
|
||||||
|
},
|
||||||
|
resetEndpointsErrors() {
|
||||||
|
const allEndpoints = Object.values(this.instance.getManagedElements()).flatMap(
|
||||||
|
(el) => el.endpoints,
|
||||||
|
);
|
||||||
|
|
||||||
|
allEndpoints
|
||||||
|
.filter((endpoint) => endpoint?.endpoint.type === N8nAddInputEndpointType)
|
||||||
|
.forEach((endpoint) => {
|
||||||
|
const n8nAddInputEndpoint = endpoint?.endpoint as N8nAddInputEndpoint;
|
||||||
|
if (n8nAddInputEndpoint && (endpoint?.connections ?? []).length > 0) {
|
||||||
|
n8nAddInputEndpoint.resetError();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
refreshEndpointsErrorsState() {
|
||||||
|
const nodeIssues = this.workflowsStore.allNodes.filter((n) => n.issues);
|
||||||
|
// Set input color to red if there are issues
|
||||||
|
this.resetEndpointsErrors();
|
||||||
|
nodeIssues.forEach((node) => {
|
||||||
|
const managedNode = this.instance.getManagedElement(node.id);
|
||||||
|
const endpoints = this.instance.getEndpoints(managedNode);
|
||||||
|
|
||||||
|
Object.keys(node?.issues?.input ?? {}).forEach((connectionType) => {
|
||||||
|
const inputEndpointsWithIssues = endpoints.filter(
|
||||||
|
(e) => e._defaultType.scope === connectionType,
|
||||||
|
);
|
||||||
|
inputEndpointsWithIssues.forEach((endpoint) => {
|
||||||
|
const n8nAddInputEndpoint = endpoint?.endpoint as N8nAddInputEndpoint;
|
||||||
|
if (n8nAddInputEndpoint) {
|
||||||
|
n8nAddInputEndpoint.setError();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onRunContainerClick() {
|
onRunContainerClick() {
|
||||||
if (this.containsTrigger && !this.allTriggersDisabled) return;
|
if (this.containsTrigger && !this.allTriggersDisabled) return;
|
||||||
|
@ -2398,36 +2439,21 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
this.historyStore.stopRecordingUndo();
|
this.historyStore.stopRecordingUndo();
|
||||||
},
|
},
|
||||||
insertNodeAfterSelected(info: {
|
getNodeCreatorFilter(nodeName: string, outputType?: NodeConnectionType) {
|
||||||
sourceId: string;
|
|
||||||
index: number;
|
|
||||||
eventSource: NodeCreatorOpenSource;
|
|
||||||
connection?: Connection;
|
|
||||||
nodeCreatorView?: string;
|
|
||||||
outputType?: NodeConnectionType;
|
|
||||||
endpointUuid?: string;
|
|
||||||
}) {
|
|
||||||
const type = info.outputType || NodeConnectionType.Main;
|
|
||||||
|
|
||||||
let filter;
|
let filter;
|
||||||
// Get the node and set it as active that new nodes
|
|
||||||
// which get created get automatically connected
|
|
||||||
// to it.
|
|
||||||
const sourceNode = this.workflowsStore.getNodeById(info.sourceId);
|
|
||||||
if (!sourceNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflow = this.getCurrentWorkflow();
|
const workflow = this.getCurrentWorkflow();
|
||||||
|
const workflowNode = workflow.getNode(nodeName);
|
||||||
|
if (!workflowNode) return { nodes: [] };
|
||||||
|
|
||||||
const nodeType = this.nodeTypesStore.getNodeType(sourceNode.type, sourceNode.typeVersion);
|
const nodeType = this.nodeTypesStore.getNodeType(
|
||||||
|
workflowNode?.type,
|
||||||
|
workflowNode.typeVersion,
|
||||||
|
);
|
||||||
if (nodeType) {
|
if (nodeType) {
|
||||||
const workflowNode = workflow.getNode(sourceNode.name);
|
const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType);
|
||||||
const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode!, nodeType);
|
|
||||||
|
|
||||||
const filterFound = inputs.filter((input) => {
|
const filterFound = inputs.filter((input) => {
|
||||||
if (typeof input === 'string' || input.type !== info.outputType || !input.filter) {
|
if (typeof input === 'string' || input.type !== outputType || !input.filter) {
|
||||||
// No filters defined or wrong connection type
|
// No filters defined or wrong connection type
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2440,6 +2466,26 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
},
|
||||||
|
insertNodeAfterSelected(info: {
|
||||||
|
sourceId: string;
|
||||||
|
index: number;
|
||||||
|
eventSource: NodeCreatorOpenSource;
|
||||||
|
connection?: Connection;
|
||||||
|
nodeCreatorView?: string;
|
||||||
|
outputType?: NodeConnectionType;
|
||||||
|
endpointUuid?: string;
|
||||||
|
}) {
|
||||||
|
const type = info.outputType ?? NodeConnectionType.Main;
|
||||||
|
// Get the node and set it as active that new nodes
|
||||||
|
// which get created get automatically connected
|
||||||
|
// to it.
|
||||||
|
const sourceNode = this.workflowsStore.getNodeById(info.sourceId);
|
||||||
|
if (!sourceNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.uiStore.lastSelectedNode = sourceNode.name;
|
this.uiStore.lastSelectedNode = sourceNode.name;
|
||||||
this.uiStore.lastSelectedNodeEndpointUuid =
|
this.uiStore.lastSelectedNodeEndpointUuid =
|
||||||
info.endpointUuid ?? info.connection?.target.jtk?.endpoint.uuid;
|
info.endpointUuid ?? info.connection?.target.jtk?.endpoint.uuid;
|
||||||
|
@ -2464,7 +2510,11 @@ export default defineComponent({
|
||||||
|
|
||||||
if (isScopedConnection) {
|
if (isScopedConnection) {
|
||||||
useViewStacks()
|
useViewStacks()
|
||||||
.gotoCompatibleConnectionView(type, isOutput, filter)
|
.gotoCompatibleConnectionView(
|
||||||
|
type,
|
||||||
|
isOutput,
|
||||||
|
this.getNodeCreatorFilter(sourceNode.name, type),
|
||||||
|
)
|
||||||
.catch((e) => {});
|
.catch((e) => {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2688,7 +2738,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.dropPrevented = false;
|
this.dropPrevented = false;
|
||||||
void this.updateNodesInputIssues();
|
this.updateNodesInputIssues();
|
||||||
|
this.resetEndpointsErrors();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
@ -3616,6 +3667,9 @@ export default defineComponent({
|
||||||
}, recordingTimeout);
|
}, recordingTimeout);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async onSwitchSelectedNode(nodeName: string) {
|
||||||
|
this.nodeSelectedByName(nodeName, true, true);
|
||||||
|
},
|
||||||
async redrawNode(nodeName: string) {
|
async redrawNode(nodeName: string) {
|
||||||
// TODO: Improve later
|
// TODO: Improve later
|
||||||
// For now we redraw the node by simply renaming it. Can for sure be
|
// For now we redraw the node by simply renaming it. Can for sure be
|
||||||
|
@ -4617,6 +4671,37 @@ export default defineComponent({
|
||||||
|
|
||||||
sourceControlEventBus.on('pull', this.onSourceControlPull);
|
sourceControlEventBus.on('pull', this.onSourceControlPull);
|
||||||
|
|
||||||
|
this.registerCustomAction({
|
||||||
|
key: 'openNodeDetail',
|
||||||
|
action: ({ node }: { node: string }) => {
|
||||||
|
this.nodeSelectedByName(node, true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.registerCustomAction({
|
||||||
|
key: 'openSelectiveNodeCreator',
|
||||||
|
action: ({ connectiontype, node }: { connectiontype: NodeConnectionType; node: string }) => {
|
||||||
|
this.onToggleNodeCreator({
|
||||||
|
source: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
|
||||||
|
createNodeActive: true,
|
||||||
|
nodeCreatorView: AI_NODE_CREATOR_VIEW,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ndvStore.activeNodeName = null;
|
||||||
|
// Select the node so that the node creator knows which node to connect to
|
||||||
|
const nodeData = this.workflowsStore.getNodeByName(node);
|
||||||
|
if (connectiontype && nodeData) {
|
||||||
|
this.insertNodeAfterSelected({
|
||||||
|
index: 0,
|
||||||
|
endpointUuid: `${nodeData.id}-input${connectiontype}0`,
|
||||||
|
eventSource: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
|
||||||
|
outputType: connectiontype,
|
||||||
|
sourceId: nodeData.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.readOnlyEnvRouteCheck();
|
this.readOnlyEnvRouteCheck();
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
|
@ -4684,6 +4769,8 @@ export default defineComponent({
|
||||||
document.removeEventListener('keydown', this.keyDown);
|
document.removeEventListener('keydown', this.keyDown);
|
||||||
document.removeEventListener('keyup', this.keyUp);
|
document.removeEventListener('keyup', this.keyUp);
|
||||||
this.unregisterCustomAction('showNodeCreator');
|
this.unregisterCustomAction('showNodeCreator');
|
||||||
|
this.unregisterCustomAction('openNodeDetail');
|
||||||
|
this.unregisterCustomAction('openSelectiveNodeCreator');
|
||||||
|
|
||||||
if (!this.isDemo) {
|
if (!this.isDemo) {
|
||||||
this.pushStore.pushDisconnect();
|
this.pushStore.pushDisconnect();
|
||||||
|
|
|
@ -22,10 +22,11 @@ export class OpenAiApi implements ICredentialType {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Organization ID',
|
displayName: 'Organization ID (optional)',
|
||||||
name: 'organizationId',
|
name: 'organizationId',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
hint: 'Only required if you belong to multiple organisations',
|
||||||
description:
|
description:
|
||||||
"For users who belong to multiple organizations, you can set which organization is used for an API request. Usage from these API requests will count against the specified organization's subscription quota.",
|
"For users who belong to multiple organizations, you can set which organization is used for an API request. Usage from these API requests will count against the specified organization's subscription quota.",
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,6 +25,9 @@ export class SupabaseApi implements ICredentialType {
|
||||||
name: 'serviceRole',
|
name: 'serviceRole',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: '',
|
default: '',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1098,6 +1098,7 @@ export interface ILoadOptions {
|
||||||
|
|
||||||
export interface INodePropertyTypeOptions {
|
export interface INodePropertyTypeOptions {
|
||||||
action?: string; // Supported by: button
|
action?: string; // Supported by: button
|
||||||
|
containerClass?: string; // Supported by: notice
|
||||||
alwaysOpenEditWindow?: boolean; // Supported by: json
|
alwaysOpenEditWindow?: boolean; // Supported by: json
|
||||||
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
|
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
|
||||||
editor?: EditorType; // Supported by: string
|
editor?: EditorType; // Supported by: string
|
||||||
|
@ -1526,6 +1527,7 @@ export interface IPostReceiveSort extends IPostReceiveBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectionTypes =
|
export type ConnectionTypes =
|
||||||
|
| 'ai_agent'
|
||||||
| 'ai_chain'
|
| 'ai_chain'
|
||||||
| 'ai_document'
|
| 'ai_document'
|
||||||
| 'ai_embedding'
|
| 'ai_embedding'
|
||||||
|
@ -1540,6 +1542,8 @@ export type ConnectionTypes =
|
||||||
| 'main';
|
| 'main';
|
||||||
|
|
||||||
export const enum NodeConnectionType {
|
export const enum NodeConnectionType {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
AiAgent = 'ai_agent',
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
AiChain = 'ai_chain',
|
AiChain = 'ai_chain',
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
@ -2376,3 +2380,4 @@ export type BannerName =
|
||||||
| 'EMAIL_CONFIRMATION';
|
| 'EMAIL_CONFIRMATION';
|
||||||
|
|
||||||
export type Severity = 'warning' | 'error';
|
export type Severity = 'warning' | 'error';
|
||||||
|
export type Functionality = 'regular' | 'configuration-node';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { IDataObject, JsonObject, Severity } from '../../Interfaces';
|
import type { Functionality, IDataObject, JsonObject, Severity } from '../../Interfaces';
|
||||||
import { ApplicationError } from '../application.error';
|
import { ApplicationError } from '../application.error';
|
||||||
|
|
||||||
interface ExecutionBaseErrorOptions {
|
interface ExecutionBaseErrorOptions {
|
||||||
|
@ -21,6 +21,8 @@ export abstract class ExecutionBaseError extends ApplicationError {
|
||||||
|
|
||||||
severity: Severity = 'error';
|
severity: Severity = 'error';
|
||||||
|
|
||||||
|
functionality: Functionality = 'regular';
|
||||||
|
|
||||||
constructor(message: string, { cause }: ExecutionBaseErrorOptions) {
|
constructor(message: string, { cause }: ExecutionBaseErrorOptions) {
|
||||||
const options = cause instanceof Error ? { cause } : {};
|
const options = cause instanceof Error ? { cause } : {};
|
||||||
super(message, options);
|
super(message, options);
|
||||||
|
|
|
@ -4,7 +4,14 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
import { parseString } from 'xml2js';
|
import { parseString } from 'xml2js';
|
||||||
import type { INode, JsonObject, IDataObject, IStatusCodeMessages, Severity } from '..';
|
import type {
|
||||||
|
INode,
|
||||||
|
JsonObject,
|
||||||
|
IDataObject,
|
||||||
|
IStatusCodeMessages,
|
||||||
|
Severity,
|
||||||
|
Functionality,
|
||||||
|
} from '../Interfaces';
|
||||||
import { NodeError } from './abstract/node.error';
|
import { NodeError } from './abstract/node.error';
|
||||||
import { removeCircularRefs } from '../utils';
|
import { removeCircularRefs } from '../utils';
|
||||||
|
|
||||||
|
@ -15,6 +22,7 @@ export interface NodeOperationErrorOptions {
|
||||||
itemIndex?: number;
|
itemIndex?: number;
|
||||||
severity?: Severity;
|
severity?: Severity;
|
||||||
messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node
|
messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node
|
||||||
|
functionality?: Functionality;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NodeApiErrorOptions extends NodeOperationErrorOptions {
|
interface NodeApiErrorOptions extends NodeOperationErrorOptions {
|
||||||
|
@ -113,6 +121,7 @@ export class NodeApiError extends NodeError {
|
||||||
runIndex,
|
runIndex,
|
||||||
itemIndex,
|
itemIndex,
|
||||||
severity,
|
severity,
|
||||||
|
functionality,
|
||||||
messageMapping,
|
messageMapping,
|
||||||
}: NodeApiErrorOptions = {},
|
}: NodeApiErrorOptions = {},
|
||||||
) {
|
) {
|
||||||
|
@ -206,6 +215,7 @@ export class NodeApiError extends NodeError {
|
||||||
messageMapping,
|
messageMapping,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (functionality !== undefined) this.context.functionality = functionality;
|
||||||
if (runIndex !== undefined) this.context.runIndex = runIndex;
|
if (runIndex !== undefined) this.context.runIndex = runIndex;
|
||||||
if (itemIndex !== undefined) this.context.itemIndex = itemIndex;
|
if (itemIndex !== undefined) this.context.itemIndex = itemIndex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export class NodeOperationError extends NodeError {
|
||||||
|
|
||||||
if (options.message) this.message = options.message;
|
if (options.message) this.message = options.message;
|
||||||
if (options.severity) this.severity = options.severity;
|
if (options.severity) this.severity = options.severity;
|
||||||
|
if (options.functionality) this.functionality = options.functionality;
|
||||||
this.description = options.description;
|
this.description = options.description;
|
||||||
this.context.runIndex = options.runIndex;
|
this.context.runIndex = options.runIndex;
|
||||||
this.context.itemIndex = options.itemIndex;
|
this.context.itemIndex = options.itemIndex;
|
||||||
|
|
|
@ -965,6 +965,9 @@ importers:
|
||||||
'@sentry/vite-plugin':
|
'@sentry/vite-plugin':
|
||||||
specifier: ^2.5.0
|
specifier: ^2.5.0
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
|
'@testing-library/vue':
|
||||||
|
specifier: ^7.0.0
|
||||||
|
version: 7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4)
|
||||||
'@types/dateformat':
|
'@types/dateformat':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
|
@ -9722,7 +9725,7 @@ packages:
|
||||||
lodash.uniqby: 4.7.0
|
lodash.uniqby: 4.7.0
|
||||||
node-fetch: 2.6.8
|
node-fetch: 2.6.8
|
||||||
parse-github-url: 1.0.2
|
parse-github-url: 1.0.2
|
||||||
regenerator-runtime: 0.13.9
|
regenerator-runtime: 0.13.11
|
||||||
semver: 7.5.4
|
semver: 7.5.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
@ -19248,10 +19251,6 @@ packages:
|
||||||
/regenerator-runtime@0.13.11:
|
/regenerator-runtime@0.13.11:
|
||||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||||
|
|
||||||
/regenerator-runtime@0.13.9:
|
|
||||||
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/regenerator-transform@0.15.1:
|
/regenerator-transform@0.15.1:
|
||||||
resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==}
|
resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue