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')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
|
@ -120,8 +124,8 @@ describe('Execution', () => {
|
|||
workflowPage.getters.clearExecutionDataButton().click();
|
||||
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)
|
||||
workflowPage.getters.warningToast().should('be.visible');
|
||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||
workflowPage.getters.successToast().should('be.visible');
|
||||
});
|
||||
|
||||
it('should test webhook workflow', () => {
|
||||
|
@ -267,7 +271,7 @@ describe('Execution', () => {
|
|||
workflowPage.getters.clearExecutionDataButton().click();
|
||||
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)
|
||||
workflowPage.getters.warningToast().should('be.visible');
|
||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
||||
workflowPage.getters.successToast().should('be.visible');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'cypress-real-events';
|
||||
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) => {
|
||||
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', () => {
|
||||
cy.window().then((win) => {
|
||||
const spy = cy.spy(win.console, 'error');
|
||||
|
|
|
@ -39,6 +39,7 @@ declare global {
|
|||
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
|
||||
): void;
|
||||
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
||||
push(type: string, data: unknown): void;
|
||||
shouldNotHaveConsoleErrors(): void;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,8 +47,8 @@
|
|||
"@types/supertest": "^2.0.12",
|
||||
"@vitest/coverage-v8": "^0.33.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress-otp": "^1.0.3",
|
||||
"cypress": "^12.17.2",
|
||||
"cypress-otp": "^1.0.3",
|
||||
"cypress-real-events": "^1.9.1",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Request } from 'express';
|
||||
import { Service } from 'typedi';
|
||||
import { Container, Service } from 'typedi';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import config from '@/config';
|
||||
import type { Role } from '@db/entities/Role';
|
||||
|
@ -13,8 +13,9 @@ import { License } from '@/License';
|
|||
import { LICENSE_FEATURES, inE2ETests } from '@/constants';
|
||||
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
|
||||
import type { UserSetupPayload } from '@/requests';
|
||||
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||
import type { BooleanLicenseFeature, IPushDataType } from '@/Interfaces';
|
||||
import { MfaService } from '@/Mfa/mfa.service';
|
||||
import { Push } from '@/push';
|
||||
|
||||
if (!inE2ETests) {
|
||||
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()
|
||||
@NoAuthRequired()
|
||||
@RestController('/e2e')
|
||||
|
@ -95,6 +106,17 @@ export class E2EController {
|
|||
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')
|
||||
setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) {
|
||||
const { enabled, feature } = req.body;
|
||||
|
|
|
@ -63,6 +63,10 @@ export class Push extends EventEmitter {
|
|||
this.backend.send(type, data, sessionId);
|
||||
}
|
||||
|
||||
getBackend() {
|
||||
return this.backend;
|
||||
}
|
||||
|
||||
sendToUsers<D>(type: IPushDataType, data: D, userIds: Array<User['id']>) {
|
||||
this.backend.sendToUsers(type, data, userIds);
|
||||
}
|
||||
|
|
|
@ -3301,9 +3301,11 @@ export function getExecuteFunctions(
|
|||
// Display on the calling node which node has the error
|
||||
throw new NodeOperationError(
|
||||
connectedNode,
|
||||
`Error on node "${connectedNode.name}" which is connected via input "${inputName}"`,
|
||||
`Error in sub-node ${connectedNode.name}`,
|
||||
{
|
||||
itemIndex,
|
||||
functionality: 'configuration-node',
|
||||
description: error.message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:style="iconStyleData"
|
||||
>
|
||||
<!-- 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>
|
||||
<div v-if="type !== 'unknown'" :class="$style.icon">
|
||||
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
||||
|
@ -78,6 +78,10 @@ export default defineComponent({
|
|||
showTooltip: {
|
||||
type: Boolean,
|
||||
},
|
||||
tooltipPosition: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
badge: { type: Object as PropType<{ src: string; type: string }> },
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -66,7 +66,9 @@ export default defineComponent({
|
|||
},
|
||||
sanitizeHtml(text: string): string {
|
||||
return sanitizeHtml(text, {
|
||||
allowedAttributes: { a: ['data-key', 'href', 'target'] },
|
||||
allowedAttributes: {
|
||||
a: ['data-key', 'href', 'target', 'data-action', 'data-action-parameter-connectiontype'],
|
||||
},
|
||||
});
|
||||
},
|
||||
onClick(event: MouseEvent) {
|
||||
|
|
|
@ -31,6 +31,13 @@
|
|||
|
||||
// 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
|
||||
--color-canvas-background: var(--prim-gray-820);
|
||||
--color-canvas-dot: var(--prim-gray-670);
|
||||
|
@ -174,6 +181,11 @@
|
|||
--border-color-light: var(--color-foreground-light);
|
||||
--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: 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-secondary-link: var(--prim-color-secondary-tint-200);
|
||||
--color-secondary-link-hover: var(--prim-color-secondary-tint-100);
|
||||
|
|
|
@ -64,6 +64,13 @@
|
|||
|
||||
// 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
|
||||
--color-canvas-background: var(--prim-gray-10);
|
||||
--color-canvas-dot: var(--prim-gray-120);
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
"@faker-js/faker": "^8.0.2",
|
||||
"@pinia/testing": "^0.1.3",
|
||||
"@sentry/vite-plugin": "^2.5.0",
|
||||
"@testing-library/vue": "^7.0.0",
|
||||
"@types/dateformat": "^3.0.0",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/humanize-duration": "^3.27.1",
|
||||
|
|
|
@ -1269,6 +1269,7 @@ export type NodeCreatorOpenSource =
|
|||
| 'tab'
|
||||
| 'node_connection_action'
|
||||
| 'node_connection_drop'
|
||||
| 'notice_error_message'
|
||||
| 'add_node_button';
|
||||
|
||||
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: '' },
|
||||
onboardingCallPromptEnabled: false,
|
||||
personalizationSurveyEnabled: false,
|
||||
releaseChannel: 'stable',
|
||||
posthog: {
|
||||
apiHost: '',
|
||||
apiKey: '',
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { EventBus } from 'n8n-design-system/utils';
|
|||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import Modal from './Modal.vue';
|
||||
import { CHAT_EMBED_MODAL_KEY, WEBHOOK_NODE_TYPE } from '../constants';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
||||
|
@ -22,7 +21,6 @@ const props = defineProps({
|
|||
const i18n = useI18n();
|
||||
const rootStore = useRootStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const tabs = ref([
|
||||
{
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import Vue from 'vue';
|
||||
import { addVarType } from '../utils';
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import type { CodeNodeEditorMixin } from '../types';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
const escape = (str: string) => str.replace('$', '\\$');
|
||||
|
||||
export const secretsCompletions = (Vue as CodeNodeEditorMixin).extend({
|
||||
export const secretsCompletions = defineComponent({
|
||||
methods: {
|
||||
/**
|
||||
* Complete `$secrets.` to `$secrets.providerName` and `$secrets.providerName.secretName`.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<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>
|
||||
<details>
|
||||
|
@ -125,7 +125,12 @@ import { copyPaste } from '@/mixins/copyPaste';
|
|||
import { useToast } from '@/composables/useToast';
|
||||
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 { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
@ -170,6 +175,18 @@ export default defineComponent({
|
|||
.replace(/%%PARAMETER_FULL%%/g, parameterFullName);
|
||||
},
|
||||
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) {
|
||||
return sanitizeHtml(this.error.description);
|
||||
}
|
||||
|
@ -182,6 +199,20 @@ export default defineComponent({
|
|||
getErrorMessage(): string {
|
||||
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) {
|
||||
return baseErrorMessage + this.error.message;
|
||||
}
|
||||
|
|
|
@ -166,7 +166,13 @@ import { defineComponent } from 'vue';
|
|||
import { mapStores } from 'pinia';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
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 { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||
|
@ -271,9 +277,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (
|
||||
(inputs.length === 0 ||
|
||||
inputs.find((inputName) => inputName !== NodeConnectionType.Main)) &&
|
||||
outputs.find((outputName) => outputName !== NodeConnectionType.Main)
|
||||
inputs.length === 0 ||
|
||||
(inputs.every((input) => this.filterOutConnectionType(input, NodeConnectionType.Main)) &&
|
||||
outputs.find((output) => this.filterOutConnectionType(output, NodeConnectionType.Main)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@ -384,6 +390,14 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
filterOutConnectionType(
|
||||
item: ConnectionTypes | INodeOutputConfiguration,
|
||||
type: ConnectionTypes,
|
||||
) {
|
||||
if (!item) return false;
|
||||
|
||||
return typeof item === 'string' ? item !== type : item.type !== type;
|
||||
},
|
||||
onInputModeChange(val: MappingMode) {
|
||||
this.inputMode = val;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<NDVFloatingNodes
|
||||
v-if="activeNode"
|
||||
@switchSelectedNode="onSwitchSelectedNode"
|
||||
:root-node="activeNode"
|
||||
type="input"
|
||||
/>
|
||||
<div :class="$style.inputPanel" v-if="!hideInputAndOutput" :style="inputPanelStyles">
|
||||
<slot name="input"></slot>
|
||||
</div>
|
||||
|
@ -50,6 +56,7 @@ import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '
|
|||
import { debounceHelper } from '@/mixins/debounce';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { ndvEventBus } from '@/event-bus';
|
||||
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||
|
||||
const SIDE_MARGIN = 24;
|
||||
const SIDE_PANELS_MARGIN = 80;
|
||||
|
@ -70,6 +77,7 @@ export default defineComponent({
|
|||
mixins: [debounceHelper],
|
||||
components: {
|
||||
PanelDragButton,
|
||||
NDVFloatingNodes,
|
||||
},
|
||||
props: {
|
||||
isDraggable: {
|
||||
|
@ -136,6 +144,9 @@ export default defineComponent({
|
|||
} {
|
||||
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType);
|
||||
},
|
||||
activeNode() {
|
||||
return this.ndvStore.activeNode;
|
||||
},
|
||||
supportedResizeDirections(): string[] {
|
||||
const supportedDirections = ['right'];
|
||||
|
||||
|
@ -249,6 +260,9 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onSwitchSelectedNode(node: string) {
|
||||
this.$emit('switchSelectedNode', node);
|
||||
},
|
||||
getInitialLeftPosition(width: number) {
|
||||
if (this.currentNodePaneType === 'dragless')
|
||||
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,
|
||||
INodeTypeDescription,
|
||||
ITaskData,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||
|
||||
|
@ -392,7 +393,7 @@ export default defineComponent({
|
|||
nodeExecutionStatus(): string {
|
||||
const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name];
|
||||
if (nodeExecutionRunData) {
|
||||
return nodeExecutionRunData[0].executionStatus ?? '';
|
||||
return nodeExecutionRunData.filter(Boolean)[0].executionStatus ?? '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
@ -401,7 +402,7 @@ export default defineComponent({
|
|||
const nodeExecutionRunData = this.workflowsStore.getWorkflowRunData?.[this.name];
|
||||
if (nodeExecutionRunData) {
|
||||
nodeExecutionRunData.forEach((executionRunData) => {
|
||||
if (executionRunData.error) {
|
||||
if (executionRunData?.error) {
|
||||
issues.push(
|
||||
`${executionRunData.error.message}${
|
||||
executionRunData.error.description ? ` (${executionRunData.error.description})` : ''
|
||||
|
@ -426,7 +427,10 @@ export default defineComponent({
|
|||
return this.node ? this.node.position : [0, 0];
|
||||
},
|
||||
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 {
|
||||
return this.$locale.shortNodeType(this.data.type);
|
||||
|
@ -482,9 +486,15 @@ export default defineComponent({
|
|||
borderColor = '--color-foreground-base';
|
||||
} else if (!this.isExecuting) {
|
||||
if (this.hasIssues) {
|
||||
// Do not set red border if there is an issue with the configuration node
|
||||
if (
|
||||
(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) {
|
||||
borderColor = '--color-canvas-node-pinned-border';
|
||||
} else if (this.nodeExecutionStatus === 'unknown') {
|
||||
|
@ -608,6 +618,7 @@ export default defineComponent({
|
|||
!this.isTriggerNode ||
|
||||
this.isManualTypeNode ||
|
||||
this.isScheduledGroup ||
|
||||
this.uiStore.isModalActive ||
|
||||
dataItemsCount === 0
|
||||
)
|
||||
return;
|
||||
|
@ -1333,6 +1344,10 @@ export default defineComponent({
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
&.add-input-endpoint-error {
|
||||
--endpoint-svg-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.add-input-endpoint-default {
|
||||
transition: transform var(--add-input-endpoint--transition-duration) ease;
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ function onBackButton() {
|
|||
v-if="activeViewStack.info && !activeViewStack.search"
|
||||
:class="$style.info"
|
||||
:content="activeViewStack.info"
|
||||
theme="info"
|
||||
theme="warning"
|
||||
/>
|
||||
<!-- Actions mode -->
|
||||
<ActionsRenderer v-if="isActionsMode && activeViewStack.subcategory" v-bind="$attrs" />
|
||||
|
|
|
@ -8,6 +8,7 @@ import type {
|
|||
SimplifiedNodeType,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
AI_CODE_NODE_TYPE,
|
||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||
DEFAULT_SUBCATEGORY,
|
||||
TRIGGER_NODE_CREATOR_VIEW,
|
||||
|
@ -152,6 +153,9 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
|||
},
|
||||
panelClass: relatedAIView?.properties.panelClass,
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
width="auto"
|
||||
append-to-body
|
||||
data-test-id="ndv"
|
||||
:data-has-output-connection="hasOutputConnection"
|
||||
>
|
||||
<n8n-tooltip
|
||||
placement="bottom-start"
|
||||
|
@ -42,6 +43,8 @@
|
|||
:isDraggable="!isTriggerNode"
|
||||
:hasDoubleWidth="activeNodeType?.parameterPane === 'wide'"
|
||||
:nodeType="activeNodeType"
|
||||
:key="activeNode.name"
|
||||
@switchSelectedNode="onSwitchSelectedNode"
|
||||
@close="close"
|
||||
@init="onPanelsInit"
|
||||
@dragstart="onDragStart"
|
||||
|
@ -275,6 +278,15 @@ export default defineComponent({
|
|||
workflow(): Workflow {
|
||||
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[] {
|
||||
if (this.activeNode) {
|
||||
return (
|
||||
|
@ -634,6 +646,9 @@ export default defineComponent({
|
|||
nodeTypeSelected(nodeTypeName: string) {
|
||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||
},
|
||||
async onSwitchSelectedNode(nodeTypeName: string) {
|
||||
this.$emit('switchSelectedNode', nodeTypeName);
|
||||
},
|
||||
async close() {
|
||||
if (this.isDragging) {
|
||||
return;
|
||||
|
@ -739,6 +754,10 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<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 {
|
||||
overflow: visible;
|
||||
margin-top: 0;
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
:disabled="disabled"
|
||||
:size="size"
|
||||
:circle="circle"
|
||||
:nodeTypeName="nodeType ? nodeType.displayName : ''"
|
||||
:nodeTypeName="nodeName ?? nodeType?.displayName ?? ''"
|
||||
:showTooltip="showTooltip"
|
||||
:tooltipPosition="tooltipPosition"
|
||||
:badge="badge"
|
||||
@click="(e) => $emit('click')"
|
||||
></n8n-node-icon>
|
||||
|
@ -53,6 +54,14 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tooltipPosition: {
|
||||
type: String,
|
||||
default: 'top',
|
||||
},
|
||||
nodeName: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore),
|
||||
|
|
|
@ -65,6 +65,10 @@
|
|||
$locale.baseText('ndv.output.waitingToRun')
|
||||
}}</n8n-text>
|
||||
<n8n-text v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
|
||||
<template v-if="isSubNode">
|
||||
{{ $locale.baseText('ndv.output.runNodeHintSubNode') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $locale.baseText('ndv.output.runNodeHint') }}
|
||||
<span @click="insertTestData" v-if="canPinData">
|
||||
<br />
|
||||
|
@ -73,6 +77,7 @@
|
|||
{{ $locale.baseText('ndv.output.insertTestData') }}
|
||||
</n8n-text>
|
||||
</span>
|
||||
</template>
|
||||
</n8n-text>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
<n8n-notice
|
||||
v-else-if="parameter.type === 'notice'"
|
||||
class="parameter-item"
|
||||
:class="['parameter-item', parameter.typeOptions?.containerClass ?? '']"
|
||||
:content="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||
@action="onNoticeAction"
|
||||
/>
|
||||
|
|
|
@ -1325,7 +1325,7 @@ export default defineComponent({
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (this.workflowRunData?.[this.node.name][runIndex].hasOwnProperty('error')) {
|
||||
if (this.workflowRunData?.[this.node.name]?.[runIndex]?.hasOwnProperty('error')) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -265,5 +265,6 @@ onMounted(() => {
|
|||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:indent="12"
|
||||
@node-click="onItemClick"
|
||||
:expand-on-click-node="false"
|
||||
data-test-id="lm-chat-logs-tree"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div
|
||||
|
@ -50,7 +51,11 @@
|
|||
}}
|
||||
</n8n-text>
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@ interface MemoryMessage {
|
|||
type: string;
|
||||
id: string[];
|
||||
kwargs: {
|
||||
content: string;
|
||||
content: unknown;
|
||||
additional_kwargs: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ const outputTypeParsers: {
|
|||
};
|
||||
},
|
||||
[NodeConnectionType.AiTool]: fallbackParser,
|
||||
[NodeConnectionType.AiAgent]: fallbackParser,
|
||||
[NodeConnectionType.AiMemory](execData: IDataObject) {
|
||||
const chatHistory =
|
||||
execData.chatHistory ?? execData.messages ?? execData?.response?.chat_history;
|
||||
|
@ -88,7 +89,23 @@ const outputTypeParsers: {
|
|||
const responseText = chatHistory
|
||||
.map((content: MemoryMessage) => {
|
||||
if (content.type === 'constructor' && content.id?.includes('schema') && content.kwargs) {
|
||||
interface MessageContent {
|
||||
type: string;
|
||||
image_url?: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
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) {
|
||||
message += ` (${JSON.stringify(content.kwargs.additional_kwargs)})`;
|
||||
}
|
||||
|
@ -120,7 +137,6 @@ const outputTypeParsers: {
|
|||
},
|
||||
[NodeConnectionType.AiOutputParser]: fallbackParser,
|
||||
[NodeConnectionType.AiRetriever]: fallbackParser,
|
||||
[NodeConnectionType.AiVectorRetriever]: fallbackParser,
|
||||
[NodeConnectionType.AiVectorStore](execData: IDataObject) {
|
||||
if (execData.documents) {
|
||||
return {
|
||||
|
@ -189,9 +205,17 @@ export const useAiContentParsers = () => {
|
|||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { within } from '@testing-library/dom';
|
||||
import { within } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
|
|
@ -64,17 +64,12 @@
|
|||
</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">{{
|
||||
$locale.baseText('chat.window.logs')
|
||||
}}</n8n-text>
|
||||
<div class="logs">
|
||||
<run-data-ai v-if="node" :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>
|
||||
<run-data-ai :node="node" hide-title slim :key="messages.length" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -87,6 +82,7 @@
|
|||
type="textarea"
|
||||
ref="inputField"
|
||||
:placeholder="$locale.baseText('chat.window.chat.placeholder')"
|
||||
data-test-id="workflow-chat-input"
|
||||
@keydown.stop="updated"
|
||||
/>
|
||||
<n8n-button
|
||||
|
@ -97,7 +93,7 @@
|
|||
size="large"
|
||||
icon="comment"
|
||||
type="primary"
|
||||
data-test-id="workflow-chat-button"
|
||||
data-test-id="workflow-chat-send-button"
|
||||
/>
|
||||
|
||||
<n8n-info-tip class="mt-s">
|
||||
|
@ -290,8 +286,10 @@ export default defineComponent({
|
|||
|
||||
if (!chatNode) {
|
||||
this.showError(
|
||||
new Error('Chat viable node(Agent or Chain) could not be found!'),
|
||||
'Chat node not found',
|
||||
new Error(
|
||||
'Chat only works when an AI agent or chain is connected to the chat trigger node',
|
||||
),
|
||||
'Missing AI node',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -395,6 +393,8 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
const inputKey = triggerNode.typeVersion < 1.1 ? 'input' : 'chat_input';
|
||||
|
||||
const nodeData: ITaskData = {
|
||||
startTime: new Date().getTime(),
|
||||
executionTime: 0,
|
||||
|
@ -404,7 +404,7 @@ export default defineComponent({
|
|||
[
|
||||
{
|
||||
json: {
|
||||
input: message,
|
||||
[inputKey]: message,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -504,9 +504,11 @@ export default defineComponent({
|
|||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
z-index: 9999;
|
||||
|
||||
.logs-wrapper {
|
||||
border: 1px solid #e0e0e0;
|
||||
--node-icon-color: var(--color-text-base);
|
||||
border: 1px solid var(--color-foreground-base);
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
@ -518,8 +520,8 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
.messages {
|
||||
background-color: var(--color-background-base);
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: var(--color-lm-chat-messages-background);
|
||||
border: 1px solid var(--color-foreground-base);
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -533,16 +535,17 @@ export default defineComponent({
|
|||
width: 100%;
|
||||
|
||||
.content {
|
||||
border-radius: 10px;
|
||||
border-radius: var(--border-radius-large);
|
||||
line-height: 1.5;
|
||||
margin: 0.5em 1em;
|
||||
margin: var(--spacing-2xs) var(--spacing-s);
|
||||
max-width: 75%;
|
||||
padding: 1em;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
|
||||
&.bot {
|
||||
background-color: #e0d0d0;
|
||||
background-color: var(--color-lm-chat-bot-background);
|
||||
border: 1px solid var(--color-lm-chat-bot-border);
|
||||
float: left;
|
||||
|
||||
.message-options {
|
||||
|
@ -551,7 +554,8 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
&.user {
|
||||
background-color: #d0e0d0;
|
||||
background-color: var(--color-lm-chat-user-background);
|
||||
border: 1px solid var(--color-lm-chat-user-border);
|
||||
float: 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;
|
||||
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) {
|
||||
const clickedElement = e.target;
|
||||
if (!(clickedElement instanceof Element) || clickedElement.tagName !== 'A') return;
|
||||
|
@ -24,7 +36,9 @@ export default () => {
|
|||
const actionAttribute = clickedElement.getAttribute('data-action');
|
||||
if (actionAttribute && typeof availableActions.value[actionAttribute] === 'function') {
|
||||
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 MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
||||
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 N8N_NODE_TYPE = 'n8n-nodes-base.n8n';
|
||||
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
||||
|
@ -194,6 +195,7 @@ export const NODE_CREATOR_OPEN_SOURCES: Record<
|
|||
TAB: 'tab',
|
||||
NODE_CONNECTION_ACTION: 'node_connection_action',
|
||||
NODE_CONNECTION_DROP: 'node_connection_drop',
|
||||
NOTICE_ERROR_MESSAGE: 'notice_error_message',
|
||||
CONTEXT_MENU: 'context_menu',
|
||||
'': '',
|
||||
};
|
||||
|
|
|
@ -176,6 +176,8 @@ export const nodeHelpers = defineComponent({
|
|||
}
|
||||
|
||||
for (const taskData of workflowResultData[node.name]) {
|
||||
if (!taskData) return false;
|
||||
|
||||
if (taskData.error !== undefined) {
|
||||
return true;
|
||||
}
|
||||
|
@ -313,11 +315,24 @@ export const nodeHelpers = defineComponent({
|
|||
const parentNodes = workflow.getParentNodes(node.name, input.type, 1);
|
||||
|
||||
if (parentNodes.length === 0) {
|
||||
// We want to show different error for missing AI subnodes
|
||||
if (input.type.startsWith('ai_')) {
|
||||
foundIssues[input.type] = [
|
||||
this.$locale.baseText('nodeIssues.input.missing', {
|
||||
interpolate: { inputName: input.displayName || 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 { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { NodeConnectionType, NodeHelpers, jsonParse, jsonStringify } from 'n8n-workflow';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { jsonParse, jsonStringify } from 'n8n-workflow';
|
||||
|
||||
export type PinDataSource =
|
||||
| 'pin-icon-click'
|
||||
|
@ -39,9 +39,19 @@ export const pinData = defineComponent({
|
|||
hasPinData(): boolean {
|
||||
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 {
|
||||
return (
|
||||
!!this.node &&
|
||||
!this.isSubNode &&
|
||||
!this.isMultipleOutputsNodeType &&
|
||||
!PIN_DATA_NODE_TYPES_DENYLIST.includes(this.node.type)
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ import type {
|
|||
IWorkflowBase,
|
||||
SubworkflowOperationError,
|
||||
IExecuteContextData,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
import { TelemetryHelpers } from 'n8n-workflow';
|
||||
|
||||
|
@ -396,13 +397,46 @@ export const pushConnection = defineComponent({
|
|||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
} 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;
|
||||
const nodeError = runDataExecuted.data.resultData.error as NodeOperationError;
|
||||
if (nodeError.node.name) {
|
||||
title = `Error in sub-node ‘${nodeError.node.name}‘`;
|
||||
} else {
|
||||
title = 'Problem executing workflow';
|
||||
}
|
||||
|
||||
this.showMessage({
|
||||
title,
|
||||
message:
|
||||
(nodeError?.description ?? runDataExecutedErrorMessage) +
|
||||
this.$locale.baseText('pushConnection.executionError.openNode', {
|
||||
interpolate: {
|
||||
node: nodeError.node.name,
|
||||
},
|
||||
}),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
} else {
|
||||
let title: string;
|
||||
let type = 'error';
|
||||
if (runDataExecuted.status === 'canceled') {
|
||||
title = this.$locale.baseText('nodeView.showMessage.stopExecutionTry.title');
|
||||
type = 'warning';
|
||||
} else if (runDataExecuted.data.resultData.lastNodeExecuted) {
|
||||
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';
|
||||
|
@ -411,11 +445,12 @@ export const pushConnection = defineComponent({
|
|||
this.showMessage({
|
||||
title,
|
||||
message: runDataExecutedErrorMessage,
|
||||
type,
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Workflow did execute without a problem
|
||||
this.titleSet(workflow.name as string, 'IDLE');
|
||||
|
|
|
@ -4,15 +4,12 @@ import type {
|
|||
IConnections,
|
||||
IRunExecutionData,
|
||||
IExecuteData,
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypes,
|
||||
IVersionedNodeType,
|
||||
} from 'n8n-workflow';
|
||||
import { Workflow, WorkflowDataProxy, NodeHelpers } from 'n8n-workflow';
|
||||
import { WorkflowDataProxy } from 'n8n-workflow';
|
||||
import { createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
|
||||
class NodeTypesClass implements INodeTypes {
|
||||
nodeTypes: INodeTypeData = {
|
||||
const nodeTypes: INodeTypeData = {
|
||||
'test.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
|
@ -45,16 +42,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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[] = [
|
||||
{
|
||||
|
@ -273,13 +261,13 @@ const runExecutionData: IRunExecutionData = {
|
|||
},
|
||||
};
|
||||
|
||||
const workflow = new Workflow({
|
||||
const workflow = createTestWorkflowObject({
|
||||
id: '123',
|
||||
name: 'test workflow',
|
||||
nodes,
|
||||
connections,
|
||||
active: false,
|
||||
nodeTypes: new NodeTypesClass(),
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const lastNodeName = 'End';
|
||||
|
|
|
@ -791,6 +791,7 @@
|
|||
"ndv.output.pageSize": "Page Size",
|
||||
"ndv.output.run": "Run",
|
||||
"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.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.",
|
||||
|
@ -948,6 +949,7 @@
|
|||
"nodeErrorView.dataBelowMayContain": "Data below may contain sensitive information. Proceed with caution when sharing.",
|
||||
"nodeErrorView.details": "Details",
|
||||
"nodeErrorView.error": "ERROR",
|
||||
"nodeErrorView.errorSubNode": "Error in sub-node ‘{node}’",
|
||||
"nodeErrorView.httpCode": "HTTP Code",
|
||||
"nodeErrorView.inParameter": "In or underneath Parameter",
|
||||
"nodeErrorView.itemIndex": "Item Index",
|
||||
|
@ -1240,6 +1242,7 @@
|
|||
"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.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>",
|
||||
"resourceLocator.id.placeholder": "Enter ID...",
|
||||
"resourceLocator.mode.id": "By ID",
|
||||
|
@ -1762,6 +1765,7 @@
|
|||
"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.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.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.",
|
||||
|
|
|
@ -16,7 +16,7 @@ export const register = () => {
|
|||
const sizeDifference = (unconnectedPlusSize - unconnectedDiamondWidth) / 2;
|
||||
|
||||
const container = svg.node('g', {
|
||||
style: `--svg-color: var(${endpointInstance.params.color})`,
|
||||
style: `--svg-color: var(--endpoint-svg-color, var(${endpointInstance.params.color}))`,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
|
|
@ -46,6 +46,14 @@ export class N8nAddInputEndpoint extends EndpointRepresentation<ComputedN8nAddIn
|
|||
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) => {
|
||||
if (endpoint === this.endpoint) {
|
||||
this.instance.fire(EVENT_ADD_INPUT_ENDPOINT_CLICK, this.endpoint);
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
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 {
|
||||
applyScale,
|
||||
|
@ -37,12 +41,6 @@ import {
|
|||
} from '@/utils/nodeViewUtils';
|
||||
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', () => {
|
||||
const workflowStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
|
|
@ -157,6 +157,26 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
|||
}
|
||||
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;
|
||||
|
|
|
@ -663,7 +663,7 @@ export const getOutputSummary = (
|
|||
} = {};
|
||||
|
||||
data.forEach((run: ITaskData) => {
|
||||
if (!run.data?.[connectionType]) {
|
||||
if (!run?.data?.[connectionType]) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
:renaming="renamingActive"
|
||||
:isProductionExecutionPreview="isProductionExecutionPreview"
|
||||
@redrawNode="redrawNode"
|
||||
@switchSelectedNode="onSwitchSelectedNode"
|
||||
@valueChanged="valueChanged"
|
||||
@stopExecution="stopExecution"
|
||||
@saveKeyboardShortcut="onSaveKeyboardShortcut"
|
||||
|
@ -356,7 +357,11 @@ import {
|
|||
N8nPlusEndpointType,
|
||||
EVENT_PLUS_ENDPOINT_CLICK,
|
||||
} 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 { getConnectorPaintStyleData, OVERLAY_ENDPOINT_ARROW_ID } from '@/utils/nodeViewUtils';
|
||||
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
|
||||
|
@ -788,6 +793,42 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
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() {
|
||||
if (this.containsTrigger && !this.allTriggersDisabled) return;
|
||||
|
@ -2398,36 +2439,21 @@ export default defineComponent({
|
|||
}
|
||||
this.historyStore.stopRecordingUndo();
|
||||
},
|
||||
insertNodeAfterSelected(info: {
|
||||
sourceId: string;
|
||||
index: number;
|
||||
eventSource: NodeCreatorOpenSource;
|
||||
connection?: Connection;
|
||||
nodeCreatorView?: string;
|
||||
outputType?: NodeConnectionType;
|
||||
endpointUuid?: string;
|
||||
}) {
|
||||
const type = info.outputType || NodeConnectionType.Main;
|
||||
|
||||
getNodeCreatorFilter(nodeName: string, outputType?: NodeConnectionType) {
|
||||
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 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) {
|
||||
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) => {
|
||||
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
|
||||
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.lastSelectedNodeEndpointUuid =
|
||||
info.endpointUuid ?? info.connection?.target.jtk?.endpoint.uuid;
|
||||
|
@ -2464,7 +2510,11 @@ export default defineComponent({
|
|||
|
||||
if (isScopedConnection) {
|
||||
useViewStacks()
|
||||
.gotoCompatibleConnectionView(type, isOutput, filter)
|
||||
.gotoCompatibleConnectionView(
|
||||
type,
|
||||
isOutput,
|
||||
this.getNodeCreatorFilter(sourceNode.name, type),
|
||||
)
|
||||
.catch((e) => {});
|
||||
}
|
||||
},
|
||||
|
@ -2688,7 +2738,8 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
this.dropPrevented = false;
|
||||
void this.updateNodesInputIssues();
|
||||
this.updateNodesInputIssues();
|
||||
this.resetEndpointsErrors();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -3616,6 +3667,9 @@ export default defineComponent({
|
|||
}, recordingTimeout);
|
||||
}
|
||||
},
|
||||
async onSwitchSelectedNode(nodeName: string) {
|
||||
this.nodeSelectedByName(nodeName, true, true);
|
||||
},
|
||||
async redrawNode(nodeName: string) {
|
||||
// TODO: Improve later
|
||||
// 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);
|
||||
|
||||
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();
|
||||
},
|
||||
activated() {
|
||||
|
@ -4684,6 +4769,8 @@ export default defineComponent({
|
|||
document.removeEventListener('keydown', this.keyDown);
|
||||
document.removeEventListener('keyup', this.keyUp);
|
||||
this.unregisterCustomAction('showNodeCreator');
|
||||
this.unregisterCustomAction('openNodeDetail');
|
||||
this.unregisterCustomAction('openSelectiveNodeCreator');
|
||||
|
||||
if (!this.isDemo) {
|
||||
this.pushStore.pushDisconnect();
|
||||
|
|
|
@ -22,10 +22,11 @@ export class OpenAiApi implements ICredentialType {
|
|||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
displayName: 'Organization ID (optional)',
|
||||
name: 'organizationId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
hint: 'Only required if you belong to multiple organisations',
|
||||
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.",
|
||||
},
|
||||
|
|
|
@ -25,6 +25,9 @@ export class SupabaseApi implements ICredentialType {
|
|||
name: 'serviceRole',
|
||||
type: 'string',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1098,6 +1098,7 @@ export interface ILoadOptions {
|
|||
|
||||
export interface INodePropertyTypeOptions {
|
||||
action?: string; // Supported by: button
|
||||
containerClass?: string; // Supported by: notice
|
||||
alwaysOpenEditWindow?: boolean; // Supported by: json
|
||||
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
|
||||
editor?: EditorType; // Supported by: string
|
||||
|
@ -1526,6 +1527,7 @@ export interface IPostReceiveSort extends IPostReceiveBase {
|
|||
}
|
||||
|
||||
export type ConnectionTypes =
|
||||
| 'ai_agent'
|
||||
| 'ai_chain'
|
||||
| 'ai_document'
|
||||
| 'ai_embedding'
|
||||
|
@ -1540,6 +1542,8 @@ export type ConnectionTypes =
|
|||
| 'main';
|
||||
|
||||
export const enum NodeConnectionType {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
AiAgent = 'ai_agent',
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
AiChain = 'ai_chain',
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
|
@ -2376,3 +2380,4 @@ export type BannerName =
|
|||
| 'EMAIL_CONFIRMATION';
|
||||
|
||||
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';
|
||||
|
||||
interface ExecutionBaseErrorOptions {
|
||||
|
@ -21,6 +21,8 @@ export abstract class ExecutionBaseError extends ApplicationError {
|
|||
|
||||
severity: Severity = 'error';
|
||||
|
||||
functionality: Functionality = 'regular';
|
||||
|
||||
constructor(message: string, { cause }: ExecutionBaseErrorOptions) {
|
||||
const options = cause instanceof Error ? { cause } : {};
|
||||
super(message, options);
|
||||
|
|
|
@ -4,7 +4,14 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
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 { removeCircularRefs } from '../utils';
|
||||
|
||||
|
@ -15,6 +22,7 @@ export interface NodeOperationErrorOptions {
|
|||
itemIndex?: number;
|
||||
severity?: Severity;
|
||||
messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node
|
||||
functionality?: Functionality;
|
||||
}
|
||||
|
||||
interface NodeApiErrorOptions extends NodeOperationErrorOptions {
|
||||
|
@ -113,6 +121,7 @@ export class NodeApiError extends NodeError {
|
|||
runIndex,
|
||||
itemIndex,
|
||||
severity,
|
||||
functionality,
|
||||
messageMapping,
|
||||
}: NodeApiErrorOptions = {},
|
||||
) {
|
||||
|
@ -206,6 +215,7 @@ export class NodeApiError extends NodeError {
|
|||
messageMapping,
|
||||
);
|
||||
|
||||
if (functionality !== undefined) this.context.functionality = functionality;
|
||||
if (runIndex !== undefined) this.context.runIndex = runIndex;
|
||||
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.severity) this.severity = options.severity;
|
||||
if (options.functionality) this.functionality = options.functionality;
|
||||
this.description = options.description;
|
||||
this.context.runIndex = options.runIndex;
|
||||
this.context.itemIndex = options.itemIndex;
|
||||
|
|
|
@ -965,6 +965,9 @@ importers:
|
|||
'@sentry/vite-plugin':
|
||||
specifier: ^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':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.1
|
||||
|
@ -9722,7 +9725,7 @@ packages:
|
|||
lodash.uniqby: 4.7.0
|
||||
node-fetch: 2.6.8
|
||||
parse-github-url: 1.0.2
|
||||
regenerator-runtime: 0.13.9
|
||||
regenerator-runtime: 0.13.11
|
||||
semver: 7.5.4
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
@ -19248,10 +19251,6 @@ packages:
|
|||
/regenerator-runtime@0.13.11:
|
||||
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:
|
||||
resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==}
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue