diff --git a/backend/app/routers/photos.py b/backend/app/routers/photos.py index dbdbab4..0af894f 100644 --- a/backend/app/routers/photos.py +++ b/backend/app/routers/photos.py @@ -1252,6 +1252,18 @@ async def bulk_action( elif action.action == 'set_color': for photo in photos: photo.color_label = action.value + elif action.action == 'set_notes': + # value is the replacement notes string (empty string clears). + # Sent verbatim — no whitespace trimming, callers can pre-trim + # client-side if they want. + if action.value is not None and not isinstance(action.value, str): + raise HTTPException( + status_code=400, + detail="set_notes requires a string value (or null to clear)", + ) + new_notes = action.value or None # empty string -> null + for photo in photos: + photo.user_notes = new_notes elif action.action in ('set_taken_at', 'set_taken_at_map'): # Two shapes share one code path: # set_taken_at → value is one ISO datetime, applied to every id diff --git a/frontend/src/components/layout/RightSidebar.tsx b/frontend/src/components/layout/RightSidebar.tsx index 3d4a60d..c92e8d6 100644 --- a/frontend/src/components/layout/RightSidebar.tsx +++ b/frontend/src/components/layout/RightSidebar.tsx @@ -20,6 +20,7 @@ import { BulkTagsEditor } from '../sidebar/BulkTagsEditor' import { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' import { useBulkPhotoMutations } from '../../hooks/useBulkPhotoMutations' import { formatApiError } from '../../lib/apiError' @@ -36,6 +37,7 @@ export function RightSidebar() { const { bulkRating: bulkRatingMutation, bulkColor: bulkColorMutation, + bulkNotes: bulkNotesMutation, invalidatePhotoQueries, } = useBulkPhotoMutations() @@ -144,6 +146,10 @@ export function RightSidebar() { const { data: allTags = [] } = useTagsQuery() const [tagInput, setTagInput] = useState('') + // Local draft for the bulk notes textarea. Reset on selection-size + // changes (and inside the apply handler) so a leftover note from a + // previous selection doesn't haunt the next bulk action. + const [bulkNotesDraft, setBulkNotesDraft] = useState('') // Active heap membership for the bulk Select toggle. const { activeHeap, memberIds: activeHeapMembers } = useActiveHeapMembers() @@ -422,6 +428,49 @@ export function RightSidebar() { /> + {/* Bulk Notes — replaces every selected photo's notes with the + * same string. Click Apply to commit; nothing fires on every + * keystroke (each keystroke would otherwise PATCH all N rows). + * Empty + Apply clears the field across the selection. */} +
+ +