Merge branch 'master' into launcher-handshake

This commit is contained in:
Iván Ovejero 2024-11-19 14:41:51 +01:00
commit 0715ec2512
No known key found for this signature in database
21 changed files with 327 additions and 53 deletions

View file

@ -72,6 +72,10 @@ export function getOutputPanelTable() {
return getOutputPanelDataContainer().get('table'); return getOutputPanelDataContainer().get('table');
} }
export function getRunDataInfoCallout() {
return cy.getByTestId('run-data-callout');
}
export function getOutputPanelItemsCount() { export function getOutputPanelItemsCount() {
return getOutputPanel().getByTestId('ndv-items-count'); return getOutputPanel().getByTestId('ndv-items-count');
} }

View file

@ -26,6 +26,8 @@ import {
clickCreateNewCredential, clickCreateNewCredential,
clickExecuteNode, clickExecuteNode,
clickGetBackToCanvas, clickGetBackToCanvas,
getRunDataInfoCallout,
getOutputPanelTable,
toggleParameterCheckboxInputByName, toggleParameterCheckboxInputByName,
} from '../composables/ndv'; } from '../composables/ndv';
import { import {
@ -418,4 +420,102 @@ describe('Langchain Integration', () => {
assertInputOutputText('Berlin', 'not.exist'); assertInputOutputText('Berlin', 'not.exist');
assertInputOutputText('Kyiv', 'not.exist'); assertInputOutputText('Kyiv', 'not.exist');
}); });
it('should show tool info notice if no existing tools were used during execution', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true);
addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AGENT_NODE_NAME,
true,
);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
openNode(AGENT_NODE_NAME);
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode();
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: [
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: outputMessage },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
],
lastNodeExecuted: AGENT_NODE_NAME,
});
closeManualChatModal();
openNode(AGENT_NODE_NAME);
getRunDataInfoCallout().should('exist');
});
it('should not show tool info notice if tools were used during execution', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true, true);
getRunDataInfoCallout().should('not.exist');
clickGetBackToCanvas();
addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AGENT_NODE_NAME,
true,
);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
openNode(AGENT_NODE_NAME);
getRunDataInfoCallout().should('not.exist');
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
clickExecuteNode();
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: [
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: outputMessage },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
createMockNodeExecutionData(AI_TOOL_CALCULATOR_NODE_NAME, {}),
],
lastNodeExecuted: AGENT_NODE_NAME,
});
closeManualChatModal();
openNode(AGENT_NODE_NAME);
// This waits to ensure the output panel is rendered
getOutputPanelTable();
getRunDataInfoCallout().should('not.exist');
});
}); });

View file

@ -10,7 +10,7 @@ import { WorkflowPage } from '../pages/workflow';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const NOW = 1717771477012; const NOW = Date.now();
const ONE_DAY = 24 * 60 * 60 * 1000; const ONE_DAY = 24 * 60 * 60 * 1000;
const THREE_DAYS = ONE_DAY * 3; const THREE_DAYS = ONE_DAY * 3;
const SEVEN_DAYS = ONE_DAY * 7; const SEVEN_DAYS = ONE_DAY * 7;

View file

@ -557,6 +557,8 @@ describe('General help', () => {
}).as('chatRequest'); }).as('chatRequest');
aiAssistant.getters.askAssistantFloatingButton().click(); aiAssistant.getters.askAssistantFloatingButton().click();
wf.getters.zoomToFitButton().click();
aiAssistant.actions.sendMessage('What is wrong with this workflow?'); aiAssistant.actions.sendMessage('What is wrong with this workflow?');
cy.wait('@chatRequest'); cy.wait('@chatRequest');

View file

