feat(editor): Provide default ExecuteWorkflow node names based on the selected workflow (#18953)

This commit is contained in:
Charlie Kolb
2025-09-01 15:29:08 +02:00
committed by GitHub
parent bb033fc148
commit 73cc042ebc
3 changed files with 160 additions and 0 deletions

View File

@@ -92,6 +92,7 @@ const {
searchFilter,
onSearchFilter,
getWorkflowName,
renameDefaultNodeName,
populateNextWorkflowsPage,
setWorkflowsResources,
reloadWorkflows,
@@ -174,6 +175,10 @@ function onListItemSelected(value: NodeParameterValue) {
telemetry.track('User chose sub-workflow', {});
onInputChange(value);
hideDropdown();
// we rename defaults here to allow selecting the same workflow to
// update the name, as we don't eagerly update a changed workflow name
// but rather only react on changed id elsewhere
renameDefaultNodeName(value);
}
function onInputFocus(): void {
@@ -239,6 +244,19 @@ watch(
},
);
watch(
() => props.modelValue,
(val, old) => {
// We update the name only if the actual ID changed
// Because eagerly renaming the node when the target sub-workflow
// changed name means the workflow becomes unsaved and changed just by
// opening the ExecuteWorkflow node referencing the renamed workflow
if (old.value !== val.value) {
renameDefaultNodeName(val.value);
}
},
);
onClickOutside(dropdown, () => {
isDropdownVisible.value = false;
});

View File

@@ -0,0 +1,111 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { type MockedStore, mockedStore } from '@/__tests__/utils';
import type { IWorkflowDb } from '@/Interface';
import type { Router } from 'vue-router';
import { createTestingPinia } from '@pinia/testing';
const useCanvasOperations = vi.hoisted(() => vi.fn());
vi.mock('@/composables/useCanvasOperations', () => ({
useCanvasOperations,
}));
describe('useWorkflowResourcesLocator', () => {
let workflowsStoreMock: MockedStore<typeof useWorkflowsStore>;
let ndvStoreMock: MockedStore<typeof useNDVStore>;
const renameNodeMock = vi.fn();
const routerMock = { resolve: vi.fn() } as unknown as Router;
beforeEach(() => {
vi.clearAllMocks();
createTestingPinia();
workflowsStoreMock = mockedStore(useWorkflowsStore);
ndvStoreMock = mockedStore(useNDVStore);
useCanvasOperations.mockReturnValue({ renameNode: renameNodeMock });
});
describe('renameDefaultNodeName', () => {
it.each([
{
activeNodeName: 'Execute Workflow',
workflowId: 'workflow-id',
mockedWorkflow: { name: 'Test Workflow' },
expectedRename: "Call 'Test Workflow'",
expectedCalledWith: 'Execute Workflow',
},
{
activeNodeName: 'Call n8n Workflow Tool',
workflowId: 'workflow-id',
mockedWorkflow: { name: 'Test Workflow' },
expectedRename: "Call 'Test Workflow'",
expectedCalledWith: 'Call n8n Workflow Tool',
},
{
activeNodeName: "Call 'Old Workflow'",
workflowId: 'workflow-id',
mockedWorkflow: { name: 'New Workflow' },
expectedRename: "Call 'New Workflow'",
expectedCalledWith: "Call 'Old Workflow'",
},
])(
'should rename the node correctly for activeNodeName: $activeNodeName',
({ activeNodeName, workflowId, mockedWorkflow, expectedRename, expectedCalledWith }) => {
const { renameDefaultNodeName } = useWorkflowResourcesLocator(routerMock);
ndvStoreMock.activeNodeName = activeNodeName;
workflowsStoreMock.getWorkflowById.mockReturnValue(
mockedWorkflow as unknown as IWorkflowDb,
);
renameDefaultNodeName(workflowId);
expect(workflowsStoreMock.getWorkflowById).toHaveBeenCalledWith(workflowId);
expect(renameNodeMock).toHaveBeenCalledWith(expectedCalledWith, expectedRename);
},
);
it('should not rename the node for invalid workflowId', () => {
const { renameDefaultNodeName } = useWorkflowResourcesLocator(routerMock);
const workflowId = 123;
renameDefaultNodeName(workflowId);
expect(renameNodeMock).not.toHaveBeenCalled();
});
it('should not rename the node for workflowId: workflow-id with null mockedWorkflow', () => {
const { renameDefaultNodeName } = useWorkflowResourcesLocator(routerMock);
const workflowId = 'workflow-id';
const activeNodeName = 'Execute Workflow';
ndvStoreMock.activeNodeName = activeNodeName;
workflowsStoreMock.getWorkflowById.mockReturnValue(null as unknown as IWorkflowDb);
renameDefaultNodeName(workflowId);
expect(workflowsStoreMock.getWorkflowById).toHaveBeenCalledWith(workflowId);
expect(renameNodeMock).not.toHaveBeenCalled();
});
it('should not rename the node for workflowId: workflow-id with activeNodeName: Some Other Node', () => {
const { renameDefaultNodeName } = useWorkflowResourcesLocator(routerMock);
const workflowId = 'workflow-id';
const activeNodeName = 'Some Other Node';
const mockedWorkflow = { name: 'Test Workflow' };
ndvStoreMock.activeNodeName = activeNodeName;
workflowsStoreMock.getWorkflowById.mockReturnValue(mockedWorkflow as unknown as IWorkflowDb);
renameDefaultNodeName(workflowId);
expect(workflowsStoreMock.getWorkflowById).not.toHaveBeenCalled();
expect(renameNodeMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -5,9 +5,15 @@ import type { Router } from 'vue-router';
import { VIEWS } from '@/constants';
import type { IWorkflowDb } from '@/Interface';
import type { NodeParameterValue } from 'n8n-workflow';
import { useNDVStore } from '@/stores/ndv.store';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
export function useWorkflowResourcesLocator(router: Router) {
const workflowsStore = useWorkflowsStore();
const ndvStore = useNDVStore();
const { renameNode } = useCanvasOperations();
const workflowsResources = ref<Array<{ name: string; value: string; url: string }>>([]);
const isLoadingResources = ref(true);
const searchFilter = ref('');
@@ -77,10 +83,34 @@ export function useWorkflowResourcesLocator(router: Router) {
return id;
}
function getWorkflowBaseName(id: string): string | null {
const workflow = workflowsStore.getWorkflowById(id);
if (workflow) {
return workflow.name;
}
return null;
}
function onSearchFilter(filter: string) {
searchFilter.value = filter;
}
function renameDefaultNodeName(workflowId: NodeParameterValue) {
if (typeof workflowId !== 'string') return;
const nodeName = ndvStore.activeNodeName;
if (
nodeName === 'Execute Workflow' ||
nodeName === 'Call n8n Workflow Tool' ||
(nodeName?.startsWith("Call '") && nodeName?.endsWith("'"))
) {
const baseName = getWorkflowBaseName(workflowId);
if (baseName !== null) {
void renameNode(nodeName, `Call '${baseName}'`);
}
}
}
return {
workflowsResources,
isLoadingResources,
@@ -91,6 +121,7 @@ export function useWorkflowResourcesLocator(router: Router) {
getWorkflowUrl,
onSearchFilter,
getWorkflowName,
renameDefaultNodeName,
populateNextWorkflowsPage,
setWorkflowsResources,
};