Skip to content

Conversation

@SiddharthJiyani
Copy link
Contributor

@SiddharthJiyani SiddharthJiyani commented Nov 3, 2025

Description

This PR implements a complete Albums feature , including both backend API endpoints and a fully functional frontend UI.

Features Implemented

Backend

  • Improved Album CRUD operations (Create, Read, Update, Delete)
  • Album locking/password protection
  • Cover image management
  • Image management within albums (add, remove, reorder)
  • Proper error handling and validation

Frontend

  • Albums UI with album cards
  • Create album dialog with name, description, and lock option
  • Edit album dialog with ability to change lock status
  • Delete album with confirmation dialog
  • Password-protected access for locked albums
  • Add/remove images from albums
  • Set album cover image
  • Dark/Light theme support with appropriate placeholder images

Technical Details

Architecture

  • State Management: Redux with albumsSlice.ts for persistent state
  • API Integration: usePictoQuery and usePictoMutation hooks
  • UI Components: Standard ShadCN components (Dialog, Button, Input, Label, Switch, etc.)
  • User Feedback: infoDialogSlice.ts for messages and loaderSlice.ts for loading states
  • Type Safety: Full TypeScript coverage with proper interfaces

Key Changes

  1. Created Redux slice (albumsSlice.ts) with 11 actions for album state management
  2. Created memoized selectors (albumSelectors.ts) using createSelector
  3. Implemented API functions for all album operations (albums.ts)
  4. Built comprehensive album components:
    • AlbumCard.tsx - Album display with lock icon
    • CreateAlbumDialog.tsx - New album creation
    • EditAlbumDialog.tsx - Album editing with lock management
    • DeleteConfirmDialog.tsx - Delete confirmation
    • AlbumPasswordDialog.tsx - Password entry for locked albums
    • AddImagesToAlbumDialog.tsx - Image management
  5. Created album pages:
    • Album.tsx - Albums list view
    • AlbumDetail.tsx - Album detail view with images
  6. Added proper error handling with detailed backend error messages

Testing Checklist

  • Create album (locked and unlocked)
  • Edit album (change name, description, lock status)
  • Delete album with confirmation
  • Lock/unlock albums with password
  • Add/remove images from album
  • Set album cover image
  • Auto-refresh UI after operations
  • Error messages display properly
  • Dark/Light theme support
  • Responsive design on mobile and desktop

Related Issues

Tasks

  • Use standard ShadCN components to create the UI. (Refer to other components)
  • Store all the state information using Redux only. (Refer to similar files to know how to create a Redux slice and store the query results)
  • Use only usePictoQuery and usePictoMutation to handle API calling.
  • Utilize the standard infoDialogSlice.ts and loaderSlice.ts to display dialog messages and loaders, respectively.
pictopy-album-issue_.2.mp4

Summary by CodeRabbit

  • New Features

    • Albums support locking (password protection), cover images, per-album photo counts, and a PUT endpoint to set cover images; frontend adds full album CRUD and image-management APIs and pages.
  • UI

    • New album screens and dialogs: create, edit, delete, add-images, password prompt, album card, empty state, album detail with gallery and selection tools.
  • Behavior Changes

    • Album visibility now uses "locked" semantics and album lists no longer accept a show-hidden parameter; payloads expose is_locked, cover_image_path, and image_count.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

Walkthrough

Replaces album visibility field is_hidden with is_locked, adds cover_image_path to albums schema with a migration, updates backend DB/API (including cover-image update and image-path helper), and implements full frontend albums feature set (API clients, Redux slice, components, pages, routes, dialogs, and types) including a new cover-image endpoint.

Changes

