Review view: reuse timeline action surface, simpler card UX
The /review cards now share the timeline's PhotoGrid, gridKeyNav,
BulkActionBar, and global selection store instead of carrying parallel
implementations. The card is reduced to a header (title + count +
Dismiss / Archive all), an advisory caption, and a PhotoGrid; keyboard
nav, hover affordances, multi-select, and bulk verbs come from the
shared machinery.
- New web/src/lib/services/photoActions.ts holds the canonical
dismissPhotos / archivePhotos helpers (toast wording, focus advance,
undo push, ['photos'] + ['review-groups'] cache invalidation).
BulkActionBar.onApprove / onArchive and gridKeyNav.approveCullTargets
/ toggleArchive('archive') route through it. CauseGroupCard's
Dismiss / Archive-all buttons call the same helpers - one code path
from any surface.
- Approve verb renamed to "Dismiss" across BulkActionBar, gridKeyNav
toasts ("Kept N" -> "Dismissed N"), and the new review card. The
BulkActionBar Clear/Dismiss clear button is just "Clear" now so the
verb only means the action.
- /review sets filters.section='review' on mount and restores on
unmount, which is what swings the shared action surface into review
semantics; an effect clears the selection on tab change so a
previously-selected photo from another cause can't be hit by a new
tab's bulk verb.
- The route mounts BulkActionBar at the bottom and swaps the right
aside to BulkMetadataSidebar when selection.ids.size >= 2 - same as
the timeline; gives the user a one-shot "apply this Date / Caption /
Keyword to all selected" affordance for EXIF-stripped batches.
- CauseGroupCard drops its bespoke keyboard handler, ResizeObserver,
focusedIdx state, per-tile hover Approve/Archive buttons, confirm()
dialogs, and toast.loading worker loop. The unused CauseBadges
component is removed.
This commit is contained in:
68
web/src/lib/services/photoActions.ts
Normal file
68
web/src/lib/services/photoActions.ts
Normal file
@@ -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<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user