From d4e7a2cd96c32ea0aed96cc4dd7839fdc2e60a6e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 20 Mar 2025 08:43:03 -0400 Subject: [PATCH] feat(core): Allow transferring folder to project root with delete operation (no-changelog) (#14074) --- .../repositories/workflow.repository.ts | 9 ++- packages/cli/src/services/folder.service.ts | 4 +- .../folder/folder.controller.test.ts | 57 ++++++++++++++++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index 99cc8b592a..9715ee2f8f 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -619,9 +619,12 @@ export class WorkflowRepository extends Repository { WorkflowEntity, { parentFolder: { id: fromFolderId } }, { - parentFolder: { - id: toFolderId, - }, + parentFolder: + toFolderId === PROJECT_ROOT + ? null + : { + id: toFolderId, + }, }, ); } diff --git a/packages/cli/src/services/folder.service.ts b/packages/cli/src/services/folder.service.ts index 0e7f7da012..fc35452139 100644 --- a/packages/cli/src/services/folder.service.ts +++ b/packages/cli/src/services/folder.service.ts @@ -138,7 +138,9 @@ export class FolderService { throw new UserError('Cannot transfer folder contents to the folder being deleted'); } - await this.findFolderInProjectOrFail(transferToFolderId, projectId); + if (transferToFolderId !== PROJECT_ROOT) { + await this.findFolderInProjectOrFail(transferToFolderId, projectId); + } return await this.folderRepository.manager.transaction(async (tx) => { await this.folderRepository.moveAllToFolder(folderId, transferToFolderId, tx); diff --git a/packages/cli/test/integration/folder/folder.controller.test.ts b/packages/cli/test/integration/folder/folder.controller.test.ts index 70f67b4702..e84dcf8017 100644 --- a/packages/cli/test/integration/folder/folder.controller.test.ts +++ b/packages/cli/test/integration/folder/folder.controller.test.ts @@ -1,5 +1,6 @@ import { Container } from '@n8n/di'; import { DateTime } from 'luxon'; +import { PROJECT_ROOT } from 'n8n-workflow'; import type { Project } from '@/databases/entities/project'; import type { User } from '@/databases/entities/user'; @@ -562,7 +563,7 @@ describe('PATCH /projects/:projectId/folders/:folderId', () => { expect(folder?.parentFolder?.id).toBe(parentFolder.id); const payload = { - parentFolderId: '0', + parentFolderId: PROJECT_ROOT, }; await authOwnerAgent @@ -883,6 +884,58 @@ describe('DELETE /projects/:projectId/folders/:folderId', () => { const folderInDb = await folderRepository.findOneBy({ id: folder.id }); expect(folderInDb).toBeDefined(); }); + + test('should transfer folder contents to project root when transferToFolderId is "0"', async () => { + const project = await createTeamProject('test', owner); + const sourceFolder = await createFolder(project, { name: 'Source' }); + await createFolder(project, { name: 'Target' }); + const childFolder = await createFolder(project, { + name: 'Child', + parentFolder: sourceFolder, + }); + + const workflow1 = await createWorkflow({ parentFolder: sourceFolder }, owner); + + const workflow2 = await createWorkflow({ parentFolder: childFolder }, owner); + + const payload = { + transferToFolderId: PROJECT_ROOT, + }; + + await authOwnerAgent + .delete(`/projects/${project.id}/folders/${sourceFolder.id}`) + .query(payload) + .expect(200); + + const sourceFolderInDb = await folderRepository.findOne({ + where: { id: sourceFolder.id }, + relations: ['parentFolder'], + }); + const childFolderInDb = await folderRepository.findOne({ + where: { id: childFolder.id }, + relations: ['parentFolder'], + }); + + // Check folders + expect(sourceFolderInDb).toBeNull(); + expect(childFolderInDb).toBeDefined(); + expect(childFolderInDb?.parentFolder).toBe(null); + + // Check workflows + const workflow1InDb = await workflowRepository.findOne({ + where: { id: workflow1.id }, + relations: ['parentFolder'], + }); + expect(workflow1InDb).toBeDefined(); + expect(workflow1InDb?.parentFolder).toBe(null); + + const workflow2InDb = await workflowRepository.findOne({ + where: { id: workflow2.id }, + relations: ['parentFolder'], + }); + expect(workflow2InDb).toBeDefined(); + expect(workflow2InDb?.parentFolder?.id).toBe(childFolder.id); + }); }); describe('GET /projects/:projectId/folders', () => { @@ -968,7 +1021,7 @@ describe('GET /projects/:projectId/folders', () => { const response = await authOwnerAgent .get(`/projects/${ownerProject.id}/folders`) - .query({ filter: '{ "parentFolderId": "0" }' }) + .query({ filter: `{ "parentFolderId": "${PROJECT_ROOT}" }` }) .expect(200); expect(response.body.count).toBe(3);