Files
mule-image/web/src/lib/stores/bulkAction.svelte.ts
dtoro 5be6fd9047 feat(archive): red-cross flash + instant tile removal on archive/delete
Archive/delete now flash a red cross then drop tiles from the grid
immediately, instead of a green check that lingered until the slow
server-reconcile refetch landed. Keyboard `x` archive previously never
called markRemoved, so tiles only vanished on refetch — that lag is gone.

- Add 'removed' bulk state + removedBulk() helper (red cross overlay)
- gridKeyNav archive/delete: removedBulk -> 500ms flash -> markRemoved,
  clearRemoved once refetch settles; restore stays green check
- BulkActionBar: BulkConfig.removing routes archive/delete through the
  red flash; approve/restore/label/note unchanged

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:45:38 +02:00

103 lines
3.4 KiB
TypeScript

/**
* Bulk-action status, written by BulkActionBar and read by the header
* StatusPill and individual PhotoTile overlays.
*
* State lifecycle:
* startBulk → pill spins, all target tiles go "pending"
* setDetail → pill shows the filename currently being processed (fan-out ops)
* doneBulk → pill shows completion label, tiles flash green, auto-clears after 3 s
* removedBulk→ destructive completion (archive / delete): tiles flash a red cross,
* then the caller hides them via markRemoved; map auto-clears after 3 s
* failBulk → tiles flash red, auto-clears after 2 s
*/
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
interface BulkActionState {
active: boolean;
label: string;
detail?: string;
}
export const bulkAction = $state<BulkActionState>({ active: false, label: '' });
// SvelteMap (not `$state(new Map())`) so a `.get(uid)` read in a PhotoTile
// reliably re-runs when the entry flips — the plain-Map proxy form wasn't
// re-rendering the timeline tiles' overlay.
export const bulkPhotoStates = new SvelteMap<string, 'pending' | 'done' | 'error' | 'removed'>();
/**
* UIDs hidden from the timeline grid the instant a removing action (archive /
* delete / restore) succeeds, so tiles vanish without waiting on the ~1s
* server-reconcile refetch. The caller clears each id once the refetch lands.
* This is a pure UI overlay — it never touches the query cache, so it can't
* corrupt the facet/drill caches the way a direct cache eviction did.
*/
export const removedIds = $state(new SvelteSet<string>());
export function markRemoved(ids: string[]): void {
for (const id of ids) removedIds.add(id);
}
export function clearRemoved(ids: string[]): void {
for (const id of ids) removedIds.delete(id);
}
let doneTimer: ReturnType<typeof setTimeout> | null = null;
export function startBulk(label: string, ids: string[]): void {
if (doneTimer !== null) {
clearTimeout(doneTimer);
doneTimer = null;
}
bulkPhotoStates.clear();
for (const id of ids) bulkPhotoStates.set(id, 'pending');
bulkAction.active = true;
bulkAction.label = label;
bulkAction.detail = undefined;
}
export function setDetail(path: string): void {
bulkAction.detail = path;
}
export function doneBulk(label: string, ids: string[]): void {
for (const id of ids) bulkPhotoStates.set(id, 'done');
bulkAction.active = false;
bulkAction.label = label;
bulkAction.detail = undefined;
if (doneTimer !== null) clearTimeout(doneTimer);
doneTimer = setTimeout(() => {
bulkAction.label = '';
bulkPhotoStates.clear();
doneTimer = null;
}, 3000);
}
/**
* Destructive completion (archive / permanent delete): flash a red cross on the
* target tiles instead of the green check. The caller hides the tiles via
* markRemoved shortly after the flash; this timer only cleans up the state map.
*/
export function removedBulk(label: string, ids: string[]): void {
for (const id of ids) bulkPhotoStates.set(id, 'removed');
bulkAction.active = false;
bulkAction.label = label;
bulkAction.detail = undefined;
if (doneTimer !== null) clearTimeout(doneTimer);
doneTimer = setTimeout(() => {
bulkAction.label = '';
bulkPhotoStates.clear();
doneTimer = null;
}, 3000);
}
export function failBulk(ids: string[]): void {
for (const id of ids) bulkPhotoStates.set(id, 'error');
bulkAction.active = false;
bulkAction.label = '';
bulkAction.detail = undefined;
setTimeout(() => {
for (const id of ids) bulkPhotoStates.delete(id);
}, 2000);
}