Cohort / File(s) Summary
Backend DB & API
backend/app/database/albums.py
Rename is_hiddenis_locked; add cover_image_path column and db_migrate_add_cover_image_column(); select specific columns in getters; update insert/update signatures to is_locked; add db_update_album_cover_image() and db_get_image_path().
Backend Routes
backend/app/routes/albums.py
Swap hidden → locked semantics (is_locked), remove show_hidden param, include image_count and cover_image_path in responses, update create/update flows, and add PUT /albums/{album_id}/cover with album/image validation.
Backend Schemas & Tests
backend/app/schemas/album.py, backend/tests/test_albums.py
Schema now uses is_locked, adds cover_image_path?: str and image_count:int, introduces SetCoverImageRequest; tests updated (fixture renamed to mock_db_locked_album, expectations adjusted for new fields).
Frontend API Layer
frontend/src/api/api-functions/albums.ts, frontend/src/api/apiEndpoints.ts, frontend/src/api/api-functions/index.ts
New albumsEndpoints and client functions: getAllAlbums, getAlbumById, create/update/delete albums, add/get/remove images, setAlbumCoverImage; re-exported in API index.
Frontend Redux & Store
frontend/src/features/albumsSlice.ts, frontend/src/features/albumSelectors.ts, frontend/src/app/store.ts
New albums slice (albums, selectedAlbum, albumImages) with actions and selectors (including memoized counts); integrate albumsReducer into root store.
Frontend Components — Dialogs
frontend/src/components/Albums/*.tsx
New dialog components: CreateAlbumDialog, EditAlbumDialog, DeleteConfirmDialog, AlbumPasswordDialog, AddImagesToAlbumDialog — forms, validations, mutations, multi-select image picker, and UX/error flows.
Frontend Components — Display
frontend/src/components/Albums/AlbumCard.tsx, frontend/src/components/EmptyStates/EmptyAlbumsState.tsx
AlbumCard shows cover (cover_image_path), lock indicator and actions; empty-state UI added for albums.
Frontend Pages & Routes
frontend/src/pages/Album/Album.tsx, frontend/src/pages/Album/AlbumDetail.tsx, frontend/src/routes/AppRoutes.tsx, frontend/src/constants/routes.ts
Albums list page implemented (component renamed to Albums), new AlbumDetail page with gallery and cover-setting, routes updated, new route ALBUM_DETAIL.
Frontend Types
frontend/src/types/Album.ts
Redefined Album shape (id, name, description, is_locked, cover_image_path, image_count, timestamps) and new request/prop types (Create/Update/Add/Remove/Get images, dialog props); removed legacy list interfaces.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Albums UI
    participant API as Backend API
    participant DB as Database

    User->>UI: Open CreateAlbumDialog
    UI->>User: Fill form (name, is_locked, password)
    User->>UI: Submit
    UI->>API: POST /albums {name,is_locked,password}
    API->>DB: db_insert_album(...)
    DB-->>API: OK
    API-->>UI: 201 Created
    UI->>API: GET /albums
    API->>DB: db_get_all_albums()
    DB-->>API: [albums with cover_image_path,image_count]
    API-->>UI: Album list
Loading
sequenceDiagram
    participant User
    participant Detail as AlbumDetail Page
    participant AddDialog as AddImagesToAlbumDialog
    participant API as Backend API
    participant DB as Database

    User->>Detail: View album
    Detail->>API: GET /albums/{id}
    API->>DB: db_get_album(id)
    DB-->>API: album (cover_image_path,image_count)
    API-->>Detail: album data
    User->>Detail: Add Images
    Detail->>AddDialog: open
    AddDialog->>API: GET /images
    API->>DB: db_get_all_images()
    DB-->>API: [images]
    API-->>AddDialog: images list
    AddDialog->>API: POST /albums/{id}/images {image_ids}
    API->>DB: associate images
    DB-->>API: OK
    API-->>Detail: trigger refresh -> GET /albums/{id}/images
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

  • Areas needing extra attention:
    • DB migration, SQL changes, and column rename in backend/app/database/albums.py.
    • Route validation for cover-image endpoint and locked-album password behavior in backend/app/routes/albums.py.
    • Consistency between backend response shape and frontend types/usages (cover_image_path, image_count, is_locked).
    • Redux slice edge cases when updating image_count and keeping albumImages/selectedAlbum in sync.
    • New UI components (accessibility, keyboard interactions, and error/loader flows).

Possibly related issues

  • Feat: Implement Albums UI #546 — This PR implements the Albums feature (backend schema/migration, new album endpoints, and frontend album UI/Redux), matching the objectives of that issue.

Suggested reviewers

  • rahulharpal1603

Poem

🐇
I hopped through code and found the lock,
A cover framed to grace each block.
Dialogs sing and counts align,
Albums tucked with cover fine —
Hop, commit, and carrot cake o'clock!

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: implement albums feature with create, edit, delete, and image management UI' directly and comprehensively describes the main scope of the changeset. It highlights the primary feature being implemented (albums feature) and the key capabilities involved (create, edit, delete, and image management UI). The title aligns well with the extensive changes across both backend and frontend, making it clear to reviewers what this PR primarily accomplishes.
✨ 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.

@SiddharthJiyani SiddharthJiyani force-pushed the feat/albums-ui-and-backend branch from ff90128 to afcecea Compare November 3, 2025 14:27
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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/types/Album.ts (1)

59-92: Remove duplicate interface definitions.

PasswordPromptDialogProps is defined twice (lines 59-64 and 80-85), and AddImagesToAlbumDialogProps is also defined twice (lines 66-71 and 87-92). This creates confusion and potential maintenance issues if the definitions diverge.

Remove the duplicate definitions:

 export interface CreateAlbumDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onSuccess?: () => void;
 }

 export interface PasswordPromptDialogProps {
   isOpen: boolean;
   onClose: () => void;
   onSubmit: (password: string) => void;
   albumName: string;
 }

+export interface AddImagesToAlbumDialogProps {
+  isOpen: boolean;
+  onClose: () => void;
+  albumId: string;
+  albumName: string;
+}
+
 export interface AlbumCardProps {
   album: Album;
   onClick: () => void;
   onEdit: () => void;
   onDelete: () => void;
 }
-
-export interface PasswordPromptDialogProps {
-  isOpen: boolean;
-  onClose: () => void;
-  onSubmit: (password: string) => void;
-  albumName: string;
-}
-
-export interface AddImagesToAlbumDialogProps {
-  isOpen: boolean;
-  onClose: () => void;
-  albumId: string;
-  albumName: string;
-}
🧹 Nitpick comments (4)
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1)

3-28: Consider adding aria-hidden to decorative icons.

The component structure and implementation are excellent. The layout, styling, dark mode support, and user guidance are well-executed.

For enhanced accessibility, consider adding aria-hidden="true" to decorative icons on lines 7, 18, and 22, since they don't convey information beyond what the text already provides.

Example for line 7:

-        <BookImage className="h-16 w-16 text-gray-400" strokeWidth={1.5} />
+        <BookImage className="h-16 w-16 text-gray-400" strokeWidth={1.5} aria-hidden="true" />
frontend/src/pages/Album/AlbumDetail.tsx (2)

153-174: Backend data transformation handles schema variations defensively.

The transformation logic safely handles multiple possible backend response structures using optional chaining and defaults. However, the use of any types suggests the backend API contract could benefit from stronger typing.

Consider defining a TypeScript interface for the backend album response to eliminate the any casts and improve type safety:

interface BackendAlbum {
  album_id: string;
  album_name: string;
  description?: string;
  is_locked?: boolean;
  cover_image_path?: string;
  image_count?: number;
  created_at?: string;
  updated_at?: string;
}

176-213: Image loading works but has inefficient data fetching pattern.

The implementation fetches all images from the library, then filters by album image IDs. While functional, this becomes inefficient as the image library grows.

Consider updating the backend getAlbumImages endpoint to return full image objects rather than just IDs, eliminating the need to fetch and filter the entire image library:

// Instead of fetching all images
const { data: allImagesData } = usePictoQuery({
  queryKey: ['images'],
  queryFn: () => fetchAllImages(),
  enabled: !!albumId && !!album,
});

// Backend could return full image objects directly
const { data: imagesData } = usePictoQuery({
  queryKey: ['album-images', albumId],
  queryFn: () => getAlbumImages(albumId!, password ? { password } : undefined),
  enabled: !!albumId && !!album,
});
frontend/src/features/albumsSlice.ts (1)

56-70: Consider syncing image_count from server.

The local image_count updates (increment on add, decrement on remove) are correct, but could drift from the server if operations fail or are retried. Consider refreshing the selected album from the server after image operations complete to ensure consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe2f98d and afcecea.

⛔ Files ignored due to path filters (2)
  • frontend/public/placeholder-album-light.svg is excluded by !**/*.svg
  • frontend/public/placeholder-album.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • backend/app/database/albums.py (10 hunks)
  • backend/app/routes/albums.py (10 hunks)
  • backend/app/schemas/album.py (3 hunks)
  • frontend/src/api/api-functions/albums.ts (1 hunks)
  • frontend/src/api/api-functions/index.ts (1 hunks)
  • frontend/src/api/apiEndpoints.ts (1 hunks)
  • frontend/src/app/store.ts (2 hunks)
  • frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumCard.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx (1 hunks)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/DeleteConfirmDialog.tsx (1 hunks)
  • frontend/src/components/Albums/EditAlbumDialog.tsx (1 hunks)
  • frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1 hunks)
  • frontend/src/constants/routes.ts (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
  • frontend/src/features/albumsSlice.ts (1 hunks)
  • frontend/src/pages/Album/Album.tsx (1 hunks)
  • frontend/src/pages/Album/AlbumDetail.tsx (1 hunks)
  • frontend/src/routes/AppRoutes.tsx (2 hunks)
  • frontend/src/types/Album.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (15)
frontend/src/components/Albums/AlbumCard.tsx (2)
frontend/src/types/Album.ts (1)
  • AlbumCardProps (73-78)
frontend/src/contexts/ThemeContext.tsx (1)
  • useTheme (66-73)
frontend/src/components/Albums/CreateAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • CreateAlbumDialogProps (53-57)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • createAlbum (37-45)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/Albums/AlbumPasswordDialog.tsx (1)
frontend/src/types/Album.ts (2)
  • PasswordPromptDialogProps (59-64)
  • PasswordPromptDialogProps (80-85)
backend/app/schemas/album.py (1)
frontend/src/types/Album.ts (1)
  • UpdateAlbumRequest (26-32)
frontend/src/features/albumsSlice.ts (2)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/components/Albums/EditAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • EditAlbumDialogProps (46-51)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • updateAlbum (52-61)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/api/api-functions/albums.ts (4)
frontend/src/types/API.ts (1)
  • APIResponse (1-8)
frontend/src/api/axiosConfig.ts (1)
  • apiClient (5-12)
frontend/src/api/apiEndpoints.ts (1)
  • albumsEndpoints (33-46)
frontend/src/types/Album.ts (5)
  • CreateAlbumRequest (19-24)
  • UpdateAlbumRequest (26-32)
  • AddImagesToAlbumRequest (34-36)
  • GetAlbumImagesRequest (38-40)
  • RemoveImagesFromAlbumRequest (42-44)
backend/app/routes/albums.py (2)
backend/app/schemas/album.py (5)
  • SetCoverImageRequest (55-56)
  • GetAlbumsResponse (64-66)
  • Album (6-12)
  • SuccessResponse (84-86)
  • ErrorResponse (89-92)
backend/app/database/albums.py (5)
  • db_update_album_cover_image (221-232)
  • verify_album_password (299-311)
  • db_get_all_albums (107-119)
  • db_get_album_images (235-245)
  • db_get_album (136-147)
frontend/src/routes/AppRoutes.tsx (2)
frontend/src/constants/routes.ts (1)
  • ROUTES (1-12)
frontend/src/pages/Album/AlbumDetail.tsx (1)
  • AlbumDetail (43-437)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (7)
frontend/src/types/Album.ts (2)
  • AddImagesToAlbumDialogProps (66-71)
  • AddImagesToAlbumDialogProps (87-92)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/api/api-functions/albums.ts (1)
  • addImagesToAlbum (79-88)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/Albums/DeleteConfirmDialog.tsx (1)
frontend/src/components/ui/dialog.tsx (6)
  • Dialog (133-133)
  • DialogContent (135-135)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
  • DialogFooter (137-137)
frontend/src/features/albumSelectors.ts (1)
frontend/src/app/store.ts (1)
  • RootState (24-24)
frontend/src/types/Album.ts (1)
backend/app/schemas/album.py (4)
  • CreateAlbumRequest (20-30)
  • UpdateAlbumRequest (33-44)
  • GetAlbumImagesRequest (47-48)
  • Album (6-12)
frontend/src/pages/Album/AlbumDetail.tsx (13)
frontend/src/features/albumSelectors.ts (2)
  • selectSelectedAlbum (7-8)
  • selectAlbumImages (10-10)
frontend/src/features/imageSelectors.ts (1)
  • selectIsImageViewOpen (13-16)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (4)
  • getAlbumById (26-31)
  • getAlbumImages (95-104)
  • removeMultipleImagesFromAlbum (126-135)
  • setAlbumCoverImage (142-151)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/features/albumsSlice.ts (3)
  • setSelectedAlbum (48-50)
  • setAlbumImages (52-54)
  • clearAlbumImages (72-74)
frontend/src/features/imageSlice.ts (3)
  • setImages (18-20)
  • clearImages (39-42)
  • setCurrentViewIndex (22-34)
frontend/src/components/Media/ImageCard.tsx (1)
  • ImageCard (19-109)
frontend/src/components/Media/MediaView.tsx (1)
  • MediaView (23-214)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1)
  • AddImagesToAlbumDialog (23-210)
