fix(journey): saving a suggestion turns it into an entry

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
This commit is contained in:
Maurice
2026-08-21 12:47:30 +02:00
committed by Maurice
parent df139a2d96
commit 5e78069223
4 changed files with 10 additions and 9 deletions
@@ -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 () => {
@@ -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
@@ -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) {
@@ -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 () => {