@ -30,6 +30,13 @@ export class AzureOpenAiApi implements ICredentialType {
required: true, required: true,
default: '2023-07-01-preview', default: '2023-07-01-preview',
}, },
{
displayName: 'Endpoint',
name: 'endpoint',
type: 'string',
default: undefined,
placeholder: 'https://westeurope.api.cognitive.microsoft.com',
},
]; ];
authenticate: IAuthenticateGeneric = { authenticate: IAuthenticateGeneric = {

View file

@ -128,6 +128,7 @@ export class EmbeddingsAzureOpenAi implements INodeType {
apiKey: string; apiKey: string;
resourceName: string; resourceName: string;
apiVersion: string; apiVersion: string;
endpoint?: string;
}>('azureOpenAiApi'); }>('azureOpenAiApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;
@ -144,9 +145,15 @@ export class EmbeddingsAzureOpenAi implements INodeType {
const embeddings = new OpenAIEmbeddings({ const embeddings = new OpenAIEmbeddings({
azureOpenAIApiDeploymentName: modelName, azureOpenAIApiDeploymentName: modelName,
azureOpenAIApiInstanceName: credentials.resourceName, // instance name only needed to set base url
azureOpenAIApiInstanceName: !credentials.endpoint ? credentials.resourceName : undefined,
azureOpenAIApiKey: credentials.apiKey, azureOpenAIApiKey: credentials.apiKey,
azureOpenAIApiVersion: credentials.apiVersion, azureOpenAIApiVersion: credentials.apiVersion,
// azureOpenAIEndpoint and configuration.baseURL are both ignored here
// only setting azureOpenAIBasePath worked
azureOpenAIBasePath: credentials.endpoint
? `${credentials.endpoint}/openai/deployments`
: undefined,
...options, ...options,
}); });

View file

@ -168,6 +168,7 @@ export class LmChatAzureOpenAi implements INodeType {
apiKey: string; apiKey: string;
resourceName: string; resourceName: string;
apiVersion: string; apiVersion: string;
endpoint?: string;
}>('azureOpenAiApi'); }>('azureOpenAiApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;
@ -184,9 +185,11 @@ export class LmChatAzureOpenAi implements INodeType {
const model = new ChatOpenAI({ const model = new ChatOpenAI({
azureOpenAIApiDeploymentName: modelName, azureOpenAIApiDeploymentName: modelName,
azureOpenAIApiInstanceName: credentials.resourceName, // instance name only needed to set base url
azureOpenAIApiInstanceName: !credentials.endpoint ? credentials.resourceName : undefined,
azureOpenAIApiKey: credentials.apiKey, azureOpenAIApiKey: credentials.apiKey,
azureOpenAIApiVersion: credentials.apiVersion, azureOpenAIApiVersion: credentials.apiVersion,
azureOpenAIEndpoint: credentials.endpoint,
...options, ...options,
timeout: options.timeout ?? 60000, timeout: options.timeout ?? 60000,
maxRetries: options.maxRetries ?? 2, maxRetries: options.maxRetries ?? 2,

View file

@ -93,7 +93,7 @@ export class FileSystemManager implements BinaryData.Manager {
// binary files stored in nested dirs - `filesystem-v2` // binary files stored in nested dirs - `filesystem-v2`
const binaryDataDirs = ids.map(({ workflowId, executionId }) => const binaryDataDirs = ids.map(({ workflowId, executionId }) =>
this.resolvePath(`workflows/${workflowId}/executions/${executionId}/binary_data/`), this.resolvePath(`workflows/${workflowId}/executions/${executionId}`),
); );
await Promise.all( await Promise.all(

View file

@ -147,6 +147,11 @@ describe('copyByFilePath()', () => {
}); });
describe('deleteMany()', () => { describe('deleteMany()', () => {
const rmOptions = {
force: true,
recursive: true,
};
it('should delete many files by workflow ID and execution ID', async () => { it('should delete many files by workflow ID and execution ID', async () => {
const ids = [ const ids = [
{ workflowId, executionId }, { workflowId, executionId },
@ -160,6 +165,16 @@ describe('deleteMany()', () => {
await expect(promise).resolves.not.toThrow(); await expect(promise).resolves.not.toThrow();
expect(fsp.rm).toHaveBeenCalledTimes(2); expect(fsp.rm).toHaveBeenCalledTimes(2);
expect(fsp.rm).toHaveBeenNthCalledWith(
1,
`${storagePath}/workflows/${workflowId}/executions/${executionId}`,
rmOptions,
);
expect(fsp.rm).toHaveBeenNthCalledWith(
2,
`${storagePath}/workflows/${otherWorkflowId}/executions/${otherExecutionId}`,
rmOptions,
);
}); });
it('should suppress error on non-existing filepath', async () => { it('should suppress error on non-existing filepath', async () => {

View file

@ -344,6 +344,8 @@ async function onCopyButtonClick(content: string, e: MouseEvent) {
.container { .container {
height: 100%; height: 100%;
position: relative; position: relative;
display: grid;
grid-template-rows: auto 1fr auto;
} }
p { p {
@ -373,10 +375,6 @@ p {
background-color: var(--color-background-light); background-color: var(--color-background-light);
border: var(--border-base); border: var(--border-base);
border-top: 0; border-top: 0;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
padding-bottom: 250px; // make scrollable at the end
position: relative; position: relative;
pre, pre,
@ -390,7 +388,13 @@ p {
} }
.messages { .messages {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: var(--spacing-xs); padding: var(--spacing-xs);
overflow-y: auto;
& + & { & + & {
padding-top: 0; padding-top: 0;

View file

@ -128,7 +128,8 @@ watch(defaultLocale, (newLocale) => {
.container { .container {
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
display: flex; display: grid;
grid-template-columns: 1fr auto;
} }
// App grid is the main app layout including modals and other absolute positioned elements // App grid is the main app layout including modals and other absolute positioned elements
@ -136,13 +137,12 @@ watch(defaultLocale, (newLocale) => {
position: relative; position: relative;
display: grid; display: grid;
height: 100vh; height: 100vh;
flex-basis: 100%;
grid-template-areas: grid-template-areas:
'banners banners' 'banners banners'
'sidebar header' 'sidebar header'
'sidebar content'; 'sidebar content';
grid-auto-columns: minmax(0, max-content) 1fr; grid-template-columns: auto 1fr;
grid-template-rows: auto fit-content($header-height) 1fr; grid-template-rows: auto auto 1fr;
} }
.banners { .banners {

View file

@ -103,12 +103,6 @@ function onClose() {
</template> </template>
<style module> <style module>
.container {
height: 100%;
flex-basis: content;
z-index: var(--z-index-ask-assistant-chat);
}
.wrapper { .wrapper {
height: 100%; height: 100%;
} }

View file

@ -211,7 +211,6 @@ async function navigateToExecutionsView(openInNewTab: boolean) {
.main-header { .main-header {
background-color: var(--color-background-xlight); background-color: var(--color-background-xlight);
height: $header-height;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base); border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
@ -222,16 +221,15 @@ async function navigateToExecutionsView(openInNewTab: boolean) {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 0.9em; font-size: 0.9em;
height: $header-height;
font-weight: 400; font-weight: 400;
padding: 0 var(--spacing-m) 0 var(--spacing-m); padding: var(--spacing-xs) var(--spacing-m);
} }
.github-button { .github-button {
display: flex; display: flex;
position: relative; position: relative;
align-items: center; align-items: center;
height: $header-height; align-self: stretch;
padding-left: var(--spacing-m); padding-left: var(--spacing-m);
padding-right: var(--spacing-m); padding-right: var(--spacing-m);
background-color: var(--color-background-xlight); background-color: var(--color-background-xlight);

View file

@ -40,15 +40,16 @@ function onUpdateModelValue(tab: MAIN_HEADER_TABS, event: MouseEvent): void {
<style module lang="scss"> <style module lang="scss">
.container { .container {
position: absolute; position: absolute;
top: 47px; bottom: 0;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%) translateY(50%);
min-height: 30px; min-height: 30px;
display: flex; display: flex;
padding: var(--spacing-5xs); padding: var(--spacing-5xs);
background-color: var(--color-foreground-base); background-color: var(--color-foreground-base);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
z-index: 1;
} }
@media screen and (max-width: 430px) { @media screen and (max-width: 430px) {

View file

@ -777,6 +777,10 @@ $--header-spacing: 20px;
.name-container { .name-container {
margin-right: $--header-spacing; margin-right: $--header-spacing;
:deep(.el-input) {
padding: 0;
}
} }
.name { .name {
@ -827,6 +831,7 @@ $--header-spacing: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
flex-wrap: wrap;
} }
</style> </style>
@ -837,6 +842,7 @@ $--header-spacing: 20px;
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.group { .group {

View file

@ -1,6 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'; import { ref, computed, onMounted, watch } from 'vue';
import type { IRunData, IRunExecutionData, NodeError, Workflow } from 'n8n-workflow'; import {
NodeConnectionType,
type IRunData,
type IRunExecutionData,
type NodeError,
type Workflow,
} from 'n8n-workflow';
import RunData from './RunData.vue'; import RunData from './RunData.vue';
import RunInfo from './RunInfo.vue'; import RunInfo from './RunInfo.vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@ -209,6 +215,29 @@ const canPinData = computed(() => {
return pinnedData.isValidNodeType.value && !props.isReadOnly; return pinnedData.isValidNodeType.value && !props.isReadOnly;
}); });
const allToolsWereUnusedNotice = computed(() => {
if (!node.value || runsCount.value === 0) return undefined;
// With pinned data there's no clear correct answer for whether
// we should use historic or current parents, so we don't show the notice,
// as it likely ends up unactionable noise to the user
if (pinnedData.hasData.value) return undefined;
const toolsAvailable = props.workflow.getParentNodes(
node.value.name,
NodeConnectionType.AiTool,
1,
);
const toolsUsedInLatestRun = toolsAvailable.filter(
(tool) => !!workflowRunData.value?.[tool]?.[props.runIndex],
);
if (toolsAvailable.length > 0 && toolsUsedInLatestRun.length === 0) {
return i18n.baseText('ndv.output.noToolUsedInfo');
} else {
return undefined;
}
});
// Methods // Methods
const insertTestData = () => { const insertTestData = () => {
@ -298,6 +327,7 @@ const activatePane = () => {
:hide-pagination="outputMode === 'logs'" :hide-pagination="outputMode === 'logs'"
pane-type="output" pane-type="output"
:data-output-type="outputMode" :data-output-type="outputMode"
:callout-message="allToolsWereUnusedNotice"
@activate-pane="activatePane" @activate-pane="activatePane"
@run-change="onRunIndexChange" @run-change="onRunIndexChange"
@link-run="onLinkRun" @link-run="onLinkRun"

View file

@ -120,6 +120,7 @@ type Props = {
isProductionExecutionPreview?: boolean; isProductionExecutionPreview?: boolean;
isPaneActive?: boolean; isPaneActive?: boolean;
hidePagination?: boolean; hidePagination?: boolean;
calloutMessage?: string;
}; };
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -133,6 +134,7 @@ const props = withDefaults(defineProps<Props>(), {
mappingEnabled: false, mappingEnabled: false,
isExecuting: false, isExecuting: false,
hidePagination: false, hidePagination: false,
calloutMessage: undefined,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
search: [search: string]; search: [search: string];
@ -1426,6 +1428,12 @@ defineExpose({ enterEditMode });
<slot v-if="!displaysMultipleNodes" name="before-data" /> <slot v-if="!displaysMultipleNodes" name="before-data" />
<div v-if="props.calloutMessage" :class="$style.hintCallout">
<N8nCallout theme="secondary" data-test-id="run-data-callout">
<N8nText v-n8n-html="props.calloutMessage" size="small"></N8nText>
</N8nCallout>
</div>
<N8nCallout <N8nCallout
v-for="hint in getNodeHints()" v-for="hint in getNodeHints()"
:key="hint.message" :key="hint.message"

View file

@ -247,6 +247,7 @@ exports[`InputPanel > should render 1`] = `
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->
<!--v-if-->
<!--v-if--> <!--v-if-->

View file

@ -4,6 +4,7 @@ import type {
INodeTypeDescription, INodeTypeDescription,
IWebhookDescription, IWebhookDescription,
Workflow, Workflow,
INodeConnections,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow'; import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import { useCanvasOperations } from '@/composables/useCanvasOperations'; import { useCanvasOperations } from '@/composables/useCanvasOperations';
@ -2049,36 +2050,129 @@ describe('useCanvasOperations', () => {
expect(workflowsStore.setNodes).toHaveBeenCalled(); expect(workflowsStore.setNodes).toHaveBeenCalled();
expect(workflowsStore.setConnections).toHaveBeenCalled(); expect(workflowsStore.setConnections).toHaveBeenCalled();
}); });
it('should initialize node data from node type description', () => {
const nodeTypesStore = mockedStore(useNodeTypesStore);
const type = SET_NODE_TYPE;
const version = 1;
const expectedDescription = mockNodeTypeDescription({
name: type,
version,
properties: [
{
displayName: 'Value',
name: 'value',
type: 'boolean',
default: true,
},
],
});
nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } };
const workflow = createTestWorkflow({
nodes: [createTestNode()],
connections: {},
});
const { initializeWorkspace } = useCanvasOperations({ router });
initializeWorkspace(workflow);
expect(workflow.nodes[0].parameters).toEqual({ value: true });
});
}); });
it('should initialize node data from node type description', () => { describe('filterConnectionsByNodes', () => {
const nodeTypesStore = mockedStore(useNodeTypesStore); it('should return filtered connections when all nodes are included', () => {
const type = SET_NODE_TYPE; const connections: INodeConnections = {
const version = 1; [NodeConnectionType.Main]: [
const expectedDescription = mockNodeTypeDescription({ [
name: type, { node: 'node1', type: NodeConnectionType.Main, index: 0 },
version, { node: 'node2', type: NodeConnectionType.Main, index: 0 },
properties: [ ],
{ [{ node: 'node3', type: NodeConnectionType.Main, index: 0 }],
displayName: 'Value', ],
name: 'value', };
type: 'boolean', const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
default: true,
}, const { filterConnectionsByNodes } = useCanvasOperations({ router });
], const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual(connections);
}); });
nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } }; it('should return empty connections when no nodes are included', () => {
const connections: INodeConnections = {
[NodeConnectionType.Main]: [
[
{ node: 'node1', type: NodeConnectionType.Main, index: 0 },
{ node: 'node2', type: NodeConnectionType.Main, index: 0 },
],
[{ node: 'node3', type: NodeConnectionType.Main, index: 0 }],
],
};
const includeNodeNames = new Set<string>();
const workflow = createTestWorkflow({ const { filterConnectionsByNodes } = useCanvasOperations({ router });
nodes: [createTestNode()], const result = filterConnectionsByNodes(connections, includeNodeNames);
connections: {},
expect(result).toEqual({
[NodeConnectionType.Main]: [[], []],
});
}); });
const { initializeWorkspace } = useCanvasOperations({ router }); it('should return partially filtered connections when some nodes are included', () => {
initializeWorkspace(workflow); const connections: INodeConnections = {
[NodeConnectionType.Main]: [
[
{ node: 'node1', type: NodeConnectionType.Main, index: 0 },
{ node: 'node2', type: NodeConnectionType.Main, index: 0 },
],
[{ node: 'node3', type: NodeConnectionType.Main, index: 0 }],
],
};
const includeNodeNames = new Set<string>(['node1']);
expect(workflow.nodes[0].parameters).toEqual({ value: true }); const { filterConnectionsByNodes } = useCanvasOperations({ router });
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual({
[NodeConnectionType.Main]: [
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
[],
],
});
});
it('should handle empty connections input', () => {
const connections: INodeConnections = {};
const includeNodeNames = new Set<string>(['node1']);
const { filterConnectionsByNodes } = useCanvasOperations({ router });
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual({});
});
it('should handle connections with no valid nodes', () => {
const connections: INodeConnections = {
[NodeConnectionType.Main]: [
[
{ node: 'node4', type: NodeConnectionType.Main, index: 0 },
{ node: 'node5', type: NodeConnectionType.Main, index: 0 },
],
[{ node: 'node6', type: NodeConnectionType.Main, index: 0 }],
],
};
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
const { filterConnectionsByNodes } = useCanvasOperations({ router });
const result = filterConnectionsByNodes(connections, includeNodeNames);
expect(result).toEqual({
[NodeConnectionType.Main]: [[], []],
});
});
}); });
}); });

View file

@ -1809,17 +1809,15 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
} }
function filterConnectionsByNodes( function filterConnectionsByNodes(
connections: Record<string, IConnection[][]>, connections: INodeConnections,
includeNodeNames: Set<string>, includeNodeNames: Set<string>,
): INodeConnections { ): INodeConnections {
const filteredConnections: INodeConnections = {}; const filteredConnections: INodeConnections = {};
for (const [type, typeConnections] of Object.entries(connections)) { for (const [type, typeConnections] of Object.entries(connections)) {
const validConnections = typeConnections const validConnections = typeConnections.map((sourceConnections) =>
.map((sourceConnections) => sourceConnections.filter((connection) => includeNodeNames.has(connection.node)),
sourceConnections.filter((connection) => includeNodeNames.has(connection.node)), );
)
.filter((sourceConnections) => sourceConnections.length > 0);
if (validConnections.length) { if (validConnections.length) {
filteredConnections[type] = validConnections; filteredConnections[type] = validConnections;
@ -1888,6 +1886,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
revertDeleteConnection, revertDeleteConnection,
deleteConnectionsByNodeId, deleteConnectionsByNodeId,
isConnectionAllowed, isConnectionAllowed,
filterConnectionsByNodes,
importWorkflowData, importWorkflowData,
fetchWorkflowDataFromUrl, fetchWorkflowDataFromUrl,
resetWorkspace, resetWorkspace,

View file

@ -988,6 +988,7 @@
"ndv.output.tooMuchData.showDataAnyway": "Show data", "ndv.output.tooMuchData.showDataAnyway": "Show data",
"ndv.output.tooMuchData.title": "Display data?", "ndv.output.tooMuchData.title": "Display data?",
"ndv.output.waitingToRun": "Waiting to execute...", "ndv.output.waitingToRun": "Waiting to execute...",
"ndv.output.noToolUsedInfo": "None of your tools were used in this run. Try giving your tools clearer names and descriptions to help the AI",
"ndv.title.cancel": "Cancel", "ndv.title.cancel": "Cancel",
"ndv.title.rename": "Rename", "ndv.title.rename": "Rename",
"ndv.title.renameNode": "Rename node", "ndv.title.renameNode": "Rename node",