fix(metadata-panel): apply mutation responses synchronously, no second GET

Single-photo updateMutation only invalidated, so the panel waited for
a follow-up GET /photos/{id} round-trip before showing the new value —
felt as a 200–500 ms lag after every taken_at / rating / notes edit.
Use the PATCH response (already the updated row) to merge into the
per-photo cache and patch every cached timeline list in place.

Bulk taken_at had the same shape: invalidate-only, no optimistic. When
the user dropped back from N selected to one of the modified photos
the panel briefly showed the pre-edit value. Move both bulkSetTakenAt
and bulkSetTakenAtMap into useBulkPhotoMutations alongside the rating/
color/notes pattern, with the same snapshot+patch+rollback primitives.

Tags + bulk tags still invalidate-only — separate change if needed.
This commit is contained in:
Claudio
2026-05-11 21:51:14 +02:00
parent ea08d7e3e8
commit abe5c1ec6b
3 changed files with 105 additions and 28 deletions

View File

@@ -189,5 +189,75 @@ export function useBulkPhotoMutations() {
onSuccess: invalidatePhotoQueries,
})
return { bulkRating, bulkColor, bulkNotes, invalidatePhotoQueries }
// Bulk taken-at: same iso applied to every selected id. Mirror the
// server's manual-source flip so the badge updates without waiting on
// the reconcile fetch. has_date_warning isn't patched optimistically —
// it's recomputed server-side and the post-success invalidate carries
// it back. Per-photo errors (unsupported EXIF formats) come through
// the response shape, not as a thrown error, so onError isn't the
// place to roll back; instead we rely on the invalidate to bring back
// any rows the server skipped.
const bulkTakenAt = useMutation({
mutationFn: ({ ids, iso }: { ids: string[]; iso: string }) =>
photosApi.bulkSetTakenAt(ids, iso),
onMutate: ({ ids, iso }) => {
const snapshot = snapshotPhotos(ids)
patchPhotos(ids, { taken_at: iso, taken_at_source: 'manual' })
return { snapshot }
},
onError: (e, _vars, ctx) => {
if (ctx?.snapshot) restoreFromSnapshot(ctx.snapshot)
toast.error('Date update failed', formatApiError(e))
},
onSuccess: invalidatePhotoQueries,
})
// Bulk taken-at with a per-photo iso map. Same shape as bulkTakenAt
// otherwise — patch each row to its own iso, snapshot the lot for
// rollback, reconcile via invalidate.
const bulkTakenAtMap = useMutation({
mutationFn: (map: Record<string, string>) =>
photosApi.bulkSetTakenAtMap(map),
onMutate: (map) => {
const ids = Object.keys(map)
const snapshot = snapshotPhotos(ids)
const want = new Set(ids)
// Per-id patch: can't reuse patchPhotos (single-patch primitive),
// so walk the same caches inline.
queryClient.setQueriesData<Photo[]>({ queryKey: ['photos'] }, (prev) =>
prev
? prev.map((p) =>
want.has(p.id)
? { ...p, taken_at: map[p.id], taken_at_source: 'manual' }
: p,
)
: prev,
)
for (const id of ids) {
const cur = queryClient.getQueryData<Photo>(['photo', id])
if (cur) {
queryClient.setQueryData<Photo>(['photo', id], {
...cur,
taken_at: map[id],
taken_at_source: 'manual',
})
}
}
return { snapshot }
},
onError: (e, _vars, ctx) => {
if (ctx?.snapshot) restoreFromSnapshot(ctx.snapshot)
toast.error('Date update failed', formatApiError(e))
},
onSuccess: invalidatePhotoQueries,
})
return {
bulkRating,
bulkColor,
bulkNotes,
bulkTakenAt,
bulkTakenAtMap,
invalidatePhotoQueries,
}
}