feat: loading toasts for all photo actions
Add loading→success/error toast transition to every bulk operation (archive, restore, delete, approve, add-to-heap, metadata patch). Also wires gridKeyNav + CauseGroupCard into the bulkAction store so keyboard-triggered actions show the same per-tile pending/done/error feedback as BulkActionBar buttons.
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
toggle
|
||||
} from '$lib/stores/selection.svelte';
|
||||
import { popAndRun, push as pushUndo } from '$lib/stores/undo.svelte';
|
||||
import { startBulk, doneBulk, failBulk, setDetail } from '$lib/stores/bulkAction.svelte';
|
||||
import { openPreview, toggleLeftSidebar, toggleRightSidebar, view } from '$lib/stores/view.svelte';
|
||||
|
||||
/**
|
||||
@@ -180,34 +181,24 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
target = !(first?.Archived ?? false);
|
||||
}
|
||||
|
||||
// PhotoPrism's photo PUT silently drops the Archived field — the
|
||||
// only working path is /api/v1/batch/photos/{archive,restore}. The
|
||||
// previous patchTargets call PUT'd `{Archived: true}` and got a 200
|
||||
// back, so the toast fired but nothing moved.
|
||||
const opLabel = target ? 'Archiving' : 'Restoring';
|
||||
const doneLabel = target ? `Archived ${ids.length}` : `Restored ${ids.length}`;
|
||||
const tid = toast.loading(`${opLabel} ${ids.length}…`);
|
||||
startBulk(`${opLabel}…`, ids);
|
||||
try {
|
||||
if (target) await batchArchive(ids);
|
||||
else await batchRestore(ids);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Archive failed');
|
||||
failBulk(ids);
|
||||
toast.error(err instanceof Error ? err.message : 'Archive/restore failed', { id: tid });
|
||||
return;
|
||||
}
|
||||
// Move focus forward before the photos query refetches, so the
|
||||
// user can keep X-ing through the timeline without their cursor
|
||||
// snapping back to photo[0]. Walks past every uid we just
|
||||
// archived/restored — relevant when the cull targets came from a
|
||||
// multi-selection rather than the single focused tile.
|
||||
doneBulk(doneLabel, ids);
|
||||
focusAfter(ids);
|
||||
// Drop the now-stale selection set. The archived UIDs are about
|
||||
// to leave the timeline on refetch, but the SvelteSet membership
|
||||
// keeps the selection ring on them until then — confusing for
|
||||
// the user and a footgun if they Ctrl-click to add more and end
|
||||
// up re-archiving the same photos. The BulkActionBar button path
|
||||
// clears for the same reason; mirror it here.
|
||||
clearSelection();
|
||||
invalidatePhotos(ids);
|
||||
const label = target ? `Archived ${ids.length}` : `Restored ${ids.length}`;
|
||||
toast.success(label);
|
||||
pushUndo(label, async () => {
|
||||
toast.success(doneLabel, { id: tid });
|
||||
pushUndo(doneLabel, async () => {
|
||||
if (target) await batchRestore(ids);
|
||||
else await batchArchive(ids);
|
||||
invalidatePhotos(ids);
|
||||
@@ -232,16 +223,20 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
? 'Permanently delete this photo? This cannot be undone.'
|
||||
: `Permanently delete ${ids.length} photos? This cannot be undone.`;
|
||||
if (!confirm(msg)) return;
|
||||
const tid = toast.loading(`Deleting ${ids.length}…`);
|
||||
startBulk('Deleting…', ids);
|
||||
try {
|
||||
await batchDelete(ids);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Delete failed');
|
||||
failBulk(ids);
|
||||
toast.error(err instanceof Error ? err.message : 'Delete failed', { id: tid });
|
||||
return;
|
||||
}
|
||||
doneBulk(`Deleted ${ids.length}`, ids);
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
invalidatePhotos(ids);
|
||||
toast.success(`Deleted ${ids.length}`);
|
||||
toast.success(`Deleted ${ids.length}`, { id: tid });
|
||||
}
|
||||
|
||||
/** Approve cull targets — clears them out of the review pile by
|
||||
@@ -257,21 +252,27 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { updated, errors } = await batchEdit(ids, (id) => approvePhoto(id));
|
||||
// Approve moves photos out of the review pile, so the same
|
||||
// stale-selection trap as archive/delete applies — advance focus
|
||||
// past the approved set and drop the now-irrelevant selection
|
||||
// before invalidate refetches the (smaller) view.
|
||||
const tid = toast.loading(`Keeping ${ids.length}…`);
|
||||
startBulk('Keeping…', ids);
|
||||
const { updated, errors } = await batchEdit(ids, (id) => approvePhoto(id), {
|
||||
onProgress: (_done, _total, completedId) => {
|
||||
const p = cachedPhoto(completedId);
|
||||
if (p) setDetail(p.FileName ?? completedId);
|
||||
}
|
||||
});
|
||||
if (errors.length) {
|
||||
failBulk(ids);
|
||||
toast.error(`Kept ${updated.length}; ${errors.length} failed`, {
|
||||
id: tid,
|
||||
description: errors[0].message
|
||||
});
|
||||
} else {
|
||||
doneBulk(`Kept ${ids.length}`, ids);
|
||||
toast.success(`Kept ${ids.length}`, { id: tid });
|
||||
}
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
invalidatePhotos(ids);
|
||||
if (errors.length) {
|
||||
toast.error(`Kept ${updated.length}; ${errors.length} failed`, {
|
||||
description: errors[0].message
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success(`Kept ${ids.length}`);
|
||||
}
|
||||
|
||||
// ── S chord (add-to-heap) ────────────────────────────────────────────
|
||||
@@ -297,25 +298,28 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const tid = toast.loading(`Adding ${ids.length} → ${heap.Title}…`);
|
||||
startBulk(`Adding to ${heap.Title}…`, ids);
|
||||
try {
|
||||
const { added } = await addToHeap(heap.UID, ids);
|
||||
void queryClient.invalidateQueries({ queryKey: ['heaps'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['photos'] });
|
||||
// PhotoPrism returns 200 even when nothing was added — distinguish
|
||||
// "really added N" from "skipped all N" so the toast tells the
|
||||
// truth.
|
||||
if (added.length === 0) {
|
||||
failBulk(ids);
|
||||
toast.error(`Nothing added to ${heap.Title}`, {
|
||||
id: tid,
|
||||
description: `The server rejected all ${ids.length} UIDs (already in heap, or not indexed).`
|
||||
});
|
||||
return;
|
||||
}
|
||||
doneBulk(`Added ${added.length} → ${heap.Title}`, ids);
|
||||
if (added.length < ids.length) {
|
||||
toast.success(`Added ${added.length}/${ids.length} → ${heap.Title}`, {
|
||||
id: tid,
|
||||
description: 'The rest were already in this heap.'
|
||||
});
|
||||
} else {
|
||||
toast.success(`Added ${added.length} → ${heap.Title}`);
|
||||
toast.success(`Added ${added.length} → ${heap.Title}`, { id: tid });
|
||||
}
|
||||
pushUndo(`Added ${added.length} to ${heap.Title}`, async () => {
|
||||
await removeFromHeap(heap.UID, added);
|
||||
@@ -323,7 +327,8 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) {
|
||||
void queryClient.invalidateQueries({ queryKey: ['photos'] });
|
||||
});
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Add-to-heap failed');
|
||||
failBulk(ids);
|
||||
toast.error(err instanceof Error ? err.message : 'Add-to-heap failed', { id: tid });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user