frontend/src/pages/Album/Album.tsx (7)
frontend/src/features/albumSelectors.ts (1)
  • selectAlbums (5-5)
frontend/src/features/albumsSlice.ts (2)
  • removeAlbum (38-46)
  • setAlbums (21-23)
frontend/src/types/Album.ts (1)
  • Album (1-10)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (2)
  • getAllAlbums (15-20)
  • deleteAlbum (67-72)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
🪛 GitHub Actions: PR Check
backend/app/routes/albums.py

[error] 1-1: API response for /albums endpoints is missing the 'is_hidden' field in album objects. Tests expect this field in the JSON response but it is not present.


[error] 1-1: db_get_all_albums() is being called without a boolean argument, while tests expect it to be called with a 'show_hidden' flag (True/False). This mismatch causes test failures.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (11)
frontend/src/components/EmptyStates/EmptyAlbumsState.tsx (1)

1-2: LGTM!

The icon imports are appropriate for the empty state component.

frontend/src/api/apiEndpoints.ts (1)

33-46: LGTM! Clean endpoint definitions.

The album endpoints follow the established pattern and RESTful conventions. The implementation is consistent with existing endpoint groups in the file.

frontend/src/app/store.ts (1)

9-20: LGTM! Proper Redux integration.

The albums reducer is correctly imported and integrated into the store configuration following the established pattern.

