Skip to content

Conversation

@Akash1xe
Copy link

@Akash1xe Akash1xe commented Dec 3, 2025

Fixes #681

This PR fixes the issue where the favorite (heart) icon on the PersonImages page does not update immediately after toggling favorites.

Issue: #681 – Favorite Icon Does Not Update on PersonImages Page

Changes:

  • PersonImages UI now immediately reflects the new favorite state.
  • Heart icon updates color (red/grey) instantly.
  • React Query invalidates and refetches using the correct key.
  • Keeps frontend in sync with backend updates without full page reload.

Steps to verify:

  1. Open any face collection → go to PersonImages.
  2. Click the heart icon → icon changes immediately.
  3. Open Favorites tab → image shows correctly as favorited/unfavorited.
  4. Return to PersonImages → UI remains consistent.

Summary by CodeRabbit

  • New Features

    • Images in clusters now include a favorite flag visible to the UI.
    • Optimistic updates for toggling favorites to make toggles feel instant.
    • Client now listens for query cache updates to keep cluster images in sync.
  • Bug Fixes / Reliability

    • Improved error handling and clearer failure responses when retrieving cluster images.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Walkthrough

Adds an isFavourite flag through the backend DB query, API response schema, and OpenAPI; updates the frontend with optimistic toggle handling, React Query cache invalidation, and a cache listener in PersonImages to keep Redux in sync.

Changes

Cohort / File(s) Summary
Backend DB & query mapping
backend/app/database/face_clusters.py
Selects isFavourite from images in db_get_images_by_cluster_id and maps it into each returned image dict as isFavourite.
Backend API route
backend/app/routes/face_clusters.py
Replaces ImageInCluster construction with direct dict mapping; formats images for frontend (id, path, thumbnailPath, isFavourite, metadata, face_id, confidence, bbox); adjusts error logging/messages.
Backend schema
backend/app/schemas/face_clusters.py
Adds isFavourite: bool = False to ImageInCluster Pydantic model.
API docs
docs/backend/backend_python/openapi.json
Adds boolean isFavourite field (default false) to ImageData and ImageInCluster schemas.
Frontend optimistic toggle hook
frontend/src/hooks/useToggleFav.ts
Adds React Query client usage: onMutate optimistic updates with snapshot/rollback, onError rollback, and onSuccess invalidation of person-images query.
Frontend page cache sync
frontend/src/pages/PersonImages/PersonImages.tsx
Initializes useQueryClient, enables refetchOnWindowFocus, and adds a cache subscription effect to sync ['person-images', clusterId] query updates back into Redux setImages.

Sequence Diagram(s)

sequenceDiagram
  participant U as User (UI)
  participant C as React Query Client
  participant H as useToggleFav Hook
  participant S as Backend API
  participant DB as Database
  participant R as Redux Store

  U->>H: toggleFavourite(imageId)
  H->>C: onMutate -> cancel queries, snapshot previousImages
  H->>C: update cache optimistically (set isFavourite)
  H->>S: PATCH /images/{id}/favourite
  S->>DB: update is_favourite
  DB-->>S: result
  S-->>H: 200 OK
  H->>C: onSuccess -> invalidate ['person-images', clusterId]
  C-->>C: refetch person-images query
  C->>S: GET /clusters/{id}/images
  S->>DB: select images (includes isFavourite)
  DB-->>S: images
  S-->>C: response with images
  C->>R: cache subscription fires -> dispatch setImages(updatedImages)
  alt error
    S-->>H: 4xx/5xx
    H->>C: onError -> rollback snapshot, invalidate person-images
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing closer review:
    • backend/app/database/face_clusters.py — ensure SQL column aliasing and null handling for isFavourite.
    • backend/app/routes/face_clusters.py — mapping correctness and consistent field names (isFavourite vs is_favourite) and error paths.
    • frontend/src/hooks/useToggleFav.ts — optimistic update correctness, snapshot/rollback edge cases, and query-key consistency.
    • frontend/src/pages/PersonImages/PersonImages.tsx — cache subscription lifecycle and potential memory leaks on mount/unmount.

