perf+ux: cut grid re-renders, coalesce discard, dedup bulk mutations
Frontend cleanup pass driven by the post-shadcn review. Performance - Memoize PhotoThumbnail and route cell click/double-click through stable handlers so heap-membership invalidation no longer re-renders every visible thumbnail. - Cap usePhotosQuery's eager background page-walk at 20 pages with a 50ms inter-page yield — was unbounded (up to 100k photos cold). - Drop the per-thumbnail loading spinner in favour of the existing pulse skeleton; only retry state still surfaces a spinner. UX - Coalesce rapid X/U presses into a single undo entry + one toast (1.2s window) so accidental bursts are easy to back out. - Optimistic rating/color updates with per-id snapshot rollback on error, matching the existing discard pattern. - Section-aware empty timeline state with a Clear-all-filters CTA. - Carry the search-match chip from the grid into the preview header. - Add a basket-icon badge for active heap membership so the green tint isn't the only signal (colorblind-safe). - Standardise error toasts via formatApiError(): FastAPI detail, validation arrays, axios message, with a 'Network Error' filter. Architecture - Extract useBulkPhotoMutations and stop duplicating bulkRating/bulkColor across RightSidebar and useKeyboardShortcuts. - Split RightSidebar (714 -> 448 LOC) and PhotoInfoPanel (952 -> 716) into co-located sub-components: BulkTakenAtEditor, BulkTagsEditor, TagsEditor, TakenAtEditor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ import {
|
||||
LIBRARY_STATS_QUERY_KEY,
|
||||
} from '../../hooks/useLibraryStatsQuery'
|
||||
import { registerUndoable } from '../../store/undoStore'
|
||||
import { formatApiError } from '../../lib/apiError'
|
||||
import type { Photo } from '../../types/photo'
|
||||
import { DeleteFolderDialog } from '../dialogs/DeleteFolderDialog'
|
||||
import { ShareDialog } from '../sharing/ShareDialog'
|
||||
@@ -150,7 +151,7 @@ export function LeftSidebar() {
|
||||
queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Discard failed', e?.message || 'Unknown error'),
|
||||
toast.error('Discard failed', formatApiError(e)),
|
||||
})
|
||||
|
||||
// Bulk move mutation for the drag-onto-folder interaction. The mutation
|
||||
@@ -223,7 +224,7 @@ export function LeftSidebar() {
|
||||
queryClient.invalidateQueries({ queryKey: ['folders'] })
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Move failed', e?.response?.data?.detail || e.message || 'Unknown error'),
|
||||
toast.error('Move failed', formatApiError(e)),
|
||||
})
|
||||
|
||||
// Bulk copy mutation — Alt-drag uses this instead of move.
|
||||
@@ -245,7 +246,7 @@ export function LeftSidebar() {
|
||||
queryClient.invalidateQueries({ queryKey: ['folders'] })
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Copy failed', e?.response?.data?.detail || e.message || 'Unknown error'),
|
||||
toast.error('Copy failed', formatApiError(e)),
|
||||
})
|
||||
|
||||
// Reads the dragged ids out of a drop event payload.
|
||||
@@ -314,7 +315,7 @@ export function LeftSidebar() {
|
||||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Rename failed', e?.response?.data?.detail || e.message || 'Unknown error'),
|
||||
toast.error('Rename failed', formatApiError(e)),
|
||||
})
|
||||
|
||||
const createFolderMutation = useMutation({
|
||||
@@ -328,7 +329,7 @@ export function LeftSidebar() {
|
||||
setCreateDraft('')
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Create failed', e?.response?.data?.detail || e.message || 'Unknown error'),
|
||||
toast.error('Create failed', formatApiError(e)),
|
||||
})
|
||||
|
||||
// Toggle folder hide-from-views. Invalidates every query that could
|
||||
@@ -354,7 +355,7 @@ export function LeftSidebar() {
|
||||
queryClient.invalidateQueries({ queryKey: ['tags'] })
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Toggle failed', e?.response?.data?.detail || e.message || 'Unknown error'),
|
||||
toast.error('Toggle failed', formatApiError(e)),
|
||||
})
|
||||
|
||||
const deleteFolderMutation = useMutation({
|
||||
@@ -383,7 +384,7 @@ export function LeftSidebar() {
|
||||
setDeletingFolder(null)
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Delete failed', e?.response?.data?.detail || e.message || 'Unknown error'),
|
||||
toast.error('Delete failed', formatApiError(e)),
|
||||
})
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
|
||||
Reference in New Issue
Block a user