mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Archive workflows when removing folders without transfer (#15057)
This commit is contained in:
@@ -104,7 +104,7 @@ export class ProjectController {
|
||||
const { projectId, folderId } = req.params;
|
||||
|
||||
try {
|
||||
await this.folderService.deleteFolder(folderId, projectId, payload);
|
||||
await this.folderService.deleteFolder(req.user, folderId, projectId, payload);
|
||||
} catch (e) {
|
||||
if (e instanceof FolderNotFoundError) {
|
||||
throw new NotFoundError(e.message);
|
||||
|
||||
@@ -297,6 +297,22 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
return [enrichedWorkflowsAndFolders, count] as const;
|
||||
}
|
||||
|
||||
async getAllWorkflowIdsInHierarchy(folderId: string, projectId: string): Promise<string[]> {
|
||||
const subFolderIds = await this.folderRepository.getAllFolderIdsInHierarchy(
|
||||
folderId,
|
||||
projectId,
|
||||
);
|
||||
|
||||
const query = this.createQueryBuilder('workflow');
|
||||
|
||||
this.applySelect(query, { id: true });
|
||||
this.applyParentFolderFilter(query, { parentFolderIds: [folderId, ...subFolderIds] });
|
||||
|
||||
const workflowIds = (await query.getMany()).map((workflow) => workflow.id);
|
||||
|
||||
return workflowIds;
|
||||
}
|
||||
|
||||
private getFolderIds(workflowsAndFolders: WorkflowFolderUnionRow[]) {
|
||||
return workflowsAndFolders.filter((item) => item.resource === 'folder').map((item) => item.id);
|
||||
}
|
||||
@@ -669,4 +685,11 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async moveToFolder(workflowIds: string[], toFolderId: string) {
|
||||
await this.update(
|
||||
{ id: In(workflowIds) },
|
||||
{ parentFolder: toFolderId === PROJECT_ROOT ? null : { id: toFolderId } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CreateFolderDto, DeleteFolderDto, UpdateFolderDto } from '@n8n/api-types';
|
||||
import { Folder, FolderTagMappingRepository, FolderRepository } from '@n8n/db';
|
||||
import { Folder, FolderTagMappingRepository, FolderRepository, type User } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
@@ -8,6 +8,7 @@ import { UserError, PROJECT_ROOT } from 'n8n-workflow';
|
||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
||||
import type { ListQuery } from '@/requests';
|
||||
import { WorkflowService } from '@/workflows/workflow.service';
|
||||
|
||||
export interface SimpleFolderNode {
|
||||
id: string;
|
||||
@@ -27,6 +28,7 @@ export class FolderService {
|
||||
private readonly folderRepository: FolderRepository,
|
||||
private readonly folderTagMappingRepository: FolderTagMappingRepository,
|
||||
private readonly workflowRepository: WorkflowRepository,
|
||||
private readonly workflowService: WorkflowService,
|
||||
) {}
|
||||
|
||||
async createFolder({ parentFolderId, name }: CreateFolderDto, projectId: string) {
|
||||
@@ -124,10 +126,35 @@ export class FolderService {
|
||||
return this.transformFolderPathToTree(result);
|
||||
}
|
||||
|
||||
async deleteFolder(folderId: string, projectId: string, { transferToFolderId }: DeleteFolderDto) {
|
||||
/**
|
||||
* Moves all workflows in a folder to the root of the project and archives them,
|
||||
* flattening the folder structure.
|
||||
*
|
||||
* If any workflows were active this will also deactivate those workflows.
|
||||
*/
|
||||
async flattenAndArchive(user: User, folderId: string, projectId: string): Promise<void> {
|
||||
const workflowIds = await this.workflowRepository.getAllWorkflowIdsInHierarchy(
|
||||
folderId,
|
||||
projectId,
|
||||
);
|
||||
|
||||
for (const workflowId of workflowIds) {
|
||||
await this.workflowService.archive(user, workflowId, true);
|
||||
}
|
||||
|
||||
await this.workflowRepository.moveToFolder(workflowIds, PROJECT_ROOT);
|
||||
}
|
||||
|
||||
async deleteFolder(
|
||||
user: User,
|
||||
folderId: string,
|
||||
projectId: string,
|
||||
{ transferToFolderId }: DeleteFolderDto,
|
||||
) {
|
||||
await this.findFolderInProjectOrFail(folderId, projectId);
|
||||
|
||||
if (!transferToFolderId) {
|
||||
await this.flattenAndArchive(user, folderId, projectId);
|
||||
await this.folderRepository.delete({ id: folderId });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { User, WorkflowEntity, ListQueryDb } from '@n8n/db';
|
||||
import {
|
||||
SharedWorkflow,
|
||||
ExecutionRepository,
|
||||
FolderRepository,
|
||||
WorkflowTagMappingRepository,
|
||||
SharedWorkflowRepository,
|
||||
} from '@n8n/db';
|
||||
@@ -23,6 +24,7 @@ import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import config from '@/config';
|
||||
import type { WorkflowFolderUnionFull } from '@/databases/repositories/workflow.repository';
|
||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||
import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { EventService } from '@/events/event.service';
|
||||
@@ -30,7 +32,6 @@ import { ExternalHooks } from '@/external-hooks';
|
||||
import { validateEntity } from '@/generic-helpers';
|
||||
import type { ListQuery } from '@/requests';
|
||||
import { hasSharing } from '@/requests';
|
||||
import { FolderService } from '@/services/folder.service';
|
||||
import { OwnershipService } from '@/services/ownership.service';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
@@ -60,7 +61,7 @@ export class WorkflowService {
|
||||
private readonly executionRepository: ExecutionRepository,
|
||||
private readonly eventService: EventService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly folderService: FolderService,
|
||||
private readonly folderRepository: FolderRepository,
|
||||
private readonly workflowFinderService: WorkflowFinderService,
|
||||
) {}
|
||||
|
||||
@@ -301,7 +302,14 @@ export class WorkflowService {
|
||||
if (parentFolderId) {
|
||||
const project = await this.sharedWorkflowRepository.getWorkflowOwningProject(workflow.id);
|
||||
if (parentFolderId !== PROJECT_ROOT) {
|
||||
await this.folderService.findFolderInProjectOrFail(parentFolderId, project?.id ?? '');
|
||||
try {
|
||||
await this.folderRepository.findOneOrFailFolderInProject(
|
||||
parentFolderId,
|
||||
project?.id ?? '',
|
||||
);
|
||||
} catch (e) {
|
||||
throw new FolderNotFoundError(parentFolderId);
|
||||
}
|
||||
}
|
||||
updatePayload.parentFolder = parentFolderId === PROJECT_ROOT ? null : { id: parentFolderId };
|
||||
}
|
||||
@@ -417,7 +425,11 @@ export class WorkflowService {
|
||||
return workflow;
|
||||
}
|
||||
|
||||
async archive(user: User, workflowId: string): Promise<WorkflowEntity | undefined> {
|
||||
async archive(
|
||||
user: User,
|
||||
workflowId: string,
|
||||
skipArchived: boolean = false,
|
||||
): Promise<WorkflowEntity | undefined> {
|
||||
const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [
|
||||
'workflow:delete',
|
||||
]);
|
||||
@@ -427,6 +439,10 @@ export class WorkflowService {
|
||||
}
|
||||
|
||||
if (workflow.isArchived) {
|
||||
if (skipArchived) {
|
||||
return workflow;
|
||||
}
|
||||
|
||||
throw new BadRequestError('Workflow is already archived.');
|
||||
}
|
||||
|
||||
|
||||
@@ -757,7 +757,7 @@ describe('DELETE /projects/:projectId/folders/:folderId', () => {
|
||||
expect(folderInDb).toBeNull();
|
||||
});
|
||||
|
||||
test('should delete folder, all child folders, and contained workflows when no transfer folder is specified', async () => {
|
||||
test('should delete folder, all child folders, and archive and move contained workflows to project root when no transfer folder is specified', async () => {
|
||||
const project = await createTeamProject('test', owner);
|
||||
const rootFolder = await createFolder(project, { name: 'Root' });
|
||||
const childFolder = await createFolder(project, {
|
||||
@@ -766,9 +766,8 @@ describe('DELETE /projects/:projectId/folders/:folderId', () => {
|
||||
});
|
||||
|
||||
// Create workflows in the folders
|
||||
const workflow1 = await createWorkflow({ parentFolder: rootFolder }, owner);
|
||||
|
||||
const workflow2 = await createWorkflow({ parentFolder: childFolder }, owner);
|
||||
const workflow1 = await createWorkflow({ parentFolder: rootFolder, active: false }, owner);
|
||||
const workflow2 = await createWorkflow({ parentFolder: childFolder, active: true }, owner);
|
||||
|
||||
await authOwnerAgent.delete(`/projects/${project.id}/folders/${rootFolder.id}`);
|
||||
|
||||
@@ -780,10 +779,24 @@ describe('DELETE /projects/:projectId/folders/:folderId', () => {
|
||||
expect(childFolderInDb).toBeNull();
|
||||
|
||||
// Check workflows
|
||||
const workflow1InDb = await workflowRepository.findOneBy({ id: workflow1.id });
|
||||
const workflow2InDb = await workflowRepository.findOneBy({ id: workflow2.id });
|
||||
expect(workflow1InDb).toBeNull();
|
||||
expect(workflow2InDb).toBeNull();
|
||||
|
||||
const workflow1InDb = await workflowRepository.findOne({
|
||||
where: { id: workflow1.id },
|
||||
relations: ['parentFolder'],
|
||||
});
|
||||
expect(workflow1InDb).not.toBeNull();
|
||||
expect(workflow1InDb?.isArchived).toBe(true);
|
||||
expect(workflow1InDb?.parentFolder).toBe(null);
|
||||
expect(workflow1InDb?.active).toBe(false);
|
||||
|
||||
const workflow2InDb = await workflowRepository.findOne({
|
||||
where: { id: workflow2.id },
|
||||
relations: ['parentFolder'],
|
||||
});
|
||||
expect(workflow2InDb).not.toBeNull();
|
||||
expect(workflow2InDb?.isArchived).toBe(true);
|
||||
expect(workflow2InDb?.parentFolder).toBe(null);
|
||||
expect(workflow2InDb?.active).toBe(false);
|
||||
});
|
||||
|
||||
test('should transfer folder contents when transferToFolderId is specified', async () => {
|
||||
|
||||
@@ -949,7 +949,7 @@
|
||||
"folder.count": "the {count} folder | the {count} folders",
|
||||
"workflow.count": "the {count} workflow | the {count} workflows",
|
||||
"folder.and.workflow.separator": "and",
|
||||
"folders.delete.action": "Delete all workflows and subfolders",
|
||||
"folders.delete.action": "Archive all workflows and delete subfolders",
|
||||
"folders.delete.error.message": "Problem while deleting folder",
|
||||
"folders.delete.confirmation.message": "Type \"delete {folderName}\" to confirm",
|
||||
"folders.transfer.confirm.message": "Data transferred to \"{folderName}\"",
|
||||
|
||||
Reference in New Issue
Block a user