diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx index 9604b35..f1da956 100644 --- a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -32,7 +32,6 @@ describe("ImageDialog Component", () => { render( @@ -50,7 +49,6 @@ describe("ImageDialog Component", () => { render( @@ -68,7 +66,6 @@ describe("ImageDialog Component", () => { render( @@ -85,32 +82,11 @@ describe("ImageDialog Component", () => { expect(screen.getByText("Copié!")).toBeInTheDocument(); }); - test("shows edit field when admin clicks edit button", async () => { - await act(async () => { - render( - - ); - }); - - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); - - const editButton = screen.getByTestId("edit-button-1"); - fireEvent.click(editButton); - - expect(screen.getByDisplayValue("image1.jpg")).toBeInTheDocument(); - }); - test("navigates to next and previous page", async () => { await act(async () => { render( @@ -127,4 +103,48 @@ describe("ImageDialog Component", () => { await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); }); + + test("deletes an image successfully", async () => { + jest.spyOn(ApiService, "deleteImage").mockResolvedValue(true); + + await act(async () => { + render( + + ); + }); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); + + fireEvent.click(screen.getByTestId("delete-button-1")); + + await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1")); + + expect(screen.queryByTestId("delete-button-1")).not.toBeInTheDocument(); + }); + + test("handles failed delete when image is linked", async () => { + jest.spyOn(ApiService, "deleteImage").mockResolvedValue(false); + + await act(async () => { + render( + + ); + }); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); + + fireEvent.click(screen.getByTestId("delete-button-1")); + + await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1")); + + expect(screen.getByText("Confirmer la suppression")).toBeInTheDocument(); + }); }); \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 069a846..60dfc14 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -12,31 +12,30 @@ import { TableRow, IconButton, Paper, - TextField, - Box + Box, + CircularProgress } from "@mui/material"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import CloseIcon from "@mui/icons-material/Close"; -import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; import { Images } from "../../Types/Images"; import ApiService from '../../services/ApiService'; import { ENV_VARIABLES } from '../../constants'; type Props = { galleryOpen: boolean; - admin: boolean; setDialogOpen: React.Dispatch>; setImageLinks: React.Dispatch>; -} - -const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setImageLinks }) => { +}; +const ImageDialog: React.FC = ({ galleryOpen, setDialogOpen, setImageLinks }) => { const [copiedId, setCopiedId] = useState(null); - const [editingId, setEditingId] = useState(null); const [images, setImages] = useState([]); const [totalImg, setTotalImg] = useState(0); const [imgPage, setImgPage] = useState(1); const [imgLimit] = useState(3); + const [loading, setLoading] = useState(false); + const [deleteConfirm, setDeleteConfirm] = useState<{ id: string | null; linked: boolean }>({ id: null, linked: false }); const fetchImages = async (page: number, limit: number) => { const data = await ApiService.getImages(page, limit); @@ -46,37 +45,50 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm useEffect(() => { fetchImages(imgPage, imgLimit); - }, [imgPage]); // Re-fetch images when page changes - - const handleEditClick = (id: string) => { - setEditingId(id === editingId ? null : id); - }; + }, [imgPage]); const onCopy = (id: string) => { const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; - console.log(escLink); setCopiedId(id); setImageLinks(prevLinks => [...prevLinks, escLink]); }; - const handleNextPage = () => { - if ((imgPage * imgLimit) < totalImg) { - setImgPage(prev => prev + 1); - } + const handleDelete = async (id: string) => { + setLoading(true); + const isDeleted = await ApiService.deleteImage(id); + setLoading(false); + if (!isDeleted) { + setDeleteConfirm({ id, linked: true }); + } else { + setImages(images.filter(image => image.id !== id)); + setDeleteConfirm({ id: null, linked: false }); + } }; - + + const confirmDelete = async () => { + if (deleteConfirm.id) { + setLoading(true); + await ApiService.deleteImage(deleteConfirm.id); + setImages(images.filter(image => image.id !== deleteConfirm.id)); + setDeleteConfirm({ id: null, linked: false }); + setLoading(false); + } + }; + + const handleNextPage = () => { + if ((imgPage * imgLimit) < totalImg) { + setImgPage(prev => prev + 1); + } + }; + const handlePrevPage = () => { - if (imgPage > 1) { - setImgPage(prev => prev - 1); - } + if (imgPage > 1) { + setImgPage(prev => prev - 1); + } }; return ( - setDialogOpen(false)} - maxWidth="xl" // 'md' stands for medium size - > + setDialogOpen(false)} maxWidth="xl"> Images disponibles = ({ galleryOpen, admin, setDialogOpen, setIm - - - - {images.map((obj: Images) => ( - - - {`Image - - - {admin && editingId === obj.id ? ( - - ) : ( - obj.file_name - )} - - - {obj.id} - onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> - - - {admin && ( - <> - handleEditClick(obj.id)} size="small" color="primary" data-testid={`edit-button-${obj.id}`}> - - - - )} - {copiedId === obj.id && Copié!} - - - ))} - -
-
-
- - - - + {loading ? ( + + - + ) : ( + + + + {images.map((obj: Images) => ( + + + {`Image + + {obj.file_name} + + {obj.id} + onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> + + + handleDelete(obj.id)} size="small" color="secondary" data-testid={`delete-button-${obj.id}`}> + + + {copiedId === obj.id && Copié!} + + + ))} + +
+
+ )} + + {deleteConfirm.linked && ( + setDeleteConfirm({ id: null, linked: false })}> + Confirmer la suppression + + Cette image est liée à d'autres objets. Êtes-vous sûr de vouloir la supprimer ? + + + + + + + )} + + + + + +
); }; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 0984652..b3b73ed 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -1247,6 +1247,36 @@ public async login(email: string, password: string): Promise { } } + public async deleteImage(imgId: string): Promise { + try { + const url: string = this.constructRequestUrl(`/image/delete`); + const headers = this.constructRequestHeaders(); + const uid = this.getUserID(); + let params = { uid: uid, imgId: imgId }; + + const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`); + } + const deleted = result.data.delete; + + return deleted; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + } const apiService = new ApiService(); diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 671ebe8..85b49fd 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -88,7 +88,8 @@ describe('Images', () => { insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), findOne: jest.fn(), find: jest.fn().mockReturnValue(mockFindCursor), - countDocuments: jest.fn() + countDocuments: jest.fn(), + deleteOne: jest.fn(), }; dbConn = { @@ -243,4 +244,62 @@ describe('Images', () => { expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); }); }); + + describe('delete', () => { + + it('should delete the image when it exists', async () => { + const uid = 'user123'; + const imgId = 'img123'; + + // Simulate the image being found in the collection + mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + const result = await images.delete(uid, imgId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(mockImagesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + expect(mockImagesCollection.deleteOne).toHaveBeenCalledWith({ _id: imgId }); + expect(result).toEqual({ deleted: true }); + }); + + it('should not delete the image when it does not exist', async () => { + const uid = 'user123'; + const imgId = 'img123'; + + // Simulate the image not being found in the collection + mockImagesCollection.find.mockResolvedValue([]); + + const result = await images.delete(uid, imgId); + + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + expect(mockImagesCollection.deleteOne).toHaveBeenCalled(); + expect(result).toEqual({ deleted: false }); + }); + + it('should return false if the delete operation fails', async () => { + const uid = 'user123'; + const imgId = 'img123'; + + // Simulate the image being found, but the delete operation failing + mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]); + + const result = await images.delete(uid, imgId); + + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + expect(result).toEqual({ deleted: false }); + }); + }); }); \ No newline at end of file diff --git a/server/controllers/images.js b/server/controllers/images.js index e0ac12c..7de02ad 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -85,6 +85,23 @@ class ImagesController { } }; + delete = async (req, res, next) => { + try { + const uid = req.query.uid; + const imgId = req.query.imgId; + + if (!uid || !imgId) { + throw new AppError(MISSING_REQUIRED_PARAMETER); + } + const images = await this.images.delete(uid, imgId); + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + } module.exports = ImagesController; diff --git a/server/models/images.js b/server/models/images.js index 036ce1e..1dc4912 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -102,6 +102,24 @@ class Images { return respObj; } + + async delete(uid, imgId) { + let resp = false; + await this.db.connect() + const conn = this.db.getConnection(); + const imgsColl = conn.collection('files'); + const rgxImg = new RegExp(`/api/image/get/${imgId}`); + + const result = await imgsColl.find({ userId: uid, content: { $regex: rgxImg }}); + + if(result){ + const isDeleted = await imgsColl.deleteOne({ _id: imgId }); + if(isDeleted){ + resp = true; + } + } + return { deleted: resp }; + } } module.exports = Images; diff --git a/server/routers/images.js b/server/routers/images.js index 40aa481..2be24d2 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -14,5 +14,6 @@ router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(im router.get("/get/:id", asyncHandler(images.get)); router.get("/getImages", asyncHandler(images.getImages)); router.get("/getUserImages", asyncHandler(images.getUserImages)); +router.get("/delete", asyncHandler(images.delete)); module.exports = router;