From 305ea0fb324fd9a20f0183cc6478c7e6b130b4d3 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 20 Mar 2025 09:45:10 -0400 Subject: [PATCH] feat(core): Allow moving workflow to project root (no-changelog) (#14075) --- .../cli/src/workflows/workflow.service.ts | 8 ++++--- .../workflows/workflows.controller.test.ts | 23 +++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index bf71261278..f2a497279b 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -9,7 +9,7 @@ import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPar import omit from 'lodash/omit'; import pick from 'lodash/pick'; import { BinaryDataService, Logger } from 'n8n-core'; -import { NodeApiError } from 'n8n-workflow'; +import { NodeApiError, PROJECT_ROOT } from 'n8n-workflow'; import { v4 as uuid } from 'uuid'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; @@ -281,8 +281,10 @@ export class WorkflowService { if (parentFolderId) { const project = await this.sharedWorkflowRepository.getWorkflowOwningProject(workflow.id); - await this.folderService.findFolderInProjectOrFail(parentFolderId, project?.id ?? ''); - updatePayload.parentFolder = { id: parentFolderId }; + if (parentFolderId !== PROJECT_ROOT) { + await this.folderService.findFolderInProjectOrFail(parentFolderId, project?.id ?? ''); + } + updatePayload.parentFolder = parentFolderId === PROJECT_ROOT ? null : { id: parentFolderId }; } await this.workflowRepository.update(workflowId, updatePayload); diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index f26c4c728d..043b958d4a 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -1,7 +1,7 @@ import { Container } from '@n8n/di'; import type { Scope } from '@n8n/permissions'; import { DateTime } from 'luxon'; -import type { INode, IPinData, IWorkflowBase } from 'n8n-workflow'; +import { PROJECT_ROOT, type INode, type IPinData, type IWorkflowBase } from 'n8n-workflow'; import { v4 as uuid } from 'uuid'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; @@ -2045,7 +2045,7 @@ describe('PATCH /workflows/:workflowId', () => { expect(updatedWorkflow.meta).toEqual(payload.meta); }); - test('should update workflow parent folder', async () => { + test('should move workflow to folder', async () => { const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); const folder1 = await createFolder(ownerPersonalProject, { name: 'folder1' }); @@ -2067,6 +2067,25 @@ describe('PATCH /workflows/:workflowId', () => { expect(updatedWorkflow.parentFolder?.id).toBe(folder1.id); }); + test('should move workflow to project root', async () => { + const workflow = await createWorkflow({}, owner); + const payload = { + versionId: workflow.versionId, + parentFolderId: PROJECT_ROOT, + }; + + const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload); + + expect(response.statusCode).toBe(200); + + const updatedWorkflow = await Container.get(WorkflowRepository).findOneOrFail({ + where: { id: workflow.id }, + relations: ['parentFolder'], + }); + + expect(updatedWorkflow.parentFolder).toBe(null); + }); + test('should fail if trying update workflow parent folder with a folder that does not belong to project', async () => { const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); const memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(