fix(editor): Correct WF counts on folder with archived filter (no-changelog) (#15195)

This commit is contained in:
Jaakko Husso
2025-05-10 11:38:36 +03:00
committed by GitHub
parent 403f08b6e3
commit 15e62e6dfa
6 changed files with 145 additions and 21 deletions

View File

@@ -29,7 +29,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
): SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount> { ): SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount> {
const query = this.createQueryBuilder('folder'); const query = this.createQueryBuilder('folder');
this.applySelections(query, options.select); this.applySelections(query, options.select, options.filter);
this.applyFilters(query, options.filter); this.applyFilters(query, options.filter);
this.applySorting(query, options.sortBy); this.applySorting(query, options.sortBy);
this.applyPagination(query, options); this.applyPagination(query, options);
@@ -39,21 +39,41 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
private applySelections( private applySelections(
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
select?: Record<string, boolean>, select?: ListQuery.Options['select'],
filter?: ListQuery.Options['filter'],
): void { ): void {
if (select) { if (select) {
this.applyCustomSelect(query, select); this.applyCustomSelect(query, select, filter);
} else { } else {
this.applyDefaultSelect(query); this.applyDefaultSelect(query, filter);
} }
} }
private applyDefaultSelect(query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>): void { private applyWorkflowCountSelect(
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
filter?: ListQuery.Options['filter'],
): void {
if (typeof filter?.isArchived === 'boolean') {
query.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows', 'workflow', (qb) =>
qb.andWhere('workflow.isArchived = :isArchived', {
isArchived: filter.isArchived,
}),
);
} else {
query.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows');
}
}
private applyDefaultSelect(
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
filter?: ListQuery.Options['filter'],
): void {
this.applyWorkflowCountSelect(query, filter);
query query
.leftJoinAndSelect('folder.homeProject', 'homeProject') .leftJoinAndSelect('folder.homeProject', 'homeProject')
.leftJoinAndSelect('folder.parentFolder', 'parentFolder') .leftJoinAndSelect('folder.parentFolder', 'parentFolder')
.leftJoinAndSelect('folder.tags', 'tags') .leftJoinAndSelect('folder.tags', 'tags')
.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows')
.loadRelationCountAndMap('folder.subFolderCount', 'folder.subFolders') .loadRelationCountAndMap('folder.subFolderCount', 'folder.subFolders')
.select([ .select([
'folder', 'folder',
@@ -65,17 +85,18 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
private applyCustomSelect( private applyCustomSelect(
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
select?: Record<string, boolean>, select?: ListQuery.Options['select'],
filter?: ListQuery.Options['filter'],
): void { ): void {
const selections = ['folder.id']; const selections = ['folder.id'];
this.addBasicFields(selections, select); this.addBasicFields(selections, select);
this.addRelationFields(query, selections, select); this.addRelationFields(query, selections, select, filter);
query.select(selections); query.select(selections);
} }
private addBasicFields(selections: string[], select?: Record<string, boolean>): void { private addBasicFields(selections: string[], select?: ListQuery.Options['select']): void {
if (select?.name) selections.push('folder.name'); if (select?.name) selections.push('folder.name');
if (select?.createdAt) selections.push('folder.createdAt'); if (select?.createdAt) selections.push('folder.createdAt');
if (select?.updatedAt) selections.push('folder.updatedAt'); if (select?.updatedAt) selections.push('folder.updatedAt');
@@ -84,7 +105,8 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
private addRelationFields( private addRelationFields(
query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>, query: SelectQueryBuilder<FolderWithWorkflowAndSubFolderCount>,
selections: string[], selections: string[],
select?: Record<string, boolean>, select?: ListQuery.Options['select'],
filter?: ListQuery.Options['filter'],
): void { ): void {
if (select?.project) { if (select?.project) {
query.leftJoin('folder.homeProject', 'homeProject'); query.leftJoin('folder.homeProject', 'homeProject');
@@ -102,7 +124,7 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
} }
if (select?.workflowCount) { if (select?.workflowCount) {
query.loadRelationCountAndMap('folder.workflowCount', 'folder.workflows'); this.applyWorkflowCountSelect(query, filter);
} }
if (select?.subFolderCount) { if (select?.subFolderCount) {

View File

@@ -25,7 +25,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import type { ListQuery } from '@/requests';
import { FolderService } from '@/services/folder.service'; import { FolderService } from '@/services/folder.service';
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
@@ -124,10 +123,7 @@ export class ProjectController {
) { ) {
const { projectId } = req.params; const { projectId } = req.params;
const [data, count] = await this.folderService.getManyAndCount( const [data, count] = await this.folderService.getManyAndCount(projectId, payload);
projectId,
payload as ListQuery.Options,
);
res.json({ count, data }); res.json({ count, data });
} }

View File

@@ -242,6 +242,83 @@ describe('FolderRepository', () => {
expect(folders[0].parentFolder?.id).toBe(parentFolder.id); expect(folders[0].parentFolder?.id).toBe(parentFolder.id);
expect(folders[0].tags[0].name).toBe('important'); expect(folders[0].tags[0].name).toBe('important');
}); });
describe('workflowCount', () => {
let testFolder: Folder;
beforeEach(async () => {
const parentFolder = await createFolder(project, { name: 'Parent' });
testFolder = await createFolder(project, { name: 'Test Folder', parentFolder });
await createWorkflow({ parentFolder: testFolder, isArchived: false });
await createWorkflow({ parentFolder: testFolder, isArchived: false });
await createWorkflow({ parentFolder: testFolder, isArchived: true });
await createWorkflow({ parentFolder: testFolder, isArchived: true });
await createWorkflow({ parentFolder: testFolder, isArchived: true });
});
it('should include archived workflows in the workflow count by default', async () => {
const [folders] = await folderRepository.getManyAndCount({
select: { workflowCount: true },
});
expect(folders).toHaveLength(2);
expect(folders).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: testFolder.id,
workflowCount: 5,
}),
expect.objectContaining({
id: testFolder.parentFolderId,
workflowCount: 0,
}),
]),
);
});
it('should include only archived workflows in the workflow count if filtered', async () => {
const [folders] = await folderRepository.getManyAndCount({
select: { workflowCount: true },
filter: { isArchived: true },
});
expect(folders).toHaveLength(2);
expect(folders).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: testFolder.id,
workflowCount: 3,
}),
expect.objectContaining({
id: testFolder.parentFolderId,
workflowCount: 0,
}),
]),
);
});
it('should return only unarchived workflows in the workflow count if filtered', async () => {
const [folders] = await folderRepository.getManyAndCount({
select: { workflowCount: true },
filter: { isArchived: false },
});
expect(folders).toHaveLength(2);
expect(folders).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: testFolder.id,
workflowCount: 2,
}),
expect.objectContaining({
id: testFolder.parentFolderId,
workflowCount: 0,
}),
]),
);
});
});
}); });
describe('select', () => { describe('select', () => {

View File

@@ -287,7 +287,10 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
this.getWorkflowsAndFoldersCount(workflowIds, options), this.getWorkflowsAndFoldersCount(workflowIds, options),
]); ]);
const { workflows, folders } = await this.fetchExtraData(workflowsAndFolders); const isArchived =
typeof options.filter?.isArchived === 'boolean' ? options.filter.isArchived : undefined;
const { workflows, folders } = await this.fetchExtraData(workflowsAndFolders, isArchived);
const enrichedWorkflowsAndFolders = this.enrichDataWithExtras(workflowsAndFolders, { const enrichedWorkflowsAndFolders = this.enrichDataWithExtras(workflowsAndFolders, {
workflows, workflows,
@@ -323,13 +326,16 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
.map((item) => item.id); .map((item) => item.id);
} }
private async fetchExtraData(workflowsAndFolders: WorkflowFolderUnionRow[]) { private async fetchExtraData(
workflowsAndFolders: WorkflowFolderUnionRow[],
isArchived?: boolean,
) {
const workflowIds = this.getWorkflowsIds(workflowsAndFolders); const workflowIds = this.getWorkflowsIds(workflowsAndFolders);
const folderIds = this.getFolderIds(workflowsAndFolders); const folderIds = this.getFolderIds(workflowsAndFolders);
const [workflows, folders] = await Promise.all([ const [workflows, folders] = await Promise.all([
this.getMany(workflowIds), this.getMany(workflowIds),
this.folderRepository.getMany({ filter: { folderIds } }), this.folderRepository.getMany({ filter: { folderIds, isArchived } }),
]); ]);
return { workflows, folders }; return { workflows, folders };

View File

@@ -253,7 +253,8 @@ export class FolderService {
const workflowCountQuery = this.workflowRepository const workflowCountQuery = this.workflowRepository
.createQueryBuilder('workflow') .createQueryBuilder('workflow')
.select('COUNT(workflow.id)', 'count') .select('COUNT(workflow.id)', 'count')
.where((qb) => { .where('workflow.isArchived = :isArchived', { isArchived: false })
.andWhere((qb) => {
const folderQuery = qb.subQuery().from('folder_path', 'fp').select('fp.id').getQuery(); const folderQuery = qb.subQuery().from('folder_path', 'fp').select('fp.id').getQuery();
return `workflow.parentFolderId IN ${folderQuery}`; return `workflow.parentFolderId IN ${folderQuery}`;
}) })
@@ -279,7 +280,7 @@ export class FolderService {
} }
async getManyAndCount(projectId: string, options: ListQuery.Options) { async getManyAndCount(projectId: string, options: ListQuery.Options) {
options.filter = { ...options.filter, projectId }; options.filter = { ...options.filter, projectId, isArchived: false };
return await this.folderRepository.getManyAndCount(options); return await this.folderRepository.getManyAndCount(options);
} }
} }

View File

@@ -1320,6 +1320,23 @@ describe('GET /projects/:projectId/folders', () => {
['Owner Folder 1', 'Owner Folder 2'].sort(), ['Owner Folder 1', 'Owner Folder 2'].sort(),
); );
}); });
test('should include workflow count', async () => {
const folder = await createFolder(ownerProject, { name: 'Test Folder' });
await createWorkflow({ parentFolder: folder, isArchived: false }, ownerProject);
await createWorkflow({ parentFolder: folder, isArchived: false }, ownerProject);
// Not included in the count
await createWorkflow({ parentFolder: folder, isArchived: true }, ownerProject);
const response = await authOwnerAgent
.get(`/projects/${ownerProject.id}/folders`)
.query({ filter: '{ "name": "test" }' })
.expect(200);
expect(response.body.count).toBe(1);
expect(response.body.data).toHaveLength(1);
expect(response.body.data[0].workflowCount).toEqual(2);
});
}); });
describe('GET /projects/:projectId/folders/content', () => { describe('GET /projects/:projectId/folders/content', () => {
@@ -1371,6 +1388,11 @@ describe('GET /projects/:projectId/folders/content', () => {
await createWorkflow({ parentFolder: personalFolder1 }, ownerProject); await createWorkflow({ parentFolder: personalFolder1 }, ownerProject);
await createWorkflow({ parentFolder: personalProjectSubfolder1 }, ownerProject); await createWorkflow({ parentFolder: personalProjectSubfolder1 }, ownerProject);
await createWorkflow({ parentFolder: personalProjectSubfolder2 }, ownerProject); await createWorkflow({ parentFolder: personalProjectSubfolder2 }, ownerProject);
// Not included in the count
await createWorkflow(
{ parentFolder: personalProjectSubfolder2, isArchived: true },
ownerProject,
);
const response = await authOwnerAgent const response = await authOwnerAgent
.get(`/projects/${ownerProject.id}/folders/${personalFolder1.id}/content`) .get(`/projects/${ownerProject.id}/folders/${personalFolder1.id}/content`)