From 6b57c5738fd3bca89f0884d84edb482b4087ee54 Mon Sep 17 00:00:00 2001 From: hexastack Date: Thu, 10 Oct 2024 17:51:30 +0100 Subject: [PATCH] feat: add bulk delete --- .../controllers/nlp-value.controller.spec.ts | 34 +++++++++++- .../nlp/controllers/nlp-value.controller.ts | 27 ++++++++++ .../components/nlp/components/NlpSample.tsx | 2 +- .../components/nlp/components/NlpValues.tsx | 52 ++++++++++++++++--- 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/api/src/nlp/controllers/nlp-value.controller.spec.ts b/api/src/nlp/controllers/nlp-value.controller.spec.ts index e1f256df..609cff89 100644 --- a/api/src/nlp/controllers/nlp-value.controller.spec.ts +++ b/api/src/nlp/controllers/nlp-value.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, TestingModule } from '@nestjs/testing'; @@ -40,6 +40,7 @@ describe('NlpValueController', () => { let nlpEntityService: NlpEntityService; let jhonNlpValue: NlpValue; let positiveValue: NlpValue; + let negativeValue: NlpValue; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -67,6 +68,7 @@ describe('NlpValueController', () => { nlpEntityService = module.get(NlpEntityService); jhonNlpValue = await nlpValueService.findOne({ value: 'jhon' }); positiveValue = await nlpValueService.findOne({ value: 'positive' }); + negativeValue = await nlpValueService.findOne({ value: 'negative' }); }); afterAll(async () => { await closeInMongodConnection(); @@ -228,4 +230,34 @@ describe('NlpValueController', () => { ).rejects.toThrow(NotFoundException); }); }); + describe('deleteMany', () => { + it('should delete multiple nlp values', async () => { + const valuesToDelete = [positiveValue.id, negativeValue.id]; + + const result = await nlpValueController.deleteMany(valuesToDelete); + + expect(result.deletedCount).toEqual(valuesToDelete.length); + const remainingValues = await nlpValueService.find({ + _id: { $in: valuesToDelete }, + }); + expect(remainingValues.length).toBe(0); + }); + + it('should throw BadRequestException when no IDs are provided', async () => { + await expect(nlpValueController.deleteMany([])).rejects.toThrow( + BadRequestException, + ); + }); + + it('should throw NotFoundException when provided IDs do not exist', async () => { + const nonExistentIds = [ + '614c1b2f58f4f04c876d6b8d', + '614c1b2f58f4f04c876d6b8e', + ]; + + await expect( + nlpValueController.deleteMany(nonExistentIds), + ).rejects.toThrow(NotFoundException); + }); + }); }); diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index 4fee62f4..d0773a50 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -18,6 +18,7 @@ import { Query, NotFoundException, UseInterceptors, + BadRequestException, } from '@nestjs/common'; import { CsrfCheck } from '@tekuconcept/nestjs-csrf'; import { TFilterQuery } from 'mongoose'; @@ -25,6 +26,7 @@ import { TFilterQuery } from 'mongoose'; 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'; @@ -193,4 +195,29 @@ export class NlpValueController extends BaseController< } return result; } + + /** + * Deletes multiple NLP values by their IDs. + * @param ids - IDs of NLP values 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.nlpValueService.deleteMany({ + _id: { $in: ids }, + }); + + if (deleteResult.deletedCount === 0) { + this.logger.warn(`Unable to delete NLP values with provided IDs: ${ids}`); + throw new NotFoundException('NLP values with provided IDs not found'); + } + + this.logger.log(`Successfully deleted NLP values with IDs: ${ids}`); + return deleteResult; + } } diff --git a/frontend/src/components/nlp/components/NlpSample.tsx b/frontend/src/components/nlp/components/NlpSample.tsx index 160c6832..19cbfc8c 100644 --- a/frontend/src/components/nlp/components/NlpSample.tsx +++ b/frontend/src/components/nlp/components/NlpSample.tsx @@ -7,7 +7,7 @@ */ import CircleIcon from "@mui/icons-material/Circle"; -import DeleteIcon from "@mui/icons-material/Close"; +import DeleteIcon from "@mui/icons-material/Delete"; import DownloadIcon from "@mui/icons-material/Download"; import UploadIcon from "@mui/icons-material/Upload"; import { diff --git a/frontend/src/components/nlp/components/NlpValues.tsx b/frontend/src/components/nlp/components/NlpValues.tsx index 7996654b..74729578 100644 --- a/frontend/src/components/nlp/components/NlpValues.tsx +++ b/frontend/src/components/nlp/components/NlpValues.tsx @@ -9,8 +9,9 @@ import { faGraduationCap } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import { Box, Button, Chip, Grid, Slide } from "@mui/material"; -import { GridColDef } from "@mui/x-data-grid"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { Box, Button, ButtonGroup, Chip, Grid, Slide } from "@mui/material"; +import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -23,6 +24,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 { useGet } from "@/hooks/crud/useGet"; import { useDialog } from "@/hooks/useDialog"; @@ -72,6 +74,17 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { refetchEntity(); }, }); + const { mutateAsync: deleteNlpValues } = useDeleteMany(EntityType.NLP_VALUE, { + onError: (error) => { + toast.error(error); + }, + onSuccess: () => { + deleteEntityDialogCtl.closeDialog(); + setSelectedNlpValues([]); + toast.success(t("message.item_delete_success")); + }, + }); + const [selectedNlpValues, setSelectedNlpValues] = useState([]); const actionColumns = useActionColumns( EntityType.NLP_VALUE, [ @@ -138,6 +151,9 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { }, []); const canHaveSynonyms = nlpEntity?.lookups?.[0] === NlpLookups.keywords; + const handleSelectionChange = (selection: GridRowSelectionModel) => { + setSelectedNlpValues(selection as string[]); + }; return ( @@ -174,7 +190,7 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { - + {hasPermission( EntityType.NLP_VALUE, PermissionAction.CREATE, @@ -188,7 +204,21 @@ export const NlpValues = ({ entityId }: { entityId: string }) => { {t("button.add")} ) : null} - + {selectedNlpValues.length > 0 && ( + + + + )} + { { - if (deleteEntityDialogCtl.data) + if (selectedNlpValues.length > 0) { + deleteNlpValues(selectedNlpValues); + setSelectedNlpValues([]); + deleteEntityDialogCtl.closeDialog(); + } else if (deleteEntityDialogCtl.data) { deleteNlpValue(deleteEntityDialogCtl.data); + } }} /> { callback={() => {}} /> - +