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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user