Possibly related issues

Possibly related PRs

Suggested labels

bug, UI, frontend, backend

Suggested reviewers

  • rahulharpal1603

Poem

🐇 A tiny flag hops from DB to view,
I nudge the cache and hope it stays true,
Optimistic heart, rollback if it fails,
Redux and Query trace the fairy-tale trails,
Hooray — favourite hops follow through! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main fix: favorite icon updates correctly on PersonImages page. It's specific, clear, and directly reflects the primary change across backend and frontend modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
backend/app/schemas/face_clusters.py (1)

54-54: Remove leftover development comment.

The inline comment # Add this field appears to be a development note that should be removed before merging.

-    isFavourite: bool = False  # Add this field
+    isFavourite: bool = False
frontend/src/hooks/useToggleFav.ts (1)

16-62: Incomplete optimistic update rollback for person-images.

The onMutate optimistically updates both images and person-images caches, but on error, only previousImages (the images cache snapshot) is restored. The person-images cache isn't explicitly rolled back. While the subsequent invalidateQueries on line 61 will refetch and correct the state, this creates a brief window of inconsistency.

For a more robust pattern, consider snapshotting and restoring both caches:

     onMutate: async (image_id: string) => {
       // Cancel outgoing refetches
       await queryClient.cancelQueries({ queryKey: ['images'] });
+      await queryClient.cancelQueries({ queryKey: ['person-images'] });

       // Snapshot the previous value
       const previousImages = queryClient.getQueryData(['images']);
+      const previousPersonImages = queryClient.getQueriesData({ queryKey: ['person-images'] });

       // Optimistically update images query
       // ... existing code ...

-      return { previousImages };
+      return { previousImages, previousPersonImages };
     },
     onError: (err, image_id, context) => {
       if (context?.previousImages) {
         queryClient.setQueryData(['images'], context.previousImages);
       }
+      if (context?.previousPersonImages) {
+        context.previousPersonImages.forEach(([queryKey, data]: [unknown[], unknown]) => {
+          queryClient.setQueryData(queryKey, data);
+        });
+      }
       // Refetch to restore correct state
       queryClient.invalidateQueries({ queryKey: ['person-images'] });
     },
frontend/src/pages/PersonImages/PersonImages.tsx (1)

52-68: Cache subscription approach works, but consider simplification.

The cache subscription correctly syncs optimistic updates from useToggleFav to Redux. However, this creates dual state management: the existing useEffect (lines 38-50) handles query state transitions, while this new subscription handles cache mutations.

A simpler alternative would be to have useToggleFav return the updated images directly or use React Query's state as the source of truth instead of syncing to Redux. That said, if Redux is needed elsewhere, this approach is acceptable.

backend/app/routes/face_clusters.py (1)

172-183: Consider using a list comprehension for cleaner transformation.

The manual loop building formatted_images is verbose. A list comprehension would be more Pythonic and concise.

Apply this diff to refactor using a list comprehension:

-        # 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"],
-            })
+        # Transform the data to match the frontend schema
+        formatted_images = [
+            {
+                "id": img["image_id"],
+                "path": img["image_path"],
+                "thumbnailPath": img["thumbnail_path"],
+                "metadata": img["metadata"],
+                "isFavourite": bool(img["isFavourite"]),
+                "face_id": img["face_id"],
+                "confidence": img["confidence"],
+                "bbox": img["bbox"],
+            }
+            for img in images
+        ]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c37d8df and a4da30d.

📒 Files selected for processing (6)
  • backend/app/database/face_clusters.py (3 hunks)
  • backend/app/routes/face_clusters.py (1 hunks)
  • backend/app/schemas/face_clusters.py (1 hunks)
  • docs/backend/backend_python/openapi.json (1 hunks)
  • frontend/src/hooks/useToggleFav.ts (1 hunks)
  • frontend/src/pages/PersonImages/PersonImages.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/routes/face_clusters.py (2)
