Frontend: Enhance album creation logic to handle partial failures and improve user feedback

This commit is contained in:
Ömer Duran
2025-09-24 23:55:46 +03:00
parent 9368820102
commit e42f7c0b0f
2 changed files with 123 additions and 18 deletions

View File

@@ -143,25 +143,59 @@ export default {
return;
}
Promise.all(
namesToCreate.map((title) => {
const newAlbum = new Album({ Title: title, UID: "", Favorite: false });
return newAlbum
.save()
.then((a) => a?.UID)
.catch(() => null);
})
)
.then((created) => {
const createdUids = created.filter((u) => typeof u === "string" && u.length > 0);
this.$emit("confirm", [...uniqueExistingUids, ...createdUids]);
})
.catch((error) => {
console.error("Failed to create some albums:", error);
// Still emit successful ones if any exist
if (uniqueExistingUids.length > 0) {
this.$emit("confirm", uniqueExistingUids);
// Create albums in parallel and handle partial failures without closing the dialog
const creations = namesToCreate.map((title) => ({
title,
promise: new Album({ Title: title, UID: "", Favorite: false }).save(),
}));
Promise.allSettled(creations.map((c) => c.promise))
.then((results) => {
const createdAlbums = [];
const failedTitles = [];
results.forEach((res, idx) => {
const originalTitle = creations[idx].title;
if (res.status === "fulfilled" && res.value && res.value.UID) {
createdAlbums.push(res.value);
} else {
failedTitles.push(originalTitle);
}
});
if (failedTitles.length > 0) {
// Replace successfully created string tokens with album objects so they are not retried
const byTitle = new Map(createdAlbums.map((a) => [a.Title || a.title || "", a]));
this.selectedAlbums = (this.selectedAlbums || []).map((it) => {
if (typeof it === "string") {
const t = it.trim();
const created = byTitle.get(t);
return created ? created : it;
}
return it;
});
// Add created albums to the combobox items so they can be selected by object
const known = new Set((this.items || []).map((a) => a.UID));
createdAlbums.forEach((a) => {
if (a && a.UID && !known.has(a.UID)) {
this.items.push(a);
known.add(a.UID);
}
});
// Notify user and keep dialog open for corrections
this.$notify.error(
this.$gettext("Some albums could not be created. Please correct the names and try again.")
);
return; // Do not emit confirm; keep dialog open
}
// All created successfully → emit and let parent close the dialog
const createdUids = createdAlbums
.map((a) => a && a.UID)
.filter((u) => typeof u === "string" && u.length > 0);
this.$emit("confirm", [...uniqueExistingUids, ...createdUids]);
})
.finally(() => {
this.loading = false;

View File

@@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest';
import { processAlbumSelection } from '../../../src/common/albums.js';
function album(title, uid) {
return { Title: title, UID: uid };
}
describe('processAlbumSelection', () => {
it('trims whitespace and matches existing albums (case-insensitive)', () => {
const available = [album('Summer', '1')];
const selected = [' summer '];
const { processed, changed } = processAlbumSelection(selected, available);
expect(processed).toHaveLength(1);
expect(processed[0]).toEqual(available[0]);
expect(changed).toBe(true);
});
it('deduplicates identical UIDs and strings resolving to same album', () => {
const a1 = album('Trips', 't1');
const selected = [a1, a1, 'trips', 'TRIPS'];
const { processed, changed } = processAlbumSelection(selected, [a1]);
expect(processed).toHaveLength(1);
expect(processed[0]).toEqual(a1);
expect(changed).toBe(true);
});
it('keeps unmatched names as trimmed strings (for creation later)', () => {
const selected = [' New Album '];
const { processed, changed } = processAlbumSelection(selected, []);
expect(processed).toEqual(['New Album']);
// No structural change: only trimming does not count as change if lengths are equal and no replacements/drops
expect(changed).toBe(false);
});
it('drops empty / whitespace-only entries and reports change', () => {
const selected = [' ', '\n', '\t', ' Name '];
const { processed, changed } = processAlbumSelection(selected, []);
expect(processed).toEqual(['Name']);
expect(changed).toBe(true);
});
it('reconciles selection with new available items (race condition)', () => {
const selected = [' Road Trip '];
const availableThen = [album('Road Trip', 'rt01')];
const { processed, changed } = processAlbumSelection(selected, availableThen);
expect(processed).toHaveLength(1);
expect(processed[0]).toEqual(availableThen[0]);
expect(changed).toBe(true);
});
it('preserves existing album objects and prevents duplicates', () => {
const a = album('Family', 'fam1');
const selected = [a, { ...a }]; // two distinct objects with same UID
const { processed, changed } = processAlbumSelection(selected, [a]);
expect(processed).toHaveLength(1);
expect(processed[0]).toEqual(a);
expect(changed).toBe(true);
});
});