From 80d94699bf1847b19c88cfd05b682b50cb4db849 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 6 Feb 2025 11:26:26 +0100 Subject: [PATCH 1/3] fix: add label bulk delete --- .../chat/controllers/label.controller.spec.ts | 31 +++++++++++++- api/src/chat/controllers/label.controller.ts | 27 ++++++++++++ frontend/src/components/labels/index.tsx | 41 ++++++++++++++++--- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/api/src/chat/controllers/label.controller.spec.ts b/api/src/chat/controllers/label.controller.spec.ts index 0c5fae41..9499d425 100644 --- a/api/src/chat/controllers/label.controller.spec.ts +++ b/api/src/chat/controllers/label.controller.spec.ts @@ -6,7 +6,7 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { NotFoundException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { MongooseModule } from '@nestjs/mongoose'; import { Test } from '@nestjs/testing'; @@ -22,6 +22,7 @@ import { RoleModel } from '@/user/schemas/role.schema'; import { UserModel } from '@/user/schemas/user.schema'; import { RoleService } from '@/user/services/role.service'; import { UserService } from '@/user/services/user.service'; +import { NOT_FOUND_ID } from '@/utils/constants/mock'; import { IGNORED_TEST_FIELDS } from '@/utils/test/constants'; import { getUpdateOneError } from '@/utils/test/errors/messages'; import { labelFixtures } from '@/utils/test/fixtures/label'; @@ -203,6 +204,34 @@ describe('LabelController', () => { }); }); + describe('deleteMany', () => { + it('should delete multiple labels', async () => { + const valuesToDelete = [label.id, labelToDelete.id]; + + const result = await labelController.deleteMany(valuesToDelete); + + expect(result.deletedCount).toEqual(valuesToDelete.length); + const remainingValues = await labelService.find({ + _id: { $in: valuesToDelete }, + }); + expect(remainingValues.length).toBe(0); + }); + + it('should throw BadRequestException when no IDs are provided', async () => { + await expect(labelController.deleteMany([])).rejects.toThrow( + BadRequestException, + ); + }); + + it('should throw NotFoundException when provided IDs do not exist', async () => { + const nonExistentIds = [NOT_FOUND_ID, NOT_FOUND_ID.replace(/9/g, '8')]; + + await expect(labelController.deleteMany(nonExistentIds)).rejects.toThrow( + NotFoundException, + ); + }); + }); + describe('updateOne', () => { const labelUpdateDto: LabelUpdateDto = { description: 'test description 1', diff --git a/api/src/chat/controllers/label.controller.ts b/api/src/chat/controllers/label.controller.ts index c3e61106..b02d796c 100644 --- a/api/src/chat/controllers/label.controller.ts +++ b/api/src/chat/controllers/label.controller.ts @@ -7,6 +7,7 @@ */ import { + BadRequestException, Body, Controller, Delete, @@ -24,6 +25,7 @@ import { CsrfCheck } from '@tekuconcept/nestjs-csrf'; import { CsrfInterceptor } from '@/interceptors/csrf.interceptor'; import { LoggerService } from '@/logger/logger.service'; import { BaseController } from '@/utils/generics/base-controller'; +import { DeleteResult } from '@/utils/generics/base-repository'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe'; import { PopulatePipe } from '@/utils/pipes/populate.pipe'; @@ -125,4 +127,29 @@ export class LabelController extends BaseController< } return result; } + + /** + * Deletes multiple Labels by their IDs. + * @param ids - IDs of Labels to be deleted. + * @returns A Promise that resolves to the deletion result. + */ + @CsrfCheck(true) + @Delete('') + @HttpCode(204) + async deleteMany(@Body('ids') ids: string[]): Promise { + if (!ids || ids.length === 0) { + throw new BadRequestException('No IDs provided for deletion.'); + } + const deleteResult = await this.labelService.deleteMany({ + _id: { $in: ids }, + }); + + if (deleteResult.deletedCount === 0) { + this.logger.warn(`Unable to delete Labels with provided IDs: ${ids}`); + throw new NotFoundException('Labels with provided IDs not found'); + } + + this.logger.log(`Successfully deleted Labels with IDs: ${ids}`); + return deleteResult; + } } diff --git a/frontend/src/components/labels/index.tsx b/frontend/src/components/labels/index.tsx index 6eebf7b5..e719e92d 100644 --- a/frontend/src/components/labels/index.tsx +++ b/frontend/src/components/labels/index.tsx @@ -8,8 +8,10 @@ import { faTags } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; +import DeleteIcon from "@mui/icons-material/Delete"; import { Button, Grid, Paper } from "@mui/material"; -import { GridColDef } from "@mui/x-data-grid"; +import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; +import { useState } from "react"; import { ConfirmDialogBody } from "@/app-components/dialogs"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; @@ -20,6 +22,7 @@ import { import { renderHeader } from "@/app-components/tables/columns/renderHeader"; import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; +import { useDeleteMany } from "@/hooks/crud/useDeleteMany"; import { useFind } from "@/hooks/crud/useFind"; import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; @@ -48,14 +51,16 @@ export const Labels = () => { params: searchPayload, }, ); - const { mutate: deleteLabel } = useDelete(EntityType.LABEL, { + const options = { onError: () => { toast.error(t("message.internal_server_error")); }, onSuccess() { toast.success(t("message.item_delete_success")); }, - }); + }; + const { mutate: deleteLabel } = useDelete(EntityType.LABEL, options); + const { mutate: deleteLabels } = useDeleteMany(EntityType.LABEL, options); const actionColumns = useActionColumns( EntityType.LABEL, [ @@ -78,6 +83,7 @@ export const Labels = () => { ], t("label.operations"), ); + const [selectedLabels, setSelectedLabels] = useState([]); const columns: GridColDef[] = [ { field: "id", headerName: "ID" }, { @@ -123,7 +129,6 @@ export const Labels = () => { headerAlign: "left", }, - { minWidth: 140, field: "createdAt", @@ -148,6 +153,9 @@ export const Labels = () => { }, actionColumns, ]; + const handleSelectionChange = (selection: GridRowSelectionModel) => { + setSelectedLabels(selection as string[]); + }; return ( @@ -175,12 +183,35 @@ export const Labels = () => { ) : null} + - + From 49f0a9e1cf3bd5bfb5e359e825f67dc75175400d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 6 Feb 2025 11:48:03 +0100 Subject: [PATCH 2/3] fix: label unit tests --- .../chat/controllers/label.controller.spec.ts | 60 ++++++++++--------- api/src/utils/test/fixtures/label.ts | 11 ++++ 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/api/src/chat/controllers/label.controller.spec.ts b/api/src/chat/controllers/label.controller.spec.ts index 9499d425..4f6cdfa0 100644 --- a/api/src/chat/controllers/label.controller.spec.ts +++ b/api/src/chat/controllers/label.controller.spec.ts @@ -49,6 +49,7 @@ describe('LabelController', () => { let labelService: LabelService; let label: Label; let labelToDelete: Label; + let secondLabelToDelete: Label; let subscriberService: SubscriberService; beforeAll(async () => { @@ -88,6 +89,9 @@ describe('LabelController', () => { labelToDelete = (await labelService.findOne({ name: 'TEST_TITLE_2', })) as Label; + secondLabelToDelete = (await labelService.findOne({ + name: 'TEST_TITLE_3', + })) as Label; }); afterEach(jest.clearAllMocks); @@ -204,34 +208,6 @@ describe('LabelController', () => { }); }); - describe('deleteMany', () => { - it('should delete multiple labels', async () => { - const valuesToDelete = [label.id, labelToDelete.id]; - - const result = await labelController.deleteMany(valuesToDelete); - - expect(result.deletedCount).toEqual(valuesToDelete.length); - const remainingValues = await labelService.find({ - _id: { $in: valuesToDelete }, - }); - expect(remainingValues.length).toBe(0); - }); - - it('should throw BadRequestException when no IDs are provided', async () => { - await expect(labelController.deleteMany([])).rejects.toThrow( - BadRequestException, - ); - }); - - it('should throw NotFoundException when provided IDs do not exist', async () => { - const nonExistentIds = [NOT_FOUND_ID, NOT_FOUND_ID.replace(/9/g, '8')]; - - await expect(labelController.deleteMany(nonExistentIds)).rejects.toThrow( - NotFoundException, - ); - }); - }); - describe('updateOne', () => { const labelUpdateDto: LabelUpdateDto = { description: 'test description 1', @@ -259,4 +235,32 @@ describe('LabelController', () => { ).rejects.toThrow(getUpdateOneError(Label.name, labelToDelete.id)); }); }); + + describe('deleteMany', () => { + it('should delete multiple labels', async () => { + const valuesToDelete = [label.id, secondLabelToDelete.id]; + + const result = await labelController.deleteMany(valuesToDelete); + + expect(result.deletedCount).toEqual(valuesToDelete.length); + const remainingValues = await labelService.find({ + _id: { $in: valuesToDelete }, + }); + expect(remainingValues.length).toBe(0); + }); + + it('should throw BadRequestException when no IDs are provided', async () => { + await expect(labelController.deleteMany([])).rejects.toThrow( + BadRequestException, + ); + }); + + it('should throw NotFoundException when provided IDs do not exist', async () => { + const nonExistentIds = [NOT_FOUND_ID, NOT_FOUND_ID.replace(/9/g, '8')]; + + await expect(labelController.deleteMany(nonExistentIds)).rejects.toThrow( + NotFoundException, + ); + }); + }); }); diff --git a/api/src/utils/test/fixtures/label.ts b/api/src/utils/test/fixtures/label.ts index 7621272a..3a21369d 100644 --- a/api/src/utils/test/fixtures/label.ts +++ b/api/src/utils/test/fixtures/label.ts @@ -43,6 +43,17 @@ export const labels: TLabelFixtures['values'][] = [ name: 'TEST_TITLE_2', title: 'test title 2', }, + { + description: 'test description 3', + label_id: { + messenger: 'messenger', + web: 'web', + twitter: 'twitter', + dimelo: 'dimelo', + }, + name: 'TEST_TITLE_3', + title: 'test title 3', + }, ]; export const labelFixtures = getFixturesWithDefaultValues< From 44bb8b08e4a9fb2bf1b3f59e1c843f60d48cce52 Mon Sep 17 00:00:00 2001 From: Yassine Sallemi Date: Wed, 19 Feb 2025 22:02:52 +0100 Subject: [PATCH 3/3] fix: labels cascade delete --- api/src/chat/repositories/label.repository.ts | 10 +++++-- api/src/chat/services/block.service.ts | 30 +++++++++++++++++++ api/src/chat/services/subscriber.service.ts | 21 +++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/api/src/chat/repositories/label.repository.ts b/api/src/chat/repositories/label.repository.ts index 655ec6d0..af7af401 100644 --- a/api/src/chat/repositories/label.repository.ts +++ b/api/src/chat/repositories/label.repository.ts @@ -82,9 +82,13 @@ export class LabelRepository extends BaseRepository< >, _criteria: TFilterQuery