From 5e78069223f1679bbf51055e05157b1b09556436 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 21 Aug 2026 12:47:30 +0200 Subject: [PATCH] fix(journey): saving a suggestion turns it into an entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A trip visit shows up in the journey as a suggestion, and opening one puts you in the entry editor. Pressing Save there only promoted it when a story, a photo or a linked photo had been added — a title, a mood, the weather or a set of tags all left it a suggestion, and so did saving it untouched. From the outside that looks like the save did nothing. An explicit Save is the user saying this is their entry now. handleSave only ever runs from the Save button, so there is no autosave to misread as intent. Closes #2008 --- .../Journey/JourneyDetailPageEntryEditor.test.tsx | 4 ++-- .../components/Journey/JourneyDetailPageEntryEditor.tsx | 4 +++- client/src/mobile/screens/journey/MJourneyEntrySheet.tsx | 7 +++---- .../tests/unit/mobile/journey/MJourneyEntrySheet.test.tsx | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx b/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx index 8f5fa14d5..9a5f0e37e 100644 --- a/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx +++ b/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx @@ -539,14 +539,14 @@ describe('EntryEditor', () => { expect(onUploadPhotos).not.toHaveBeenCalled() }) - it('FE-JRN-EDITOR-026: leaves a skeleton without any content a skeleton', async () => { + it('FE-JRN-EDITOR-026: promotes a skeleton the user saved without editing (#2008)', async () => { const user = userEvent.setup() const { onSave } = mountEditor(buildEntry({ id: 21, type: 'skeleton', title: 'Venice' })) await user.click(screen.getByRole('button', { name: 'Save' })) await waitFor(() => expect(onSave).toHaveBeenCalled()) - expect(onSave.mock.calls[0][0]).toMatchObject({ type: undefined }) + expect(onSave.mock.calls[0][0]).toMatchObject({ type: 'entry' }) }) it('FE-JRN-EDITOR-027: removes a con row again', async () => { diff --git a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx index 7cba4e956..19f57f0ab 100644 --- a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx +++ b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx @@ -144,7 +144,9 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, mood: mood || null, weather: weather || null, pros_cons: { pros: pros.filter(p => p.trim()), cons: cons.filter(c => c.trim()) }, - type: ((entry.type === 'skeleton' && (story.trim() || pendingFiles.length > 0 || pendingLinkIds.length > 0 || pendingProviderGroups.length > 0)) ? 'entry' : undefined), + // An explicit Save is the user saying this suggestion is now their entry — + // it does not need a story to earn that (#2008). + type: entry.type === 'skeleton' ? 'entry' : undefined, }, persistedEntryIdRef.current ?? undefined) if (entryId > 0) persistedEntryIdRef.current = entryId // upload queued files after entry is created diff --git a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx index 9be546706..33f4db055 100644 --- a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx +++ b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx @@ -204,10 +204,9 @@ export default function MJourneyEntrySheet({ weather: weather || null, tags: tags.filter(tag => tag.trim()), pros_cons: { pros: pros.filter(p => p.trim()), cons: cons.filter(c => c.trim()) }, - type: entry.type === 'skeleton' - && (story.trim() || pendingFiles.length > 0 || pendingLinkIds.length > 0 || pendingProviderGroups.length > 0) - ? 'entry' - : undefined, + // An explicit Save is the user saying this suggestion is now their entry — + // it does not need a story to earn that (#2008). + type: entry.type === 'skeleton' ? 'entry' : undefined, }, persistedEntryIdRef.current ?? undefined) if (entryId > 0) persistedEntryIdRef.current = entryId if (pendingFiles.length > 0 && entryId) { diff --git a/client/tests/unit/mobile/journey/MJourneyEntrySheet.test.tsx b/client/tests/unit/mobile/journey/MJourneyEntrySheet.test.tsx index ccdbb0a6e..002f4384b 100644 --- a/client/tests/unit/mobile/journey/MJourneyEntrySheet.test.tsx +++ b/client/tests/unit/mobile/journey/MJourneyEntrySheet.test.tsx @@ -453,14 +453,14 @@ describe('MJourneyEntrySheet full editor', () => { expect(onSave.mock.calls[0][0]).toMatchObject({ type: 'entry' }); }); - it('FE-MOB-JENTRY-010: leaves an untouched skeleton a skeleton', async () => { + it('FE-MOB-JENTRY-010: promotes a skeleton the user saved without editing (#2008)', async () => { const user = userEvent.setup(); const { onSave } = mountSheet(buildEntry({ id: 9, type: 'skeleton', title: 'Venice' })); await user.click(screen.getByRole('button', { name: 'Save' })); await waitFor(() => expect(onSave).toHaveBeenCalled()); - expect(onSave.mock.calls[0][0]).toMatchObject({ type: undefined }); + expect(onSave.mock.calls[0][0]).toMatchObject({ type: 'entry' }); }); it('FE-MOB-JENTRY-011: queues picked files, reports progress and uploads them on save', async () => {