mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
FIX - ajustement delete image et tests
This commit is contained in:
parent
f4bcfd27b9
commit
b7ec35c0c9
7 changed files with 269 additions and 105 deletions
|
|
@ -32,7 +32,6 @@ describe("ImageDialog Component", () => {
|
|||
render(
|
||||
<ImageDialog
|
||||
galleryOpen={true}
|
||||
admin={false}
|
||||
setDialogOpen={setDialogOpenMock}
|
||||
setImageLinks={setImageLinksMock}
|
||||
/>
|
||||
|
|
@ -50,7 +49,6 @@ describe("ImageDialog Component", () => {
|
|||
render(
|
||||
<ImageDialog
|
||||
galleryOpen={true}
|
||||
admin={false}
|
||||
setDialogOpen={setDialogOpenMock}
|
||||
setImageLinks={setImageLinksMock}
|
||||
/>
|
||||
|
|
@ -68,7 +66,6 @@ describe("ImageDialog Component", () => {
|
|||
render(
|
||||
<ImageDialog
|
||||
galleryOpen={true}
|
||||
admin={false}
|
||||
setDialogOpen={setDialogOpenMock}
|
||||
setImageLinks={setImageLinksMock}
|
||||
/>
|
||||
|
|
@ -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(
|
||||
<ImageDialog
|
||||
galleryOpen={true}
|
||||
admin={true}
|
||||
setDialogOpen={setDialogOpenMock}
|
||||
setImageLinks={setImageLinksMock}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
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(
|
||||
<ImageDialog
|
||||
galleryOpen={true}
|
||||
admin={false}
|
||||
setDialogOpen={setDialogOpenMock}
|
||||
setImageLinks={setImageLinksMock}
|
||||
/>
|
||||
|
|
@ -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(
|
||||
<ImageDialog
|
||||
galleryOpen={true}
|
||||
setDialogOpen={setDialogOpenMock}
|
||||
setImageLinks={setImageLinksMock}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
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(
|
||||
<ImageDialog
|
||||
galleryOpen={true}
|
||||
setDialogOpen={setDialogOpenMock}
|
||||
setImageLinks={setImageLinksMock}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<React.SetStateAction<boolean>>;
|
||||
setImageLinks: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
const ImageDialog: React.FC<Props> = ({ galleryOpen, admin, setDialogOpen, setImageLinks }) => {
|
||||
};
|
||||
|
||||
const ImageDialog: React.FC<Props> = ({ galleryOpen, setDialogOpen, setImageLinks }) => {
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [images, setImages] = useState<Images[]>([]);
|
||||
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<Props> = ({ 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 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);
|
||||
}
|
||||
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 (
|
||||
<Dialog
|
||||
open={galleryOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
maxWidth="xl" // 'md' stands for medium size
|
||||
>
|
||||
<Dialog open={galleryOpen} onClose={() => setDialogOpen(false)} maxWidth="xl">
|
||||
<DialogTitle>
|
||||
Images disponibles
|
||||
<IconButton
|
||||
|
|
@ -89,60 +101,67 @@ const ImageDialog: React.FC<Props> = ({ galleryOpen, admin, setDialogOpen, setIm
|
|||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{images.map((obj: Images) => (
|
||||
<TableRow key={obj.id}>
|
||||
<TableCell>
|
||||
<img
|
||||
src={`data:${obj.mime_type};base64,${obj.file_content}`}
|
||||
alt={`Image ${obj.file_name}`}
|
||||
style={{ width: 350, height: "auto", borderRadius: 8 }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{admin && editingId === obj.id ? (
|
||||
<TextField
|
||||
value={obj.file_name}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
style={{ maxWidth: 150 }}
|
||||
/>
|
||||
) : (
|
||||
obj.file_name
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell style={{ minWidth: 150 }}>
|
||||
{obj.id}
|
||||
<IconButton onClick={() => onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{admin && (
|
||||
<>
|
||||
<IconButton onClick={() => handleEditClick(obj.id)} size="small" color="primary" data-testid={`edit-button-${obj.id}`}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{copiedId === obj.id && <span style={{ marginLeft: 8, color: "green" }}>Copié!</span>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</DialogContent>
|
||||
<DialogActions style={{ justifyContent: "center", width: "100%" }}>
|
||||
<Box display="flex" justifyContent="center" width="100%">
|
||||
<Button onClick={handlePrevPage} disabled={imgPage === 1} color="primary">
|
||||
Précédent
|
||||
</Button>
|
||||
<Button onClick={handleNextPage} disabled={(imgPage * imgLimit) >= totalImg} color="primary">
|
||||
Suivant
|
||||
</Button>
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height={200}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</DialogActions>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{images.map((obj: Images) => (
|
||||
<TableRow key={obj.id}>
|
||||
<TableCell>
|
||||
<img
|
||||
src={`data:${obj.mime_type};base64,${obj.file_content}`}
|
||||
alt={`Image ${obj.file_name}`}
|
||||
style={{ width: 350, height: "auto", borderRadius: 8 }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{obj.file_name}</TableCell>
|
||||
<TableCell style={{ minWidth: 150 }}>
|
||||
{obj.id}
|
||||
<IconButton onClick={() => onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleDelete(obj.id)} size="small" color="secondary" data-testid={`delete-button-${obj.id}`}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{copiedId === obj.id && <span style={{ marginLeft: 8, color: "green" }}>Copié!</span>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</DialogContent>
|
||||
{deleteConfirm.linked && (
|
||||
<Dialog open={Boolean(deleteConfirm.id)} onClose={() => setDeleteConfirm({ id: null, linked: false })}>
|
||||
<DialogTitle>Confirmer la suppression</DialogTitle>
|
||||
<DialogContent>
|
||||
Cette image est liée à d'autres objets. Êtes-vous sûr de vouloir la supprimer ?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteConfirm({ id: null, linked: false })} color="primary">
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={confirmDelete} color="secondary">
|
||||
Supprimer
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
<DialogActions style={{ justifyContent: "center", width: "100%" }}>
|
||||
<Box display="flex" justifyContent="center" width="100%">
|
||||
<Button onClick={handlePrevPage} disabled={imgPage === 1} color="primary">
|
||||
Précédent
|
||||
</Button>
|
||||
<Button onClick={handleNextPage} disabled={(imgPage * imgLimit) >= totalImg} color="primary">
|
||||
Suivant
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1247,6 +1247,36 @@ public async login(email: string, password: string): Promise<any> {
|
|||
}
|
||||
}
|
||||
|
||||
public async deleteImage(imgId: string): Promise<ApiResponse> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue