From a4da30df1c92c1de90bb91ac171d2cdf14469c6a Mon Sep 17 00:00:00 2001 From: Akash Shrivastav Date: Wed, 3 Dec 2025 14:56:32 +0530 Subject: [PATCH 1/2] Fix: Favorite Icon Updates Correctly on PersonImages Page #681 --- backend/app/database/face_clusters.py | 3 + backend/app/routes/face_clusters.py | 50 ++++++++-------- backend/app/schemas/face_clusters.py | 1 + docs/backend/backend_python/openapi.json | 5 ++ frontend/src/hooks/useToggleFav.ts | 58 ++++++++++++++++++- .../src/pages/PersonImages/PersonImages.tsx | 24 +++++++- 6 files changed, 114 insertions(+), 27 deletions(-) diff --git a/backend/app/database/face_clusters.py b/backend/app/database/face_clusters.py index ceac7f556..010ef6c08 100644 --- a/backend/app/database/face_clusters.py +++ b/backend/app/database/face_clusters.py @@ -301,6 +301,7 @@ def db_get_images_by_cluster_id( i.path as image_path, i.thumbnailPath as thumbnail_path, i.metadata, + i.isFavourite, f.face_id, f.confidence, f.bbox @@ -321,6 +322,7 @@ def db_get_images_by_cluster_id( image_path, thumbnail_path, metadata, + is_favourite, face_id, confidence, bbox_json, @@ -340,6 +342,7 @@ def db_get_images_by_cluster_id( "image_path": image_path, "thumbnail_path": thumbnail_path, "metadata": metadata_dict, + "isFavourite": is_favourite, "face_id": face_id, "confidence": confidence, "bbox": bbox, diff --git a/backend/app/routes/face_clusters.py b/backend/app/routes/face_clusters.py index 99974ac4a..345ff89ee 100644 --- a/backend/app/routes/face_clusters.py +++ b/backend/app/routes/face_clusters.py @@ -153,7 +153,7 @@ def get_all_clusters(): def get_cluster_images(cluster_id: str): """Get all images that contain faces belonging to a specific cluster.""" try: - # Step 1: Validate cluster exists + # Check if cluster exists cluster = db_get_cluster_by_id(cluster_id) if not cluster: raise HTTPException( @@ -161,48 +161,48 @@ def get_cluster_images(cluster_id: str): detail=ErrorResponse( success=False, error="Cluster Not Found", - message=f"Cluster with ID '{cluster_id}' does not exist.", + message=f"Cluster with ID '{cluster_id}' does not exist", ).model_dump(), ) - # Step 2: Get images for this cluster - images_data = db_get_images_by_cluster_id(cluster_id) - - # Step 3: Convert to response models - images = [ - ImageInCluster( - id=img["image_id"], - path=img["image_path"], - thumbnailPath=img["thumbnail_path"], - metadata=img["metadata"], - face_id=img["face_id"], - confidence=img["confidence"], - bbox=img["bbox"], - ) - for img in images_data - ] + # Get images for this cluster + images = db_get_images_by_cluster_id(cluster_id) + + # Transform the data to match the frontend schema + formatted_images = [] + for img in images: + formatted_images.append({ + "id": img["image_id"], # Changed from image_id to id + "path": img["image_path"], # Changed from image_path to path + "thumbnailPath": img["thumbnail_path"], # Changed from thumbnail_path to thumbnailPath + "metadata": img["metadata"], + "isFavourite": bool(img["isFavourite"]), # Ensure boolean + "face_id": img["face_id"], + "confidence": img["confidence"], + "bbox": img["bbox"], + }) return GetClusterImagesResponse( success=True, - message=f"Successfully retrieved {len(images)} image(s) for cluster '{cluster_id}'", + message=f"Successfully retrieved {len(formatted_images)} images for cluster", data=GetClusterImagesData( cluster_id=cluster_id, - cluster_name=cluster["cluster_name"], - images=images, - total_images=len(images), + cluster_name=cluster.get("cluster_name"), + images=formatted_images, + total_images=len(formatted_images), ), ) except HTTPException as e: - # Re-raise HTTPExceptions to preserve the status code and detail raise e except Exception as e: + logger.error(f"Error getting cluster images: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ErrorResponse( success=False, - error="Internal server error", - message=f"Unable to retrieve images for cluster: {str(e)}", + error="Internal Server Error", + message=f"Failed to retrieve cluster images: {str(e)}", ).model_dump(), ) diff --git a/backend/app/schemas/face_clusters.py b/backend/app/schemas/face_clusters.py index 7744d91ce..7a0758652 100644 --- a/backend/app/schemas/face_clusters.py +++ b/backend/app/schemas/face_clusters.py @@ -51,6 +51,7 @@ class ImageInCluster(BaseModel): path: str thumbnailPath: Optional[str] = None metadata: Optional[Dict[str, Any]] = None + isFavourite: bool = False # Add this field face_id: int confidence: Optional[float] = None bbox: Optional[Dict[str, Union[int, float]]] = None diff --git a/docs/backend/backend_python/openapi.json b/docs/backend/backend_python/openapi.json index a29e7c4f1..e6e4d976e 100644 --- a/docs/backend/backend_python/openapi.json +++ b/docs/backend/backend_python/openapi.json @@ -2212,6 +2212,11 @@ ], "title": "Metadata" }, + "isFavourite": { + "type": "boolean", + "title": "Isfavourite", + "default": false + }, "face_id": { "type": "integer", "title": "Face Id" diff --git a/frontend/src/hooks/useToggleFav.ts b/frontend/src/hooks/useToggleFav.ts index 8ce0bb94d..5f24ee235 100644 --- a/frontend/src/hooks/useToggleFav.ts +++ b/frontend/src/hooks/useToggleFav.ts @@ -1,18 +1,74 @@ import { usePictoMutation } from '@/hooks/useQueryExtension'; import { useMutationFeedback } from '@/hooks/useMutationFeedback'; import { togglefav } from '@/api/api-functions/togglefav'; +import { useQueryClient } from '@tanstack/react-query'; export const useToggleFav = () => { + const queryClient = useQueryClient(); + const toggleFavouriteMutation = usePictoMutation({ mutationFn: async (image_id: string) => togglefav(image_id), autoInvalidateTags: ['images'], + onSuccess: () => { + // Invalidate person-images queries to refetch cluster images + queryClient.invalidateQueries({ queryKey: ['person-images'] }); + }, + onMutate: async (image_id: string) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ['images'] }); + + // Snapshot the previous value + const previousImages = queryClient.getQueryData(['images']); + + // Optimistically update images query + queryClient.setQueryData(['images'], (old: any) => { + if (!old?.data) return old; + + return { + ...old, + data: old.data.map((img: any) => + img.id === image_id + ? { ...img, isFavourite: !img.isFavourite } + : img + ) + }; + }); + + // Optimistically update person-images queries + queryClient.setQueriesData({ queryKey: ['person-images'] }, (old: any) => { + if (!old?.data?.images) return old; + + return { + ...old, + data: { + ...old.data, + images: old.data.images.map((img: any) => + img.id === image_id + ? { ...img, isFavourite: !img.isFavourite } + : img + ) + } + }; + }); + + return { previousImages }; + }, + onError: (err, image_id, context) => { + if (context?.previousImages) { + queryClient.setQueryData(['images'], context.previousImages); + } + // Refetch to restore correct state + queryClient.invalidateQueries({ queryKey: ['person-images'] }); + }, }); + useMutationFeedback(toggleFavouriteMutation, { showLoading: false, showSuccess: false, }); + return { toggleFavourite: (id: any) => toggleFavouriteMutation.mutate(id), toggleFavouritePending: toggleFavouriteMutation.isPending, }; -}; +}; \ No newline at end of file diff --git a/frontend/src/pages/PersonImages/PersonImages.tsx b/frontend/src/pages/PersonImages/PersonImages.tsx index ea646b8ad..3b218cf8b 100644 --- a/frontend/src/pages/PersonImages/PersonImages.tsx +++ b/frontend/src/pages/PersonImages/PersonImages.tsx @@ -13,10 +13,12 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ROUTES } from '@/constants/routes'; import { Check, Pencil, ArrowLeft } from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; export const PersonImages = () => { const dispatch = useDispatch(); const navigate = useNavigate(); + const queryClient = useQueryClient(); const { clusterId } = useParams<{ clusterId: string }>(); const isImageViewOpen = useSelector(selectIsImageViewOpen); const images = useSelector(selectImages); @@ -25,6 +27,7 @@ export const PersonImages = () => { const { data, isLoading, isSuccess, isError } = usePictoQuery({ queryKey: ['person-images', clusterId], queryFn: async () => fetchClusterImages({ clusterId: clusterId || '' }), + refetchOnWindowFocus: true, }); const { mutate: renameClusterMutate } = usePictoMutation({ @@ -46,6 +49,24 @@ export const PersonImages = () => { } }, [data, isSuccess, isError, isLoading, dispatch]); + // Listen to query cache changes and update Redux state + useEffect(() => { + const unsubscribe = queryClient.getQueryCache().subscribe((event) => { + if ( + event.type === 'updated' && + event.query.queryKey[0] === 'person-images' && + event.query.queryKey[1] === clusterId + ) { + const updatedData = event.query.state.data as any; + if (updatedData?.data?.images) { + dispatch(setImages(updatedData.data.images)); + } + } + }); + + return () => unsubscribe(); + }, [clusterId, dispatch, queryClient]); + const handleEditName = () => { setClusterName(clusterName); setIsEditing(true); @@ -66,6 +87,7 @@ export const PersonImages = () => { handleSaveName(); } }; + return (
@@ -124,4 +146,4 @@ export const PersonImages = () => { {isImageViewOpen && }
); -}; +}; \ No newline at end of file From 77c462030d4f3080946acf2e0df535a9f1661320 Mon Sep 17 00:00:00 2001 From: Akash Shrivastav Date: Wed, 3 Dec 2025 15:20:41 +0530 Subject: [PATCH 2/2] Fix: Corrected Coderabbit warning - attribute access on ClusterData object --- backend/app/routes/face_clusters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/app/routes/face_clusters.py b/backend/app/routes/face_clusters.py index 345ff89ee..c39cd7c38 100644 --- a/backend/app/routes/face_clusters.py +++ b/backend/app/routes/face_clusters.py @@ -172,11 +172,11 @@ def get_cluster_images(cluster_id: str): formatted_images = [] for img in images: formatted_images.append({ - "id": img["image_id"], # Changed from image_id to id - "path": img["image_path"], # Changed from image_path to path - "thumbnailPath": img["thumbnail_path"], # Changed from thumbnail_path to thumbnailPath + "id": img["image_id"], + "path": img["image_path"], + "thumbnailPath": img["thumbnail_path"], "metadata": img["metadata"], - "isFavourite": bool(img["isFavourite"]), # Ensure boolean + "isFavourite": bool(img["isFavourite"]), "face_id": img["face_id"], "confidence": img["confidence"], "bbox": img["bbox"], @@ -187,7 +187,7 @@ def get_cluster_images(cluster_id: str): message=f"Successfully retrieved {len(formatted_images)} images for cluster", data=GetClusterImagesData( cluster_id=cluster_id, - cluster_name=cluster.get("cluster_name"), + cluster_name=cluster["cluster_name"], # ✅ CHANGE THIS LINE - Remove .get() images=formatted_images, total_images=len(formatted_images), ),