frontend/src/components/Albums/EditAlbumDialog.tsx (2)

92-112: Validation logic handles lock state transitions correctly.

The validation properly distinguishes between:

  • Locking an unlocked album (requires new password)
  • Modifying a locked album (requires current password)
  • Changing password on locked album (current password required, new password optional)

22-301: Well-implemented edit dialog with proper password state management.

The component correctly handles the complex logic for editing locked/unlocked albums, including:

  • Appropriate validation for different lock states
  • Clear UI feedback with contextual labels
  • Proper cleanup on close
frontend/src/components/Albums/AlbumCard.tsx (2)

26-55: Robust image handling with proper error fallback.

The component correctly:

  • Selects theme-appropriate placeholders
  • Uses Tauri's convertFileSrc for local file paths
  • Prevents infinite error loops by setting img.onerror = null

92-103: Clean presentation with proper text handling.

The info section correctly:

  • Truncates long album names
  • Clamps description to 2 lines
  • Pluralizes "photo/photos" appropriately
frontend/src/pages/Album/AlbumDetail.tsx (2)

215-221: Proper cleanup prevents state leaks.

The cleanup effect correctly clears all album-related state on unmount, preventing stale data from persisting.


43-437: Comprehensive album detail page with good UX patterns.

The component effectively handles:

  • Password-protected album access
  • Multiple viewing modes (normal, selection, media viewer)
  • Bulk operations with clear visual feedback
  • Context menus for per-image actions
  • Proper state cleanup on unmount
