diff --git a/packages/@n8n/api-types/src/dto/workflows/transfer.dto.ts b/packages/@n8n/api-types/src/dto/workflows/transfer.dto.ts index 27afab9de8..b97db930a4 100644 --- a/packages/@n8n/api-types/src/dto/workflows/transfer.dto.ts +++ b/packages/@n8n/api-types/src/dto/workflows/transfer.dto.ts @@ -4,4 +4,5 @@ import { Z } from 'zod-class'; export class TransferWorkflowBodyDto extends Z.class({ destinationProjectId: z.string(), shareCredentials: z.array(z.string()).optional(), + destinationParentFolderId: z.string().optional(), }) {} diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 3cbacdf29c..20f476bdb6 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -20,6 +20,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { TransferWorkflowError } from '@/errors/response-errors/transfer-workflow.error'; +import { FolderService } from '@/services/folder.service'; import { OwnershipService } from '@/services/ownership.service'; import { ProjectService } from '@/services/project.service.ee'; @@ -43,6 +44,7 @@ export class EnterpriseWorkflowService { private readonly credentialsFinderService: CredentialsFinderService, private readonly enterpriseCredentialsService: EnterpriseCredentialsService, private readonly workflowFinderService: WorkflowFinderService, + private readonly folderService: FolderService, ) {} async shareWithProjects( @@ -265,6 +267,7 @@ export class EnterpriseWorkflowService { workflowId: string, destinationProjectId: string, shareCredentials: string[] = [], + destinationParentFolderId?: string, ) { // 1. get workflow const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ @@ -303,6 +306,21 @@ export class EnterpriseWorkflowService { ); } + let parentFolder = null; + + if (destinationParentFolderId) { + try { + parentFolder = await this.folderService.findFolderInProjectOrFail( + destinationParentFolderId, + destinationProjectId, + ); + } catch { + throw new TransferWorkflowError( + `The destination folder with id "${destinationParentFolderId}" does not exist in the project "${destinationProject.name}".`, + ); + } + } + // 6. deactivate workflow if necessary const wasActive = workflow.active; if (wasActive) { @@ -345,10 +363,10 @@ export class EnterpriseWorkflowService { } }); - // 9. detach workflow from parent folder in source project - await this.workflowRepository.update({ id: workflow.id }, { parentFolder: null }); + // 9. Move workflow to the right folder if any + await this.workflowRepository.update({ id: workflow.id }, { parentFolder }); - // 9. try to activate it again if it was active + // 10. try to activate it again if it was active if (wasActive) { try { await this.activeWorkflowManager.add(workflowId, 'update'); diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 4bbe8bf1bb..8bbfabc5b4 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -510,6 +510,7 @@ export class WorkflowsController { workflowId, body.destinationProjectId, body.shareCredentials, + body.destinationParentFolderId, ); } } diff --git a/packages/cli/test/integration/workflows/workflow.service.ee.test.ts b/packages/cli/test/integration/workflows/workflow.service.ee.test.ts index 92f07feb92..70be3117da 100644 --- a/packages/cli/test/integration/workflows/workflow.service.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflow.service.ee.test.ts @@ -36,6 +36,7 @@ describe('EnterpriseWorkflowService', () => { mock(), mock(), mock(), + mock(), ); }); diff --git a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts index 4cd3c51332..28a5168d35 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts @@ -1620,7 +1620,7 @@ describe('PUT /:workflowId/transfer', () => { expect(activeWorkflowManager.add).toHaveBeenCalledWith(workflow.id, 'update'); }); - test('should detach workflow from parent folder in source project', async () => { + test('should move workflow to project root if `destinationParentFolderId` is not provided', async () => { // // ARRANGE // @@ -1652,6 +1652,72 @@ describe('PUT /:workflowId/transfer', () => { expect(workflowFromDB.parentFolder).toBeNull(); }); + test('should move workflow to the parent folder in source project if `destinationParentFolderId` is provided', async () => { + // + // ARRANGE + // + const destinationProject = await createTeamProject('Team Project', member); + + const folder = await createFolder(destinationProject, { name: 'Test Folder' }); + + const workflow = await createWorkflow({ active: true, parentFolder: folder }, member); + + // + // ACT + // + const response = await testServer + .authAgentFor(member) + .put(`/workflows/${workflow.id}/transfer`) + .send({ destinationProjectId: destinationProject.id, destinationParentFolderId: folder.id }) + .expect(200); + + // + // ASSERT + // + expect(response.body).toEqual({}); + + const workflowFromDB = await workflowRepository.findOneOrFail({ + where: { id: workflow.id }, + relations: ['parentFolder'], + }); + + expect(workflowFromDB.parentFolder?.id).toBe(folder.id); + }); + + test('should fail destination parent folder does not exist in project', async () => { + // + // ARRANGE + // + const destinationProject = await createTeamProject('Team Project', member); + + const anotherProject = await createTeamProject('Another Project', member); + + const folderInDestinationProject = await createFolder(destinationProject, { + name: 'Test Folder', + }); + + const anotherFolder = await createFolder(destinationProject, { + name: 'Another Test Folder', + }); + + const workflow = await createWorkflow( + { active: true, parentFolder: folderInDestinationProject }, + member, + ); + + // + // ACT + // + await testServer + .authAgentFor(member) + .put(`/workflows/${workflow.id}/transfer`) + .send({ + destinationProjectId: anotherProject.id, + destinationParentFolderId: anotherFolder.id, + }) + .expect(400); + }); + test('deactivates the workflow if it cannot be added to the active workflow manager again and returns the WorkflowActivationError as data', async () => { // // ARRANGE