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:
@@ -144,11 +144,8 @@
|
||||
async function onApprove() {
|
||||
const ids = snapshotIds();
|
||||
if (ids.length === 0) return;
|
||||
const tid = toast.loading(`Keeping ${ids.length}…`);
|
||||
await withBusy(async () => {
|
||||
// PhotoPrism's approve is one-way (Quality jumps to 3+); there's
|
||||
// no /unapprove route. We fan out per-photo because there's no
|
||||
// batch endpoint either. Errors are tallied rather than aborting
|
||||
// the loop so a single bad UID doesn't block the rest.
|
||||
const { updated, errors } = await batchEdit(ids, (id) => approvePhoto(id), {
|
||||
onProgress: (_done, _total, completedId) => {
|
||||
const p = cachedPhoto(completedId);
|
||||
@@ -156,9 +153,9 @@
|
||||
}
|
||||
});
|
||||
if (errors.length) {
|
||||
toast.error(`Kept ${updated.length}; ${errors.length} failed`);
|
||||
toast.error(`Kept ${updated.length}; ${errors.length} failed`, { id: tid });
|
||||
} else {
|
||||
toast.success(`Kept ${ids.length}`);
|
||||
toast.success(`Kept ${ids.length}`, { id: tid });
|
||||
}
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
@@ -178,6 +175,7 @@
|
||||
async function onArchive() {
|
||||
const ids = snapshotIds();
|
||||
if (ids.length === 0) return;
|
||||
const tid = toast.loading(`Archiving ${ids.length}…`);
|
||||
await withBusy(async () => {
|
||||
try {
|
||||
await batchArchive(ids);
|
||||
@@ -185,14 +183,11 @@
|
||||
await batchRestore(ids);
|
||||
void qc.invalidateQueries({ queryKey: ['photos'] });
|
||||
});
|
||||
// Advance focus to the photo immediately after the archived
|
||||
// set before the multi-selection is dropped — lets the user
|
||||
// keep stepping through the timeline with X.
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
toast.success(`Archived ${ids.length}`);
|
||||
toast.success(`Archived ${ids.length}`, { id: tid });
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Archive failed');
|
||||
toast.error(err instanceof Error ? err.message : 'Archive failed', { id: tid });
|
||||
}
|
||||
}, { ids, label: 'Archiving', doneLabel: `Archived ${ids.length}` });
|
||||
}
|
||||
@@ -205,14 +200,15 @@
|
||||
? '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}…`);
|
||||
await withBusy(async () => {
|
||||
try {
|
||||
await batchDelete(ids);
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
toast.success(`Deleted ${ids.length}`);
|
||||
toast.success(`Deleted ${ids.length}`, { id: tid });
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Delete failed');
|
||||
toast.error(err instanceof Error ? err.message : 'Delete failed', { id: tid });
|
||||
}
|
||||
}, { ids, label: 'Deleting', doneLabel: `Deleted ${ids.length}` });
|
||||
}
|
||||
@@ -220,6 +216,7 @@
|
||||
async function onRestore() {
|
||||
const ids = snapshotIds();
|
||||
if (ids.length === 0) return;
|
||||
const tid = toast.loading(`Restoring ${ids.length}…`);
|
||||
await withBusy(async () => {
|
||||
try {
|
||||
await batchRestore(ids);
|
||||
@@ -229,9 +226,9 @@
|
||||
});
|
||||
focusAfter(ids);
|
||||
clearSelection();
|
||||
toast.success(`Restored ${ids.length}`);
|
||||
toast.success(`Restored ${ids.length}`, { id: tid });
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Restore failed');
|
||||
toast.error(err instanceof Error ? err.message : 'Restore failed', { id: tid });
|
||||
}
|
||||
}, { ids, label: 'Restoring', doneLabel: `Restored ${ids.length}` });
|
||||
}
|
||||
@@ -240,18 +237,16 @@
|
||||
const ids = snapshotIds();
|
||||
if (!ids.length) return;
|
||||
heapPickerOpen = false;
|
||||
const tid = toast.loading(`Adding ${ids.length} → ${heap.Title}…`);
|
||||
startBulk(`Adding to ${heap.Title}…`, ids);
|
||||
await withBusy(async () => {
|
||||
try {
|
||||
const { added } = await addToHeap(heap.UID, ids);
|
||||
qc.invalidateQueries({ queryKey: ['heaps'] });
|
||||
// PhotoPrism returns 200 even when nothing was added (UIDs
|
||||
// already present or unknown to the index) — surface the
|
||||
// real delta so the user isn't fooled by a green toast over
|
||||
// a no-op.
|
||||
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;
|
||||
@@ -260,10 +255,11 @@
|
||||
await delay(400);
|
||||
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);
|
||||
@@ -272,7 +268,7 @@
|
||||
clearSelection();
|
||||
} catch (err) {
|
||||
failBulk(ids);
|
||||
toast.error(err instanceof Error ? err.message : 'Add-to-heap failed');
|
||||
toast.error(err instanceof Error ? err.message : 'Add-to-heap failed', { id: tid });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user