mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
Frontend: Enhance album creation logic to handle partial failures and improve user feedback
This commit is contained in:
@@ -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;
|
||||
|
71
frontend/tests/vitest/common/albums.test.js
Normal file
71
frontend/tests/vitest/common/albums.test.js
Normal 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);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user