From 76dded4bea9d26ad84fdbde74d577d244eb4e223 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Mon, 6 Jan 2025 09:07:59 +0100 Subject: [PATCH 01/15] feat: Allow using Vector Stores directly as Tools (#12311) Co-authored-by: Oleg Ivaniv --- cypress/composables/ndv.ts | 2 +- cypress/composables/workflow.ts | 21 ++- cypress/e2e/4-node-creator.cy.ts | 14 +- .../ToolVectorStore/ToolVectorStore.node.ts | 19 ++- .../VectorStorePGVector.node.ts | 2 +- .../VectorStorePinecone.node.ts | 2 +- .../VectorStoreSupabase.node.ts | 2 +- .../shared/createVectorStoreNode.test.ts | 161 ++++++++++++++++++ .../shared/createVectorStoreNode.ts | 142 +++++++++++++-- packages/editor-ui/src/Interface.ts | 1 + .../Node/NodeCreator/Modes/NodesMode.vue | 33 +++- .../composables/useActionsGeneration.ts | 18 +- .../NodeCreator/composables/useViewStacks.ts | 24 ++- .../src/components/Node/NodeCreator/utils.ts | 2 + packages/editor-ui/src/constants.ts | 1 + packages/workflow/src/Interfaces.ts | 1 + 16 files changed, 402 insertions(+), 43 deletions(-) create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.test.ts diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index 4819e7fcccf91..90146ab374f4e 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -2,7 +2,7 @@ * Getters */ -import { getVisibleSelect } from '../utils'; +import { getVisibleSelect } from '../utils/popper'; export function getCredentialSelect(eq = 0) { return cy.getByTestId('node-credentials-select').eq(eq); diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index bc270482192dd..29b871f560a40 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -1,4 +1,5 @@ import { getManualChatModal } from './modals/chat-modal'; +import { clickGetBackToCanvas, getParameterInputByName } from './ndv'; import { ROUTES } from '../constants'; /** @@ -127,7 +128,7 @@ export function navigateToNewWorkflowPage(preventNodeViewUnload = true) { }); } -export function addSupplementalNodeToParent( +function connectNodeToParent( nodeName: string, endpointType: EndpointType, parentNodeName: string, @@ -141,6 +142,15 @@ export function addSupplementalNodeToParent( } else { getNodeCreatorItems().contains(nodeName).click(); } +} + +export function addSupplementalNodeToParent( + nodeName: string, + endpointType: EndpointType, + parentNodeName: string, + exactMatch = false, +) { + connectNodeToParent(nodeName, endpointType, parentNodeName, exactMatch); getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist'); } @@ -160,6 +170,15 @@ export function addToolNodeToParent(nodeName: string, parentNodeName: string) { addSupplementalNodeToParent(nodeName, 'ai_tool', parentNodeName); } +export function addVectorStoreToolToParent(nodeName: string, parentNodeName: string) { + connectNodeToParent(nodeName, 'ai_tool', parentNodeName, false); + getParameterInputByName('mode') + .find('input') + .should('have.value', 'Retrieve Documents (As Tool for AI Agent)'); + clickGetBackToCanvas(); + getConnectionBySourceAndTarget(nodeName, parentNodeName).should('exist'); +} + export function addOutputParserNodeToParent(nodeName: string, parentNodeName: string) { addSupplementalNodeToParent(nodeName, 'ai_outputParser', parentNodeName); } diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index a2cd5968d182e..e841605863044 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -1,10 +1,12 @@ +import { clickGetBackToCanvas } from '../composables/ndv'; import { addNodeToCanvas, addRetrieverNodeToParent, addVectorStoreNodeToParent, + addVectorStoreToolToParent, getNodeCreatorItems, } from '../composables/workflow'; -import { IF_NODE_NAME } from '../constants'; +import { AGENT_NODE_NAME, IF_NODE_NAME, MANUAL_CHAT_TRIGGER_NODE_NAME } from '../constants'; import { NodeCreator } from '../pages/features/node-creator'; import { NDV } from '../pages/ndv'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; @@ -536,7 +538,7 @@ describe('Node Creator', () => { }); }); - it('should add node directly for sub-connection', () => { + it('should add node directly for sub-connection as vector store', () => { addNodeToCanvas('Question and Answer Chain', true); addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain'); cy.realPress('Escape'); @@ -544,4 +546,12 @@ describe('Node Creator', () => { cy.realPress('Escape'); WorkflowPage.getters.canvasNodes().should('have.length', 4); }); + + it('should add node directly for sub-connection as tool', () => { + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(AGENT_NODE_NAME, true, true); + clickGetBackToCanvas(); + + addVectorStoreToolToParent('In-Memory Vector Store', AGENT_NODE_NAME); + }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts index aaa2ca37d96aa..97113643f3fab 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts @@ -15,15 +15,15 @@ import { getConnectionHintNoticeField } from '@utils/sharedFields'; export class ToolVectorStore implements INodeType { description: INodeTypeDescription = { - displayName: 'Vector Store Tool', + displayName: 'Vector Store Question Answer Tool', name: 'toolVectorStore', icon: 'fa:database', iconColor: 'black', group: ['transform'], version: [1], - description: 'Retrieve context from vector store', + description: 'Answer questions with a vector store', defaults: { - name: 'Vector Store Tool', + name: 'Answer questions with a vector store', }, codex: { categories: ['AI'], @@ -60,20 +60,23 @@ export class ToolVectorStore implements INodeType { properties: [ getConnectionHintNoticeField([NodeConnectionType.AiAgent]), { - displayName: 'Name', + displayName: 'Data Name', name: 'name', type: 'string', default: '', - placeholder: 'e.g. company_knowledge_base', + placeholder: 'e.g. users_info', validateType: 'string-alphanumeric', - description: 'Name of the vector store', + description: + 'Name of the data in vector store. This will be used to fill this tool description: Useful for when you need to answer questions about [name]. Whenever you need information about [data description], you should ALWAYS use this. Input should be a fully formed question.', }, { - displayName: 'Description', + displayName: 'Description of Data', name: 'description', type: 'string', default: '', - placeholder: 'Retrieves data about [insert information about your data here]...', + placeholder: "[Describe your data here, e.g. a user's name, email, etc.]", + description: + 'Describe the data in vector store. This will be used to fill this tool description: Useful for when you need to answer questions about [name]. Whenever you need information about [data description], you should ALWAYS use this. Input should be a fully formed question.', typeOptions: { rows: 3, }, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts index 6d5da1615b036..d9d5ee611a499 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts @@ -228,7 +228,7 @@ export class VectorStorePGVector extends createVectorStoreNode({ testedBy: 'postgresConnectionTest', }, ], - operationModes: ['load', 'insert', 'retrieve'], + operationModes: ['load', 'insert', 'retrieve', 'retrieve-as-tool'], }, sharedFields, insertFields, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts index 711425df55edf..5a11acea24e44 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts @@ -65,7 +65,7 @@ export class VectorStorePinecone extends createVectorStoreNode({ required: true, }, ], - operationModes: ['load', 'insert', 'retrieve', 'update'], + operationModes: ['load', 'insert', 'retrieve', 'update', 'retrieve-as-tool'], }, methods: { listSearch: { pineconeIndexSearch } }, retrieveFields, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts index b1b80fea5a153..a462ff8cf6a97 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.ts @@ -55,7 +55,7 @@ export class VectorStoreSupabase extends createVectorStoreNode({ required: true, }, ], - operationModes: ['load', 'insert', 'retrieve', 'update'], + operationModes: ['load', 'insert', 'retrieve', 'update', 'retrieve-as-tool'], }, methods: { listSearch: { supabaseTableNameSearch }, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.test.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.test.ts new file mode 100644 index 0000000000000..26036dce80cb0 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.test.ts @@ -0,0 +1,161 @@ +import type { DocumentInterface } from '@langchain/core/documents'; +import type { Embeddings } from '@langchain/core/embeddings'; +import type { VectorStore } from '@langchain/core/vectorstores'; +import { mock } from 'jest-mock-extended'; +import type { DynamicTool } from 'langchain/tools'; +import type { ISupplyDataFunctions, NodeParameterValueType } from 'n8n-workflow'; + +import type { VectorStoreNodeConstructorArgs } from './createVectorStoreNode'; +import { createVectorStoreNode } from './createVectorStoreNode'; + +jest.mock('@utils/logWrapper', () => ({ + logWrapper: jest.fn().mockImplementation((val: DynamicTool) => ({ logWrapped: val })), +})); + +const DEFAULT_PARAMETERS = { + options: {}, + topK: 1, +}; + +const MOCK_DOCUMENTS: Array<[DocumentInterface, number]> = [ + [ + { + pageContent: 'first page', + metadata: { + id: 123, + }, + }, + 0, + ], + [ + { + pageContent: 'second page', + metadata: { + id: 567, + }, + }, + 0, + ], +]; + +const MOCK_SEARCH_VALUE = 'search value'; +const MOCK_EMBEDDED_SEARCH_VALUE = [1, 2, 3]; + +describe('createVectorStoreNode', () => { + const vectorStore = mock({ + similaritySearchVectorWithScore: jest.fn().mockResolvedValue(MOCK_DOCUMENTS), + }); + + const vectorStoreNodeArgs = mock({ + sharedFields: [], + insertFields: [], + loadFields: [], + retrieveFields: [], + updateFields: [], + getVectorStoreClient: jest.fn().mockReturnValue(vectorStore), + }); + + const embeddings = mock({ + embedQuery: jest.fn().mockResolvedValue(MOCK_EMBEDDED_SEARCH_VALUE), + }); + + const context = mock({ + getNodeParameter: jest.fn(), + getInputConnectionData: jest.fn().mockReturnValue(embeddings), + }); + + describe('retrieve mode', () => { + it('supplies vector store as data', async () => { + // ARRANGE + const parameters: Record = { + ...DEFAULT_PARAMETERS, + mode: 'retrieve', + }; + context.getNodeParameter.mockImplementation( + (parameterName: string): NodeParameterValueType | object => parameters[parameterName], + ); + + // ACT + const VectorStoreNodeType = createVectorStoreNode(vectorStoreNodeArgs); + const nodeType = new VectorStoreNodeType(); + const data = await nodeType.supplyData.call(context, 1); + const wrappedVectorStore = (data.response as { logWrapped: VectorStore }).logWrapped; + + // ASSERT + expect(wrappedVectorStore).toEqual(vectorStore); + expect(vectorStoreNodeArgs.getVectorStoreClient).toHaveBeenCalled(); + }); + }); + + describe('retrieve-as-tool mode', () => { + it('supplies DynamicTool that queries vector store and returns documents with metadata', async () => { + // ARRANGE + const parameters: Record = { + ...DEFAULT_PARAMETERS, + mode: 'retrieve-as-tool', + description: 'tool description', + toolName: 'tool name', + includeDocumentMetadata: true, + }; + context.getNodeParameter.mockImplementation( + (parameterName: string): NodeParameterValueType | object => parameters[parameterName], + ); + + // ACT + const VectorStoreNodeType = createVectorStoreNode(vectorStoreNodeArgs); + const nodeType = new VectorStoreNodeType(); + const data = await nodeType.supplyData.call(context, 1); + const tool = (data.response as { logWrapped: DynamicTool }).logWrapped; + const output = await tool?.func(MOCK_SEARCH_VALUE); + + // ASSERT + expect(tool?.getName()).toEqual(parameters.toolName); + expect(tool?.description).toEqual(parameters.toolDescription); + expect(embeddings.embedQuery).toHaveBeenCalledWith(MOCK_SEARCH_VALUE); + expect(vectorStore.similaritySearchVectorWithScore).toHaveBeenCalledWith( + MOCK_EMBEDDED_SEARCH_VALUE, + parameters.topK, + parameters.filter, + ); + expect(output).toEqual([ + { type: 'text', text: JSON.stringify(MOCK_DOCUMENTS[0][0]) }, + { type: 'text', text: JSON.stringify(MOCK_DOCUMENTS[1][0]) }, + ]); + }); + + it('supplies DynamicTool that queries vector store and returns documents without metadata', async () => { + // ARRANGE + const parameters: Record = { + ...DEFAULT_PARAMETERS, + mode: 'retrieve-as-tool', + description: 'tool description', + toolName: 'tool name', + includeDocumentMetadata: false, + }; + context.getNodeParameter.mockImplementation( + (parameterName: string): NodeParameterValueType | object => parameters[parameterName], + ); + + // ACT + const VectorStoreNodeType = createVectorStoreNode(vectorStoreNodeArgs); + const nodeType = new VectorStoreNodeType(); + const data = await nodeType.supplyData.call(context, 1); + const tool = (data.response as { logWrapped: DynamicTool }).logWrapped; + const output = await tool?.func(MOCK_SEARCH_VALUE); + + // ASSERT + expect(tool?.getName()).toEqual(parameters.toolName); + expect(tool?.description).toEqual(parameters.toolDescription); + expect(embeddings.embedQuery).toHaveBeenCalledWith(MOCK_SEARCH_VALUE); + expect(vectorStore.similaritySearchVectorWithScore).toHaveBeenCalledWith( + MOCK_EMBEDDED_SEARCH_VALUE, + parameters.topK, + parameters.filter, + ); + expect(output).toEqual([ + { type: 'text', text: JSON.stringify({ pageContent: MOCK_DOCUMENTS[0][0].pageContent }) }, + { type: 'text', text: JSON.stringify({ pageContent: MOCK_DOCUMENTS[1][0].pageContent }) }, + ]); + }); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts index 84f1d550e5032..b7c0de392256a 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts @@ -3,6 +3,7 @@ import type { Document } from '@langchain/core/documents'; import type { Embeddings } from '@langchain/core/embeddings'; import type { VectorStore } from '@langchain/core/vectorstores'; +import { DynamicTool } from 'langchain/tools'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { IExecuteFunctions, @@ -28,9 +29,14 @@ import { getConnectionHintNoticeField } from '@utils/sharedFields'; import { processDocument } from './processDocuments'; -type NodeOperationMode = 'insert' | 'load' | 'retrieve' | 'update'; +type NodeOperationMode = 'insert' | 'load' | 'retrieve' | 'update' | 'retrieve-as-tool'; -const DEFAULT_OPERATION_MODES: NodeOperationMode[] = ['load', 'insert', 'retrieve']; +const DEFAULT_OPERATION_MODES: NodeOperationMode[] = [ + 'load', + 'insert', + 'retrieve', + 'retrieve-as-tool', +]; interface NodeMeta { displayName: string; @@ -43,7 +49,7 @@ interface NodeMeta { operationModes?: NodeOperationMode[]; } -interface VectorStoreNodeConstructorArgs { +export interface VectorStoreNodeConstructorArgs { meta: NodeMeta; methods?: { listSearch?: { @@ -102,10 +108,18 @@ function getOperationModeOptions(args: VectorStoreNodeConstructorArgs): INodePro action: 'Add documents to vector store', }, { - name: 'Retrieve Documents (For Agent/Chain)', + name: 'Retrieve Documents (As Vector Store for AI Agent)', value: 'retrieve', - description: 'Retrieve documents from vector store to be used with AI nodes', - action: 'Retrieve documents for AI processing', + description: 'Retrieve documents from vector store to be used as vector store with AI nodes', + action: 'Retrieve documents for AI processing as Vector Store', + outputConnectionType: NodeConnectionType.AiVectorStore, + }, + { + name: 'Retrieve Documents (As Tool for AI Agent)', + value: 'retrieve-as-tool', + description: 'Retrieve documents from vector store to be used as tool with AI nodes', + action: 'Retrieve documents for AI processing as Tool', + outputConnectionType: NodeConnectionType.AiTool, }, { name: 'Update Documents', @@ -136,7 +150,8 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => codex: { categories: ['AI'], subcategories: { - AI: ['Vector Stores', 'Root Nodes'], + AI: ['Vector Stores', 'Tools', 'Root Nodes'], + Tools: ['Other Tools'], }, resources: { primaryDocumentation: [ @@ -153,6 +168,10 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => const mode = parameters?.mode; const inputs = [{ displayName: "Embedding", type: "${NodeConnectionType.AiEmbedding}", required: true, maxConnections: 1}] + if (mode === 'retrieve-as-tool') { + return inputs; + } + if (['insert', 'load', 'update'].includes(mode)) { inputs.push({ displayName: "", type: "${NodeConnectionType.Main}"}) } @@ -166,6 +185,11 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => outputs: `={{ ((parameters) => { const mode = parameters?.mode ?? 'retrieve'; + + if (mode === 'retrieve-as-tool') { + return [{ displayName: "Tool", type: "${NodeConnectionType.AiTool}"}] + } + if (mode === 'retrieve') { return [{ displayName: "Vector Store", type: "${NodeConnectionType.AiVectorStore}"}] } @@ -189,6 +213,37 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => }, }, }, + { + displayName: 'Name', + name: 'toolName', + type: 'string', + default: '', + required: true, + description: 'Name of the vector store', + placeholder: 'e.g. company_knowledge_base', + validateType: 'string-alphanumeric', + displayOptions: { + show: { + mode: ['retrieve-as-tool'], + }, + }, + }, + { + displayName: 'Description', + name: 'toolDescription', + type: 'string', + default: '', + required: true, + typeOptions: { rows: 2 }, + description: + 'Explain to the LLM what this tool does, a good, specific description would allow LLMs to produce expected results much more often', + placeholder: `e.g. ${args.meta.description}`, + displayOptions: { + show: { + mode: ['retrieve-as-tool'], + }, + }, + }, ...args.sharedFields, ...transformDescriptionForOperationMode(args.insertFields ?? [], 'insert'), // Prompt and topK are always used for the load operation @@ -214,7 +269,19 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => description: 'Number of top results to fetch from vector store', displayOptions: { show: { - mode: ['load'], + mode: ['load', 'retrieve-as-tool'], + }, + }, + }, + { + displayName: 'Include Metadata', + name: 'includeDocumentMetadata', + type: 'boolean', + default: true, + description: 'Whether or not to include document metadata', + displayOptions: { + show: { + mode: ['load', 'retrieve-as-tool'], }, }, }, @@ -271,10 +338,16 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => filter, ); + const includeDocumentMetadata = this.getNodeParameter( + 'includeDocumentMetadata', + itemIndex, + true, + ) as boolean; + const serializedDocs = docs.map(([doc, score]) => { const document = { - metadata: doc.metadata, pageContent: doc.pageContent, + ...(includeDocumentMetadata ? { metadata: doc.metadata } : {}), }; return { @@ -381,12 +454,12 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => throw new NodeOperationError( this.getNode(), - 'Only the "load" and "insert" operation modes are supported with execute', + 'Only the "load", "update" and "insert" operation modes are supported with execute', ); } async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { - const mode = this.getNodeParameter('mode', 0) as 'load' | 'insert' | 'retrieve'; + const mode = this.getNodeParameter('mode', 0) as NodeOperationMode; const filter = getMetadataFiltersValues(this, itemIndex); const embeddings = (await this.getInputConnectionData( NodeConnectionType.AiEmbedding, @@ -400,9 +473,54 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => }; } + if (mode === 'retrieve-as-tool') { + const toolDescription = this.getNodeParameter('toolDescription', itemIndex) as string; + const toolName = this.getNodeParameter('toolName', itemIndex) as string; + const topK = this.getNodeParameter('topK', itemIndex, 4) as number; + const includeDocumentMetadata = this.getNodeParameter( + 'includeDocumentMetadata', + itemIndex, + true, + ) as boolean; + + const vectorStoreTool = new DynamicTool({ + name: toolName, + description: toolDescription, + func: async (input) => { + const vectorStore = await args.getVectorStoreClient( + this, + filter, + embeddings, + itemIndex, + ); + const embeddedPrompt = await embeddings.embedQuery(input); + const documents = await vectorStore.similaritySearchVectorWithScore( + embeddedPrompt, + topK, + filter, + ); + return documents + .map((document) => { + if (includeDocumentMetadata) { + return { type: 'text', text: JSON.stringify(document[0]) }; + } + return { + type: 'text', + text: JSON.stringify({ pageContent: document[0].pageContent }), + }; + }) + .filter((document) => !!document); + }, + }); + + return { + response: logWrapper(vectorStoreTool, this), + }; + } + throw new NodeOperationError( this.getNode(), - 'Only the "retrieve" operation mode is supported to supply data', + 'Only the "retrieve" and "retrieve-as-tool" operation mode is supported to supply data', ); } }; diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 2b898f7b444c2..6cdd045703d10 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -720,6 +720,7 @@ export interface ActionTypeDescription extends SimplifiedNodeType { displayOptions?: IDisplayOptions; values?: IDataObject; actionKey: string; + outputConnectionType?: NodeConnectionType; codex: { label: string; categories: string[]; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue index b577669169143..d7ed3d87f0a34 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue @@ -1,7 +1,7 @@ diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue index cc0713e2b202e..3a12304afcc71 100644 --- a/packages/editor-ui/src/components/WorkflowCard.vue +++ b/packages/editor-ui/src/components/WorkflowCard.vue @@ -268,6 +268,7 @@ function moveResource() {
@@ -139,6 +145,25 @@ onBeforeMount(async () => { .filter-button { height: 40px; align-items: center; + + .filter-button-count { + margin-right: var(--spacing-4xs); + + @include mixins.breakpoint('xs-only') { + margin-right: 0; + } + } + + @media screen and (max-width: 480px) { + .filter-button-text { + text-indent: -10000px; + } + + // Remove icon margin when the "Filters" text is hidden + :global(span + span) { + margin: 0; + } + } } .filters-dropdown { diff --git a/packages/editor-ui/src/components/layouts/PageViewLayout.vue b/packages/editor-ui/src/components/layouts/PageViewLayout.vue index 419a41663ff3c..6ab16365c9de9 100644 --- a/packages/editor-ui/src/components/layouts/PageViewLayout.vue +++ b/packages/editor-ui/src/components/layouts/PageViewLayout.vue @@ -17,6 +17,10 @@ box-sizing: border-box; align-content: start; padding: var(--spacing-l) var(--spacing-2xl) 0; + + @include mixins.breakpoint('sm-and-down') { + padding: var(--spacing-s) var(--spacing-s) 0; + } } .content { diff --git a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue index 40f1d1e07cba7..33e165a6500a3 100644 --- a/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue +++ b/packages/editor-ui/src/components/layouts/ResourcesListLayout.vue @@ -475,6 +475,7 @@ onMounted(async () => { flex-direction: row; align-items: center; justify-content: space-between; + width: 100%; } .filters { @@ -483,10 +484,24 @@ onMounted(async () => { grid-auto-columns: max-content; gap: var(--spacing-2xs); align-items: center; + width: 100%; + + @include mixins.breakpoint('xs-only') { + grid-template-columns: 1fr auto; + grid-auto-flow: row; + + > *:last-child { + grid-column: auto; + } + } } .search { max-width: 240px; + + @include mixins.breakpoint('sm-and-down') { + max-width: 100%; + } } .listWrapper { @@ -497,6 +512,10 @@ onMounted(async () => { .sort-and-filter { white-space: nowrap; + + @include mixins.breakpoint('sm-and-down') { + width: 100%; + } } .datatable { diff --git a/packages/editor-ui/src/styles/plugins/_vueflow.scss b/packages/editor-ui/src/styles/plugins/_vueflow.scss index 49aad6a076e8c..7d8a7e84ae0c3 100644 --- a/packages/editor-ui/src/styles/plugins/_vueflow.scss +++ b/packages/editor-ui/src/styles/plugins/_vueflow.scss @@ -56,6 +56,10 @@ fill: var(--color-foreground-dark); opacity: 0.2; } + + @include mixins.breakpoint('xs-only') { + display: none; + } } /** @@ -100,3 +104,20 @@ .vue-flow__edge-label.selected { z-index: 1 !important; } + +/** + * Controls + */ + +.vue-flow__controls { + margin: var(--spacing-s); + + @include mixins.breakpoint('xs-only') { + max-width: calc(100% - 3 * var(--spacing-s) - var(--spacing-2xs)); + overflow: auto; + margin-left: 0; + margin-right: 0; + padding-left: var(--spacing-s); + padding-right: var(--spacing-s); + } +} diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 886b4dbc70b8e..82f0bc29a93e3 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -1772,11 +1772,13 @@ onBeforeUnmount(() => { align-items: center; left: 50%; transform: translateX(-50%); - bottom: var(--spacing-l); + bottom: var(--spacing-s); width: auto; - @media (max-width: $breakpoint-2xs) { - bottom: 150px; + @include mixins.breakpoint('sm-only') { + left: auto; + right: var(--spacing-s); + transform: none; } button { @@ -1788,6 +1790,17 @@ onBeforeUnmount(() => { &:first-child { margin: 0; } + + @include mixins.breakpoint('xs-only') { + text-indent: -10000px; + width: 42px; + height: 42px; + padding: 0; + + span { + margin: 0; + } + } } } diff --git a/packages/editor-ui/vite.config.mts b/packages/editor-ui/vite.config.mts index e939215aa6778..e4266a2d23da5 100644 --- a/packages/editor-ui/vite.config.mts +++ b/packages/editor-ui/vite.config.mts @@ -85,7 +85,11 @@ export default mergeConfig( css: { preprocessorOptions: { scss: { - additionalData: '\n@use "@/n8n-theme-variables.scss" as *;\n', + additionalData: [ + '', + '@use "@/n8n-theme-variables.scss" as *;', + '@use "n8n-design-system/css/mixins" as mixins;', + ].join('\n'), }, }, }, From f609371257163807e7ce0579c19fbc4745beca1e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 6 Jan 2025 10:37:21 -0500 Subject: [PATCH 13/15] =?UTF-8?q?fix(editor):=20Hide=20credential=E2=80=99?= =?UTF-8?q?s=20modal=20menu=20when=20the=20credential=20is=20managed=20(no?= =?UTF-8?q?-changelog)=20(#12471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CredentialEdit/CredentialEdit.vue | 2 +- .../__tests__/CredentialEdit.test.ts | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 1720cb5e00aed..0e2abb675aada 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -1106,7 +1106,7 @@ function resetCredentialData(): void {