diff --git a/web/src/lib/actions/gridKeyNav.ts b/web/src/lib/actions/gridKeyNav.ts index fc62d2f..8d70ad7 100644 --- a/web/src/lib/actions/gridKeyNav.ts +++ b/web/src/lib/actions/gridKeyNav.ts @@ -1,6 +1,6 @@ import { toast } from 'svelte-sonner'; import { batchEdit } from '$lib/services/batch'; -import { invalidatePhotos, evictFromCache } from '$lib/services/bulk'; +import { invalidatePhotos } from '$lib/services/bulk'; import { addToHeap, approvePhoto, @@ -194,10 +194,10 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return; } doneBulk(doneLabel, ids); - evictFromCache(ids); focusAfter(ids); clearSelection(); invalidatePhotos(ids); + void queryClient.invalidateQueries({ queryKey: ['marks'] }); toast.success(doneLabel, { id: tid }); pushUndo(doneLabel, async () => { if (target) await batchRestore(ids); @@ -234,10 +234,10 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return; } doneBulk(`Deleted ${ids.length}`, ids); - evictFromCache(ids); focusAfter(ids); clearSelection(); invalidatePhotos(ids); + void queryClient.invalidateQueries({ queryKey: ['marks'] }); toast.success(`Deleted ${ids.length}`, { id: tid }); } @@ -272,7 +272,6 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { doneBulk(`Kept ${ids.length}`, ids); toast.success(`Kept ${ids.length}`, { id: tid }); } - evictFromCache(ids); focusAfter(ids); clearSelection(); invalidatePhotos(ids); diff --git a/web/src/lib/components/review/CauseGroupCard.svelte b/web/src/lib/components/review/CauseGroupCard.svelte index a64d53b..2910f46 100644 --- a/web/src/lib/components/review/CauseGroupCard.svelte +++ b/web/src/lib/components/review/CauseGroupCard.svelte @@ -13,7 +13,6 @@ import { useQueryClient } from '@tanstack/svelte-query'; import { toast } from 'svelte-sonner'; import { batchArchive } from '$lib/services/photoprism'; - import { evictFromCache } from '$lib/services/bulk'; import { startBulk, doneBulk, failBulk } from '$lib/stores/bulkAction.svelte'; import PhotoGrid from '$lib/components/timeline/PhotoGrid.svelte'; import { type ReviewGroup } from '$lib/services/adapters/review'; @@ -36,11 +35,11 @@ startBulk(`Archiving…`, uids); try { await batchArchive(uids); - evictFromCache(uids); doneBulk(`Archived ${uids.length}`, uids); toast.success(`Archived ${uids.length}`, { id: tid }); void qc.invalidateQueries({ queryKey: ['review-groups'] }); void qc.invalidateQueries({ queryKey: ['photos'] }); + void qc.invalidateQueries({ queryKey: ['marks'] }); } catch (err) { failBulk(uids); toast.error(err instanceof Error ? err.message : 'Archive all failed', { id: tid }); diff --git a/web/src/lib/components/timeline/BulkActionBar.svelte b/web/src/lib/components/timeline/BulkActionBar.svelte index b5335b8..df2fae7 100644 --- a/web/src/lib/components/timeline/BulkActionBar.svelte +++ b/web/src/lib/components/timeline/BulkActionBar.svelte @@ -14,7 +14,6 @@ } from '$lib/services/photoprism'; import { batchEdit } from '$lib/services/batch'; import { acceptDateAndKeep, cachedPhoto } from '$lib/services/photoActions'; - import { evictFromCache } from '$lib/services/bulk'; import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath'; import { photoNameAndDir } from '$lib/types/photoprism'; import { @@ -130,6 +129,7 @@ const result = await fn(); if (bulk) { doneBulk(bulk.doneLabel, bulk.ids); + await delay(200); } return result; } catch (e) { @@ -138,6 +138,8 @@ } finally { busy = false; void qc.invalidateQueries({ queryKey: ['photos'] }); + void qc.invalidateQueries({ queryKey: ['marks'] }); + void qc.invalidateQueries({ queryKey: ['review-groups'] }); } } @@ -152,7 +154,6 @@ setDetail(p?.FileName ?? completedId); } }); - evictFromCache(ids); if (errors.length) { toast.error(`Kept ${updated.length}; ${errors.length} failed`, { id: tid }); } else { @@ -180,7 +181,6 @@ await withBusy(async () => { try { await batchArchive(ids); - evictFromCache(ids); pushUndo(`Archived ${ids.length}`, async () => { await batchRestore(ids); void qc.invalidateQueries({ queryKey: ['photos'] }); @@ -206,7 +206,6 @@ await withBusy(async () => { try { await batchDelete(ids); - evictFromCache(ids); focusAfter(ids); clearSelection(); toast.success(`Deleted ${ids.length}`, { id: tid }); @@ -223,7 +222,6 @@ await withBusy(async () => { try { await batchRestore(ids); - evictFromCache(ids); pushUndo(`Restored ${ids.length}`, async () => { await batchArchive(ids); void qc.invalidateQueries({ queryKey: ['photos'] }); diff --git a/web/src/lib/services/bulk.ts b/web/src/lib/services/bulk.ts index 15177d4..f6cd9fa 100644 --- a/web/src/lib/services/bulk.ts +++ b/web/src/lib/services/bulk.ts @@ -32,36 +32,12 @@ export function invalidatePhotos(uids: string[]): void { } } -/** - * Remove uids from every cached infinite photo-list query so the grid - * updates instantly instead of waiting for a refetch round-trip. Call - * after the API confirms the mutation, then still invalidate for eventual - * sync. Only targets infinite queries (those with a `pages` array) — - * flat list caches like marks-pool, with-notes, keywords, etc. are left - * intact so tag/category drill pages don't lose referenced photos. - */ -export function evictFromCache(uids: string[]): void { - const uidSet = new Set(uids); - const lists = queryClient.getQueriesData< - { pages?: PpPhoto[][] } | { pages?: unknown[] } - >({ - queryKey: ['photos'] - }); - for (const [key, data] of lists) { - if (!data) continue; - if ('pages' in data && Array.isArray((data as { pages?: unknown[] }).pages)) { - const pages = (data as { pages: PpPhoto[][] }).pages; - let changed = false; - const filtered = pages.map((page: PpPhoto[]) => { - const f = page.filter((p) => !uidSet.has(p.UID)); - if (f.length < page.length) changed = true; - return f; - }); - if (changed) { - queryClient.setQueryData(key, { ...data, pages: filtered }); - } - } - } +export function invalidateAllPhotoCaches(): void { + void queryClient.invalidateQueries({ queryKey: ['photos'] }); + void queryClient.invalidateQueries({ queryKey: ['marks'] }); + void queryClient.invalidateQueries({ queryKey: ['labels'] }); + void queryClient.invalidateQueries({ queryKey: ['review-groups'] }); + void queryClient.invalidateQueries({ queryKey: ['heaps'] }); } /** diff --git a/web/src/lib/services/photoActions.ts b/web/src/lib/services/photoActions.ts index af54c29..13b02ac 100644 --- a/web/src/lib/services/photoActions.ts +++ b/web/src/lib/services/photoActions.ts @@ -13,7 +13,7 @@ import { toast } from 'svelte-sonner'; import { batchEdit } from './batch'; -import { invalidatePhotos, evictFromCache } from './bulk'; +import { invalidatePhotos } from './bulk'; import { approvePhoto, batchArchive, @@ -68,7 +68,6 @@ export async function dismissPhotos(uids: string[]): Promise { if (uids.length === 0) return; const tid = toast.loading(`Dismissing ${uids.length}…`); const { updated, errors } = await batchEdit(uids, (id) => approvePhoto(id)); - evictFromCache(uids); focusAfter(uids); clearSelection(); invalidatePhotos(uids); @@ -108,7 +107,6 @@ export async function acceptDateAndKeep(uids: string[]): Promise { await approvePhoto(id); return id; }); - evictFromCache(uids); focusAfter(uids); clearSelection(); invalidatePhotos(uids); @@ -135,7 +133,6 @@ export async function archivePhotos(uids: string[]): Promise { toast.error(err instanceof Error ? err.message : 'Archive failed', { id: tid }); return; } - evictFromCache(uids); pushUndo(`Archived ${uids.length}`, async () => { await batchRestore(uids); invalidatePhotos(uids);