diff --git a/web/src/lib/services/photoActions.ts b/web/src/lib/services/photoActions.ts new file mode 100644 index 0000000..e377365 --- /dev/null +++ b/web/src/lib/services/photoActions.ts @@ -0,0 +1,68 @@ +/** + * Shared action helpers for the photo-app's review / archive flows. + * + * Three callers reach for these: + * 1. `gridKeyNav` — the document-level keyboard action (S / X) + * 2. `BulkActionBar` — the footer button row that appears on selection + * 3. `/review` CauseGroupCard — per-cause "Dismiss all" / "Archive all" + * + * Centralising the toast text, undo wiring, focus advance, and cache + * invalidation here keeps the three surfaces in lockstep — change the + * toast wording in one place and everywhere shows the same verb. + */ + +import { toast } from 'svelte-sonner'; +import { batchEdit } from './batch'; +import { invalidatePhotos } from './bulk'; +import { approvePhoto, batchArchive, batchRestore } from './photoprism'; +import { queryClient } from '$lib/queryClient'; +import { clearSelection, focusAfter } from '$lib/stores/selection.svelte'; +import { push as pushUndo } from '$lib/stores/undo.svelte'; + +/** + * Dismiss photos out of the review queue by bumping their quality + * score above PhotoPrism's threshold. One-way: there's no + * `/unapprove` endpoint, so we do NOT push an undo entry — a re-keyed + * action would just be a no-op on already-approved photos. + */ +export async function dismissPhotos(uids: string[]): Promise { + if (uids.length === 0) return; + const { updated, errors } = await batchEdit(uids, (id) => approvePhoto(id)); + // Advance focus past the dismissed set before the timeline refetches + // so the cursor doesn't snap back to photo[0]; clear the now-stale + // selection ring for the same reason. + focusAfter(uids); + clearSelection(); + invalidatePhotos(uids); + void queryClient.invalidateQueries({ queryKey: ['review-groups'] }); + if (errors.length) { + toast.error(`Dismissed ${updated.length}; ${errors.length} failed`, { + description: errors[0].message + }); + return; + } + toast.success(`Dismissed ${uids.length}`); +} + +/** + * Archive photos. Reversible via the undo stack (Restore on ⌘Z). + */ +export async function archivePhotos(uids: string[]): Promise { + if (uids.length === 0) return; + try { + await batchArchive(uids); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Archive failed'); + return; + } + pushUndo(`Archived ${uids.length}`, async () => { + await batchRestore(uids); + invalidatePhotos(uids); + void queryClient.invalidateQueries({ queryKey: ['review-groups'] }); + }); + focusAfter(uids); + clearSelection(); + invalidatePhotos(uids); + void queryClient.invalidateQueries({ queryKey: ['review-groups'] }); + toast.success(`Archived ${uids.length}`); +} diff --git a/web/src/routes/review/+page.svelte b/web/src/routes/review/+page.svelte index ab4a12f..9d7926e 100644 --- a/web/src/routes/review/+page.svelte +++ b/web/src/routes/review/+page.svelte @@ -1,12 +1,15 @@ @@ -81,8 +102,6 @@ Review {#if tabs.length > 0} -
{#each tabs as t (t.id)}