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:
@@ -21,6 +21,7 @@ import { heaps as heapsApi, downloads, type Heap } from '../../services/api'
|
||||
import { useFilterStore } from '../../store/filterStore'
|
||||
import { toast } from '../ToastContainer'
|
||||
import { PHOTO_DRAG_MIME } from '../timeline/PhotoThumbnail'
|
||||
import { formatApiError } from '../../lib/apiError'
|
||||
import { HeapConvertDialog } from './HeapConvertDialog'
|
||||
import { ShareDialog } from '../sharing/ShareDialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -79,7 +80,7 @@ export function HeapsPanel() {
|
||||
setCreating(false)
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Failed to create heap', e.message || 'Unknown error'),
|
||||
toast.error('Failed to create heap', formatApiError(e)),
|
||||
})
|
||||
|
||||
const setActiveMutation = useMutation({
|
||||
@@ -90,7 +91,7 @@ export function HeapsPanel() {
|
||||
toast.success('Active heap', `Now adding to "${heap.name}" with T`)
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Failed to set active', e.message || 'Unknown error'),
|
||||
toast.error('Failed to set active', formatApiError(e)),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
@@ -103,7 +104,7 @@ export function HeapsPanel() {
|
||||
}
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Failed to delete heap', e.message || 'Unknown error'),
|
||||
toast.error('Failed to delete heap', formatApiError(e)),
|
||||
})
|
||||
|
||||
const renameMutation = useMutation({
|
||||
@@ -111,7 +112,7 @@ export function HeapsPanel() {
|
||||
heapsApi.update(heapId, { name }),
|
||||
onSuccess: () => invalidate(),
|
||||
onError: (e: any) =>
|
||||
toast.error('Failed to rename heap', e.message || 'Unknown error'),
|
||||
toast.error('Failed to rename heap', formatApiError(e)),
|
||||
})
|
||||
|
||||
const duplicateMutation = useMutation({
|
||||
@@ -121,7 +122,7 @@ export function HeapsPanel() {
|
||||
toast.success('Heap duplicated', heap.name)
|
||||
},
|
||||
onError: (e: any) =>
|
||||
toast.error('Failed to duplicate heap', e.message || 'Unknown error'),
|
||||
toast.error('Failed to duplicate heap', formatApiError(e)),
|
||||
})
|
||||
|
||||
// Drop handler: add the dragged photos to the target heap. Optimistically
|
||||
@@ -142,7 +143,7 @@ export function HeapsPanel() {
|
||||
if (ctx?.previous) {
|
||||
queryClient.setQueryData(['heap-photo-ids', vars.heapId], ctx.previous)
|
||||
}
|
||||
toast.error('Failed to add to heap', e.message || 'Unknown error')
|
||||
toast.error('Failed to add to heap', formatApiError(e))
|
||||
},
|
||||
onSuccess: (data, vars) => {
|
||||
const heap = heaps.find((h) => h.id === vars.heapId)
|
||||
|
||||
Reference in New Issue
Block a user