backend/app/database/face_clusters.py (2)
  • db_get_cluster_by_id (126-153)
  • db_get_images_by_cluster_id (281-354)
backend/app/schemas/face_clusters.py (3)
  • ErrorResponse (23-26)
  • GetClusterImagesResponse (69-75)
  • GetClusterImagesData (60-66)
🔇 Additional comments (4)
backend/app/database/face_clusters.py (1)

304-345: LGTM!

The isFavourite field is correctly added to the SQL query, tuple unpacking, and output dictionary. The naming convention is consistent with the existing codebase patterns.

docs/backend/backend_python/openapi.json (1)

2215-2219: LGTM!

The OpenAPI spec correctly documents the new isFavourite field with appropriate type, title, and default value, consistent with the backend Pydantic model.

frontend/src/pages/PersonImages/PersonImages.tsx (1)

30-30: Enabling refetchOnWindowFocus is appropriate for this use case.

This ensures the favorite state stays in sync when users switch between tabs, which aligns with the PR objective of keeping the UI consistent.

backend/app/routes/face_clusters.py (1)

196-207: LGTM: Improved error handling and logging.

The addition of logger.error for debugging and the updated error messages improve observability and clarity. The exception handling pattern correctly re-raises HTTPException while catching and logging generic exceptions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
backend/app/routes/face_clusters.py (1)

187-192: Critical: cluster is a ClusterData object, not a dict – fix cluster_name access

db_get_cluster_by_id returns a ClusterData object (see backend/app/database/face_clusters.py), so cluster["cluster_name"] will raise a TypeError at runtime because ClusterData is not subscriptable. This will break the /clusters/{cluster_id}/images endpoint whenever a valid cluster exists.

Use attribute access instead, and consider removing the stale inline comment:

-            data=GetClusterImagesData(
-                cluster_id=cluster_id,
-                cluster_name=cluster["cluster_name"],  # ✅ CHANGE THIS LINE - Remove .get()
-                images=formatted_images,
-                total_images=len(formatted_images),
-            ),
+            data=GetClusterImagesData(
+                cluster_id=cluster_id,
+                cluster_name=cluster.cluster_name,
+                images=formatted_images,
+                total_images=len(formatted_images),
+            ),

This keeps the response in sync with the ClusterData definition and avoids a hard runtime failure.

🧹 Nitpick comments (2)
backend/app/routes/face_clusters.py (2)

168-183: Image transformation from DB rows to API payload looks correct

The mapping from db_get_images_by_cluster_id rows to formatted_images fields (id, path, thumbnailPath, metadata, isFavourite, face_id, confidence, bbox) is consistent with the database snippet provided and normalizing isFavourite via bool(...) is appropriate for the API.

If you want to simplify later, this block could be expressed as a list comprehension for slightly tighter code, but the current explicit loop is perfectly fine and readable.


199-205: Logging on failure is good; consider not returning raw exception text

Adding logger.error(...) on the generic exception path is useful for observability, and the updated error text is fine. However, you’re still including str(e) in the client-facing message, which can leak internal details. A safer pattern is usually:

  • Log str(e) server-side.
  • Return a stable, generic message to the client.

Not blocking this PR, but worth considering for future hardening.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4da30d and 77c4620.

📒 Files selected for processing (1)
  • backend/app/routes/face_clusters.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/routes/face_clusters.py (2)
backend/app/database/face_clusters.py (2)
  • db_get_cluster_by_id (126-153)
  • db_get_images_by_cluster_id (281-354)
backend/app/schemas/face_clusters.py (1)
  • GetClusterImagesData (60-66)
🔇 Additional comments (1)
backend/app/routes/face_clusters.py (1)

156-166: 404 handling for missing clusters is consistent and clear

The existence check and 404 response for a missing cluster_id remain correct, and the updated message wording is fine and consistent with the rest of the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG:Favorite Icon Does Not Update on PersonImages Page

1 participant