Files
mule-image/web/src/lib/services/batch.ts
dtoro da63ad769a feat: bulk action progress — header pill + per-thumbnail states
- New bulkAction store: tracks active/label/detail state for the pill
  and a Map<uid, pending|done|error> for per-tile overlays
- Extract StatusPill.svelte from IndexerStatusPill (generic active/label/detail
  props); IndexerStatusPill becomes a one-line wrapper
- +layout.svelte: render a second StatusPill driven by bulkAction store,
  alongside the indexer pill in the AnimatedMule header
- BulkActionBar: extend withBusy with optional BulkConfig (ids/label/doneLabel);
  pending tiles dim + spinner on start, green checkmark flashes for 400ms before
  cache invalidation removes them; red overlay on error, auto-clears after 2s
- onApprove/batchEdit: wire onProgress callback to setDetail so the pill shows
  the filename currently being processed during fan-out keep operations
- batch.ts: add completedId as third arg to onProgress (backwards-compatible)
- PhotoTile: derive bulkState from store; pending/done/error overlays sit above
  the selection tint; hover-video guarded against pending tiles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:43:12 +02:00

51 lines
1.3 KiB
TypeScript

/**
* Fan-out helper used wherever PhotoPrism lacks a true batch endpoint.
* Bounded concurrency keeps the indexer happy on slow hosts; each item's
* result is collected and the aggregate `{updated, errors[]}` mirrors the
* shape the legacy mule-image bulk endpoint returned, so existing toast
* + undo plumbing slots in without changes.
*/
export interface BatchResult<T> {
updated: T[];
errors: { id: string; message: string }[];
}
export interface BatchOptions {
concurrency?: number;
onProgress?: (done: number, total: number, completedId: string) => void;
}
export async function batchEdit<T>(
ids: string[],
fn: (id: string) => Promise<T>,
opts: BatchOptions = {}
): Promise<BatchResult<T>> {
const concurrency = Math.max(1, opts.concurrency ?? 8);
const updated: T[] = [];
const errors: { id: string; message: string }[] = [];
let i = 0;
let done = 0;
async function worker() {
while (true) {
const idx = i++;
if (idx >= ids.length) return;
const id = ids[idx];
try {
updated.push(await fn(id));
} catch (err) {
errors.push({ id, message: err instanceof Error ? err.message : String(err) });
} finally {
done++;
opts.onProgress?.(done, ids.length, id);
}
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, ids.length) }, () => worker())
);
return { updated, errors };
}