frontend/src/api/api-functions/albums.ts (1)

1-151: LGTM! Well-structured API layer.

The API functions are well-documented, properly typed, and use appropriate HTTP methods. The consistent error handling pattern and clear separation of concerns make this module easy to maintain.

backend/app/database/albums.py (1)

65-82: LGTM! Well-implemented migration.

The migration function properly checks for column existence before altering the table, preventing errors on repeated runs. Good defensive programming.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/tests/test_albums.py (2)

467-488: Fix inconsistent mock return tuple shape.

Line 477 uses tuple(mock_db_album.values()) which creates a 5-element tuple from the fixture dict. However, other tests throughout this file explicitly construct 6-element tuples that include image_count as the last element. This inconsistency could cause test failures if db_get_album expects to return 6 elements.

Apply this diff to use a consistent 6-element tuple:

-        with patch("app.routes.albums.db_get_album") as mock_get, patch(
-            "app.routes.albums.db_remove_images_from_album"
-        ) as mock_remove_bulk:
-            mock_get.return_value = tuple(mock_db_album.values())
+        album_tuple = (
+            mock_db_album["album_id"],
+            mock_db_album["album_name"],
+            mock_db_album["description"],
+            int(mock_db_album["is_locked"]),
+            mock_db_album["password_hash"],
+            0,  # image_count
+        )
+
+        with patch("app.routes.albums.db_get_album") as mock_get, patch(
+            "app.routes.albums.db_remove_images_from_album"
+        ) as mock_remove_bulk:
+            mock_get.return_value = album_tuple
             response = client.request(
                 "DELETE", f"/albums/{album_id}/images", json=image_ids_to_remove
             )

99-105: Add missing password_hash field to the 6-element tuple for db_get_album_by_name mock.

The db_get_album_by_name function's SQL query selects 6 fields (album_id, album_name, description, is_locked, password_hash, cover_image_path), but this mock returns only 5 elements. Update the mock to include the password_hash field as the 5th element:

mock_get_by_name.return_value = (
    "existing-id",
    "Existing Album",
    "desc",
    0,
    None,  # password_hash
    None,  # cover_image_path
)
🧹 Nitpick comments (1)
backend/tests/test_albums.py (1)

149-185: Consider renaming test function for consistency.

The test function name still contains "hidden" (test_get_all_albums_include_hidden), while the docstring and implementation have been updated to use "locked" terminology. For consistency, consider renaming this to test_get_all_albums_include_locked.

-    def test_get_all_albums_include_hidden(self, mock_db_album, mock_db_locked_album):
+    def test_get_all_albums_include_locked(self, mock_db_album, mock_db_locked_album):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afcecea and 5841ada.

