From f743733edd8edd19f31dd6afb89771689c51b455 Mon Sep 17 00:00:00 2001 From: Claudio Date: Mon, 11 May 2026 00:07:02 +0200 Subject: [PATCH] ui(sidebar): drop Title field, add bulk notes editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Title (user_title) field hadn't earned its place in the sidebar form — the underlying column stays on the model but the editable row + its draft state + commit handler are gone. Bulk Notes: a textarea in the multi-photo bulk panel that replaces user_notes across the whole selection with one string. Apply commits; Clear empties the draft without committing. New backend bulk action 'set_notes' validates the value is a string (or null/empty to clear) and writes to every photo in the selection in one go. Wired through the standard useBulkPhotoMutations optimistic-patch path, so the photo cache flips immediately and rolls back on error. user_notes added to the shared Photo type so patchPhotos accepts the field; previously it was only on PhotoInfoPanel's local PhotoDetails. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/routers/photos.py | 12 +++++ .../src/components/layout/RightSidebar.tsx | 49 +++++++++++++++++++ .../src/components/sidebar/PhotoInfoPanel.tsx | 30 ------------ frontend/src/hooks/useBulkPhotoMutations.ts | 19 ++++++- frontend/src/services/api.ts | 11 +++++ frontend/src/types/photo.ts | 1 + 6 files changed, 91 insertions(+), 31 deletions(-) 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. */} +
+ +