Fix ghost Gallery entries in journal timeline and public share

- deleteEntry now deletes photos with the entry instead of moving them
  to a hidden Gallery entry that becomes an undeletable ghost
- deletePhoto cleans up empty Gallery entries after last photo removed
- getJourneyFull and getPublicJourney filter out empty Gallery entries
  so existing ghosts are hidden in both internal and shared views
This commit is contained in:
Maurice
2026-04-13 00:18:45 +02:00
parent 0d7238300e
commit e179769a8f
2 changed files with 46 additions and 32 deletions
+34 -26
View File
@@ -114,15 +114,21 @@ export function getJourneyFull(journeyId: number, userId: number) {
(photosByEntry[p.entry_id] ||= []).push(p);
}
const enrichedEntries = entries.map(e => ({
...e,
tags: e.tags ? JSON.parse(e.tags) : [],
pros_cons: e.pros_cons ? JSON.parse(e.pros_cons) : null,
photos: photosByEntry[e.id] || [],
source_trip_name: e.source_trip_id
? (db.prepare('SELECT title FROM trips WHERE id = ?').get(e.source_trip_id) as { title: string } | undefined)?.title || null
: null,
}));
const enrichedEntries = entries
.filter(e => {
// hide empty Gallery entries (no photos, no story)
if (e.title === 'Gallery' && !e.story && !(photosByEntry[e.id]?.length)) return false;
return true;
})
.map(e => ({
...e,
tags: e.tags ? JSON.parse(e.tags) : [],
pros_cons: e.pros_cons ? JSON.parse(e.pros_cons) : null,
photos: photosByEntry[e.id] || [],
source_trip_name: e.source_trip_id
? (db.prepare('SELECT title FROM trips WHERE id = ?').get(e.source_trip_id) as { title: string } | undefined)?.title || null
: null,
}));
// linked trips
const trips = db.prepare(`
@@ -552,24 +558,16 @@ export function deleteEntry(entryId: number, userId: number): boolean {
if (!entry) return false;
if (!canEdit(entry.journey_id, userId)) return false;
// move photos to hidden Gallery entry so they stay in the gallery
const hasPhotos = db.prepare('SELECT 1 FROM journey_photos WHERE entry_id = ?').get(entryId);
if (hasPhotos) {
let gallery = db.prepare(
"SELECT id FROM journey_entries WHERE journey_id = ? AND title = 'Gallery' AND id != ?"
).get(entry.journey_id, entryId) as { id: number } | undefined;
if (!gallery) {
const now = ts();
const res = db.prepare(`
INSERT INTO journey_entries (journey_id, author_id, type, title, entry_date, sort_order, created_at, updated_at)
VALUES (?, ?, 'entry', 'Gallery', ?, 999, ?, ?)
`).run(entry.journey_id, entry.author_id, entry.entry_date, now, now);
gallery = { id: Number(res.lastInsertRowid) };
}
db.prepare('UPDATE journey_photos SET entry_id = ? WHERE entry_id = ?').run(gallery.id, entryId);
}
// delete photos along with the entry — no more orphan Gallery entries
db.prepare('DELETE FROM journey_photos WHERE entry_id = ?').run(entryId);
db.prepare('DELETE FROM journey_entries WHERE id = ?').run(entryId);
// clean up any empty Gallery entries in this journey
db.prepare(`
DELETE FROM journey_entries WHERE journey_id = ? AND title = 'Gallery'
AND id NOT IN (SELECT DISTINCT entry_id FROM journey_photos)
`).run(entry.journey_id);
broadcastJourneyEvent(entry.journey_id, 'journey:entry:deleted', { entryId }, userId);
return true;
}
@@ -673,6 +671,16 @@ export function deletePhoto(photoId: number, userId: number): (JourneyPhoto & {
if (!canEdit(photo.journey_id, userId)) return null;
db.prepare('DELETE FROM journey_photos WHERE id = ?').run(photoId);
// clean up empty Gallery entries left behind
const remaining = db.prepare('SELECT 1 FROM journey_photos WHERE entry_id = ?').get(photo.entry_id);
if (!remaining) {
const entry = db.prepare('SELECT * FROM journey_entries WHERE id = ?').get(photo.entry_id) as JourneyEntry | undefined;
if (entry && entry.title === 'Gallery' && !entry.story) {
db.prepare('DELETE FROM journey_entries WHERE id = ?').run(photo.entry_id);
}
}
return photo;
}
+12 -6
View File
@@ -111,12 +111,18 @@ export function getPublicJourney(token: string) {
(photosByEntry[p.entry_id] ||= []).push(p);
}
const enrichedEntries = entries.map(e => ({
...e,
tags: e.tags ? JSON.parse(e.tags) : [],
pros_cons: e.pros_cons ? JSON.parse(e.pros_cons) : null,
photos: photosByEntry[e.id] || [],
}));
const enrichedEntries = entries
.filter(e => {
// hide empty Gallery entries (no photos, no story)
if (e.title === 'Gallery' && !e.story && !(photosByEntry[e.id]?.length)) return false;
return true;
})
.map(e => ({
...e,
tags: e.tags ? JSON.parse(e.tags) : [],
pros_cons: e.pros_cons ? JSON.parse(e.pros_cons) : null,
photos: photosByEntry[e.id] || [],
}));
// Stats
const stats = {