📒 Files selected for processing (1)
  • backend/tests/test_albums.py (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/tests/test_albums.py (1)
backend/tests/test_folders.py (1)
  • client (76-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
backend/tests/test_albums.py (3)

23-42: LGTM! Fixture updates are correct.

The fixtures have been correctly updated to reflect the migration from is_hidden to is_locked, and the naming change from mock_db_hidden_album to mock_db_locked_album is appropriate.


53-352: Test updates for CRUD operations look good.

The test methods in TestAlbumRoutes have been appropriately updated to reflect the migration from is_hidden to is_locked. The parametrized tests provide good coverage of different scenarios (public/locked albums, password validation), and most mock return tuples consistently use the 6-element format with cover_image_path and image_count fields.


355-466: Test updates for image management operations look good.

The test methods in TestAlbumImageManagement correctly use the updated 6-element tuple format for mock album data, ensuring consistency with the new album schema that includes is_locked, cover_image_path, and image_count fields.

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

♻️ Duplicate comments (3)
backend/app/database/albums.py (1)

107-118: LGTM! Past issue with unused parameter resolved.

The removal of the show_hidden/show_locked parameter addresses the previous review comment about an ignored parameter. The function now has clear, unambiguous behavior—it always returns all albums.

backend/app/routes/albums.py (2)

128-182: LGTM! Update endpoint correctly refactored.

The endpoint correctly uses is_locked throughout, and there's no duplicate route decorator issue (which was mentioned in a previous review). The password validation logic for locked albums is properly maintained.


376-430: LGTM! Cover image endpoint is well-implemented.

The new endpoint has comprehensive validation:

  1. Album existence check
  2. Image membership verification (image must be in the album)
  3. Image existence validation

The use of db_get_image_path maintains proper architectural separation (addressing a previous review comment about inline database queries).

🧹 Nitpick comments (4)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (2)

34-44: Remove redundant state duplication.

The allImages state (line 32) unnecessarily duplicates imagesData.data. This adds complexity, extra re-renders, and a type assertion that could be unsafe.

Apply this diff to use imagesData.data directly:

-  const [allImages, setAllImages] = useState<Image[]>([]);

   const { data: imagesData, isLoading } = usePictoQuery({
     queryKey: ['images'],
     queryFn: () => fetchAllImages(),
     enabled: isOpen,
   });

-  useEffect(() => {
-    if (imagesData?.data) {
-      setAllImages(imagesData.data as Image[]);
-    }
-  }, [imagesData]);

Then update line 104 to use imagesData?.data directly:

-  const filteredImages = allImages.filter((image) => {
+  const filteredImages = (imagesData?.data as Image[] || []).filter((image) => {

46-96: Simplify by removing manual loader dispatches.

The component uses both manual loader dispatches (showLoader/hideLoader at lines 50, 61, 94) and the isPending flag from the mutation. This is redundant—isPending already provides the loading state needed for UI feedback.

Remove the manual loader dispatches and rely solely on isPending:

   const handleSubmit = () => {
     if (selectedImages.size === 0) {
       dispatch(
         showInfoDialog({
           title: 'No Images Selected',
           message: 'Please select at least one image to add to the album.',
           variant: 'info',
         }),
       );
       return;
     }

-    dispatch(showLoader('Adding images to album...'));
     addImagesMutate({ image_ids: Array.from(selectedImages) });
   };

And remove hideLoader calls from the mutation callbacks (lines 50, 61):

   const { mutate: addImagesMutate, isPending } = usePictoMutation({
     mutationFn: (data: { image_ids: string[] }) =>
       addImagesToAlbum(albumId, data),
     onSuccess: () => {
-      dispatch(hideLoader());
       dispatch(
         showInfoDialog({
           title: 'Success',
           message: `Added ${selectedImages.size} image${selectedImages.size > 1 ? 's' : ''} to album!`,
           variant: 'info',
         }),
       );
       handleClose();
     },
     onError: (error: any) => {
-      dispatch(hideLoader());
       dispatch(
         showInfoDialog({
           title: 'Error',
           message: error?.message || 'Failed to add images. Please try again.',
           variant: 'error',
         }),
       );
     },
   });

The button states at lines 200 and 206 already use isPending correctly.

backend/app/database/albums.py (1)

220-322: Consider using the context manager consistently.

While not critical, there's an inconsistency in connection handling: db_update_album_cover_image and db_get_image_path use manual connection management, while some other functions (e.g., db_delete_album, db_add_images_to_album) use the get_db_connection() context manager defined at the top of the file. The context manager provides retry logic and better error handling.

Consider refactoring these functions to use the context manager:

 def db_update_album_cover_image(album_id: str, cover_image_path: str):
     """Update the cover image path for an album"""
-    conn = sqlite3.connect(DATABASE_PATH)
-    cursor = conn.cursor()
-    try:
+    with get_db_connection() as conn:
+        cursor = conn.cursor()
         cursor.execute(
             "UPDATE albums SET cover_image_path = ? WHERE album_id = ?",
             (cover_image_path, album_id),
         )
-        conn.commit()
-    finally:
-        conn.close()

Apply similar changes to db_get_image_path and other functions using manual connection management.

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

43-45: Consider optimizing image count queries.

The current implementation performs N+1 queries: one to fetch all albums, then one per album to count images. For large album collections, this could be slow.

Consider adding a database function that returns albums with image counts in a single query:

def db_get_all_albums_with_counts():
    """Get all albums with their image counts in a single query"""
    conn = sqlite3.connect(DATABASE_PATH)
    cursor = conn.cursor()
    try:
        cursor.execute(
            """
            SELECT a.album_id, a.album_name, a.description, a.is_locked, 
                   a.password_hash, a.cover_image_path, COUNT(ai.image_id) as image_count
            FROM albums a
            LEFT JOIN album_images ai ON a.album_id = ai.album_id
            GROUP BY a.album_id
            """
        )
        return cursor.fetchall()
    finally:
        conn.close()

This reduces the operation from O(n) queries to O(1).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5841ada and 67e9937.

📒 Files selected for processing (8)
  • backend/app/database/albums.py (11 hunks)
  • backend/app/routes/albums.py (9 hunks)
  • backend/app/schemas/album.py (3 hunks)
  • backend/tests/test_albums.py (14 hunks)
  • frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
  • frontend/src/types/Album.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • frontend/src/features/albumSelectors.ts
  • frontend/src/components/Albums/AlbumPasswordDialog.tsx
  • backend/app/schemas/album.py
  • frontend/src/types/Album.ts
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (7)
frontend/src/types/Album.ts (1)
  • AddImagesToAlbumDialogProps (66-71)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/hooks/useQueryExtension.ts (2)
  • usePictoQuery (80-108)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/images.ts (1)
  • fetchAllImages (5-14)
frontend/src/api/api-functions/albums.ts (1)
  • addImagesToAlbum (79-88)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
backend/app/routes/albums.py (2)
backend/app/schemas/album.py (5)
  • SetCoverImageRequest (60-61)
  • GetAlbumsResponse (69-71)
  • Album (6-12)
  • ErrorResponse (94-97)
  • SuccessResponse (89-91)
backend/app/database/albums.py (5)
  • db_update_album_cover_image (220-231)
  • db_get_image_path (313-322)
  • db_get_all_albums (107-118)
  • db_get_album_images (234-244)
  • db_get_album (135-146)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (18)
frontend/src/components/Albums/AddImagesToAlbumDialog.tsx (6)

1-21: LGTM!

Imports are well-organized and all dependencies are properly declared.


23-32: LGTM!

Component props and state initialization are correct. Using a Set for selectedImages is an efficient choice for fast lookups and toggles.


72-102: LGTM!

Handler functions are correctly implemented:

  • handleImageToggle properly creates a new Set for immutability
  • handleSubmit validates selection before proceeding
  • handleClose properly resets all local state before closing

104-108: LGTM!

Filtering logic is correct with proper handling of edge cases (undefined from .pop() via || ''). Case-insensitive search provides good UX.


161-170: Past review feedback addressed—theme-aware placeholders implemented correctly.

The previous review comment about using non-existent /placeholder.svg has been resolved. The current implementation correctly uses theme-aware placeholders (/placeholder-album.svg for dark mode, /placeholder-album-light.svg for light mode) via window.matchMedia, aligning with the project's existing assets.


110-215: LGTM!

UI structure and rendering logic are well-implemented:

  • Proper loading, empty, and error states
  • Accessible search with icon
  • Responsive grid layout with visual selection feedback
  • Clear user feedback with selected count and dynamic button text
  • Buttons correctly disabled based on pending state and selection
backend/tests/test_albums.py (4)

23-42: LGTM! Fixture renaming aligns with schema changes.

The fixture renaming from mock_db_hidden_album to mock_db_locked_album and the field update from is_hidden to is_locked correctly reflect the backend schema changes.


53-68: LGTM! Test parameterization covers locked and unlocked scenarios.

The test cases correctly exercise both is_locked=False (public) and is_locked=True (password-protected) album creation flows.


119-171: LGTM! Mock return data correctly reflects database schema changes.

The addition of cover_image_path and image_count placeholders to mock return tuples aligns with the updated database layer, and all is_hidden references have been correctly replaced with is_locked.


329-353: LGTM! Test expectations correctly updated for schema changes.

The test correctly constructs album tuples with int(mock_db_album["is_locked"]) for SQLite compatibility and includes the image_count placeholder, consistent with the database layer changes.

backend/app/database/albums.py (4)

42-62: LGTM! Album table schema correctly updated.

The schema now includes is_locked, password_hash, and cover_image_path fields, aligning with the feature requirements for locked albums and cover image management.


149-211: LGTM! Parameter renaming and SQL updates are consistent.

The renaming of is_hidden to is_locked is correctly applied in both db_insert_album and db_update_album, with proper boolean-to-integer conversion for SQLite compatibility.


220-231: LGTM! Cover image update function is straightforward.

The db_update_album_cover_image function correctly updates the cover image path for an album.


313-322: LGTM! Image path retrieval function is correct.

The db_get_image_path function appropriately queries the images table to retrieve the file path for a given image ID.

backend/app/routes/albums.py (4)

1-31: LGTM! Imports correctly updated for new functionality.

The addition of SetCoverImageRequest, db_update_album_cover_image, and db_get_image_path supports the new cover image endpoint and addresses the previous inline database query issue.


36-57: LGTM! Function correctly updated for schema changes.

The removal of the parameter and the addition of image count and cover image path support align with the backend changes. The safe indexing album[5] if len(album) > 5 else None handles potential schema migration cases gracefully.


91-125: LGTM! Album detail endpoint correctly updated.

The endpoint properly incorporates image count calculation and cover image path, with consistent error handling and safe field access.


211-267: LGTM! Image retrieval endpoint correctly updated.

The endpoint properly checks is_locked status and requires password authentication for locked albums, with clear error messages.

Comment on lines +65 to +82
def db_migrate_add_cover_image_column() -> None:
"""Add cover_image_path column to existing albums table if it doesn't exist"""
conn = None
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

# Check if column exists
cursor.execute("PRAGMA table_info(albums)")
columns = [column[1] for column in cursor.fetchall()]

if "cover_image_path" not in columns:
cursor.execute("ALTER TABLE albums ADD COLUMN cover_image_path TEXT")
conn.commit()
print("Added cover_image_path column to albums table")
finally:
if conn is not None:
conn.close()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify migration function is invoked.

The migration function logic is correct, but ensure it's being called during application initialization or in a migration script to actually apply the schema change to existing databases.

Run this script to check if the migration function is invoked:


🏁 Script executed:

#!/bin/bash
# Check if db_migrate_add_cover_image_column is called anywhere
rg -n "db_migrate_add_cover_image_column" --type=py -C 3

Length of output: 421



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for migration-related patterns in initialization and main files
rg -n "(migrate|initialization|startup)" --type=py -C 2 backend/app/__init__.py backend/app/database/__init__.py backend/app/main.py 2>/dev/null || echo "No direct matches"

# Also check if there's any file that imports this migration function
rg -n "from.*albums.*import|import.*db_migrate" --type=py

Length of output: 423



---

🏁 Script executed:

```shell
#!/bin/bash
# Check where db_create functions are called in main.py to understand the initialization flow
sed -n '15,50p' backend/main.py

Length of output: 1412



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the full lifespan function to see complete initialization
sed -n '40,70p' backend/main.py

Length of output: 921


Add migration function to application startup sequence.

The db_migrate_add_cover_image_column function is defined but never called, so the cover_image_path column won't be added to existing databases. Import and invoke it during application initialization in backend/main.py within the lifespan context manager, after db_create_albums_table() is called (around line 54).

🤖 Prompt for AI Agents
In backend/main.py around line 54 inside the lifespan context manager, the
migration function db_migrate_add_cover_image_column from
backend.app.database.albums is not invoked; import
db_migrate_add_cover_image_column at the top of the file and call it immediately
after the existing db_create_albums_table() invocation so the migration runs on
startup for existing databases (keep it synchronous, handle/let exceptions
propagate as with db_create_albums_table()).

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 (1)
frontend/src/components/Albums/EditAlbumDialog.tsx (1)

38-40: Prefer explicit typing over any for type safety.

The mutation function parameter data is typed as any, which bypasses TypeScript's type checking. Consider using the UpdateAlbumRequest type instead.

Apply this diff to improve type safety:

-  const { mutate: updateAlbumMutate, isPending } = usePictoMutation({
-    mutationFn: ({ albumId, data }: { albumId: string; data: any }) =>
-      updateAlbum(albumId, data),
+  const { mutate: updateAlbumMutate, isPending } = usePictoMutation({
+    mutationFn: ({ albumId, data }: { albumId: string; data: UpdateAlbumRequest }) =>
+      updateAlbum(albumId, data),

You'll also need to import the type:

import { UpdateAlbumRequest } from '@/types/Album';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67e9937 and 634140f.

📒 Files selected for processing (3)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx (1 hunks)
  • frontend/src/components/Albums/EditAlbumDialog.tsx (1 hunks)
  • frontend/src/features/albumSelectors.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/Albums/CreateAlbumDialog.tsx
  • frontend/src/features/albumSelectors.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/Albums/EditAlbumDialog.tsx (5)
frontend/src/types/Album.ts (1)
  • EditAlbumDialogProps (46-51)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/albums.ts (1)
  • updateAlbum (52-61)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
frontend/src/components/Albums/EditAlbumDialog.tsx (3)

80-90: LGTM!

The form population logic correctly pre-fills the form fields from the album prop while resetting password fields to empty strings for security. The effect dependency on [album] is appropriate.


92-112: LGTM!

The validation logic correctly handles all scenarios:

  • Requires name field
  • Requires current password when modifying a locked album
  • Requires new password when locking a previously unlocked album

The use of optional chaining (album?.is_locked) safely handles null cases.


205-281: LGTM!

The password fields logic is well-implemented:

  • Current password is shown and required only for albums that were originally locked
  • New password field appears when the lock toggle is enabled
  • Labels and placeholders are contextual and user-friendly
  • Required indicators correctly match the validation rules

The UX clearly communicates when passwords are optional vs. required.

Comment on lines +123 to +129
const requestData: any = {
name: formData.name.trim(),
...(formData.description.trim() && {
description: formData.description.trim(),
}),
is_locked: formData.is_locked,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix the description clearing logic.

The current implementation only includes the description field in the request when it has content (line 125-127). This prevents users from clearing/emptying the description field. If a user deletes all text from the description textarea, the field won't be sent to the backend, causing the old description to persist.

Apply this diff to allow clearing the description:

   const requestData: any = {
     name: formData.name.trim(),
-    ...(formData.description.trim() && {
-      description: formData.description.trim(),
-    }),
+    description: formData.description.trim(),
     is_locked: formData.is_locked,
   };

This ensures the description field is always sent, allowing users to clear it by submitting an empty string.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const requestData: any = {
name: formData.name.trim(),
...(formData.description.trim() && {
description: formData.description.trim(),
}),
is_locked: formData.is_locked,
};
const requestData: any = {
name: formData.name.trim(),
description: formData.description.trim(),
is_locked: formData.is_locked,
};
🤖 Prompt for AI Agents
In frontend/src/components/Albums/EditAlbumDialog.tsx around lines 123 to 129,
the requestData object currently only includes description when non-empty which
prevents clearing it; change the object so description is always present (use
the trimmed description string, allowing it to be an empty string) instead of
conditionally spreading it, so the backend receives an explicit empty value when
the user clears the textarea.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant