From 65448ce186beb71e0b9aded55de0020f4a7e1112 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 17:33:43 +0200 Subject: [PATCH] fix(journey): a failed entry save on the phone says so instead of failing silently Saving a journey entry on the phone sheet while the server refused it (a locked journey, a dropped connection) left the sheet sitting there with no message, and the rejection went unhandled. The desktop dialog toasted journey.settings.saveFailed or the server's reason. The cause was the toastSaveError option of the shared useJourneyEntryForm hook: only the desktop set it, and without it handleSave rethrew into a click handler that nothing awaited. The option only kept the phone on the broken path, so it is gone and both views toast and keep the form open. Red before the fix: src/components/Journey/useJourneyEntryForm.test.ts FE-JRN-ENTRYFORM-021 failed because handleSave rejected with "{ response: { data: { error: 'Locked' } } }" instead of toasting. Changed assertions in FE-JRN-ENTRYFORM-007 (renamed to "a failed save toasts for the desktop dialog and the sheet alike"): the sheet's save was expected to reject (rejected true) with one toast in total; it now resolves (rejected false) and the toast count is 2. --- .../Journey/JourneyDetailPageEntryEditor.tsx | 2 +- .../Journey/useJourneyEntryForm.test.ts | 20 ++++++++++++++----- .../components/Journey/useJourneyEntryForm.ts | 4 ---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx index 6c8d9d5e2..e4c49980d 100644 --- a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx +++ b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx @@ -149,7 +149,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, handleSave, handleFileChange, pickGalleryPhoto, removePhoto, searchLocation, pickLocation, handleUseCurrentLocation, } = useJourneyEntryForm({ entry, journeyId, trips, galleryPhotos, onSave, onUploadPhotos, onAddProviderPhotos, onDone, - blankVerdictRow: true, dirtyOnCoordinates: true, toastSaveError: true, autoFillWeather: showWeather, + blankVerdictRow: true, dirtyOnCoordinates: true, autoFillWeather: showWeather, }) const [photoTab, setPhotoTab] = useState<'upload' | 'gallery' | 'external'>('upload') const [availableProviders, setAvailableProviders] = useState<{ id: string; name: string }[]>([]) diff --git a/client/src/components/Journey/useJourneyEntryForm.test.ts b/client/src/components/Journey/useJourneyEntryForm.test.ts index 43877bb63..49225ae4c 100644 --- a/client/src/components/Journey/useJourneyEntryForm.test.ts +++ b/client/src/components/Journey/useJourneyEntryForm.test.ts @@ -1,4 +1,4 @@ -// FE-JRN-ENTRYFORM-001 to FE-JRN-ENTRYFORM-020: the journey entry form logic behind both +// FE-JRN-ENTRYFORM-001 to FE-JRN-ENTRYFORM-021: the journey entry form logic behind both // the desktop editor dialog and the phone entry sheet. import { act, renderHook, waitFor } from '@testing-library/react'; import type React from 'react'; @@ -198,9 +198,9 @@ describe('useJourneyEntryForm', () => { ); }); - it('FE-JRN-ENTRYFORM-007: a failed save toasts for the desktop dialog and rejects for the sheet', async () => { + it('FE-JRN-ENTRYFORM-007: a failed save toasts for the desktop dialog and the sheet alike', async () => { const onSave = vi.fn().mockRejectedValue({ response: { data: { error: 'Locked' } } }); - const desktop = setup({ onSave, toastSaveError: true }); + const desktop = setup({ onSave, blankVerdictRow: true, dirtyOnCoordinates: true }); await act(() => desktop.result.current.handleSave()); expect(addToast).toHaveBeenCalledWith('Locked', 'error', undefined); expect(desktop.result.current.saving).toBe(false); @@ -208,9 +208,9 @@ describe('useJourneyEntryForm', () => { const sheet = setup({ onSave }); let rejected = false; await act(() => sheet.result.current.handleSave().catch(() => void (rejected = true))); - expect(rejected).toBe(true); + expect(rejected).toBe(false); expect(sheet.result.current.saving).toBe(false); - expect(addToast).toHaveBeenCalledTimes(1); + expect(addToast).toHaveBeenCalledTimes(2); }); it('FE-JRN-ENTRYFORM-008: links gallery photos picked before the first save', async () => { @@ -378,4 +378,14 @@ describe('useJourneyEntryForm', () => { .offersStatsToggle ).toBe(true); }); + + it('FE-JRN-ENTRYFORM-021: a failed save on the phone sheet toasts and keeps the form open', async () => { + const onSave = vi.fn().mockRejectedValue({ response: { data: { error: 'Locked' } } }); + const onDone = vi.fn(); + const sheet = setup({ onSave, onDone, withTags: true }); + await act(() => sheet.result.current.handleSave()); + expect(addToast).toHaveBeenCalledWith('Locked', 'error', undefined); + expect(onDone).not.toHaveBeenCalled(); + expect(sheet.result.current.saving).toBe(false); + }); }); diff --git a/client/src/components/Journey/useJourneyEntryForm.ts b/client/src/components/Journey/useJourneyEntryForm.ts index 33e998826..808f18d7d 100644 --- a/client/src/components/Journey/useJourneyEntryForm.ts +++ b/client/src/components/Journey/useJourneyEntryForm.ts @@ -49,8 +49,6 @@ export interface JourneyEntryFormOptions { withTags?: boolean; /** Moving the point alone, without renaming it, counts as an unsaved change (the desktop dialog). */ dirtyOnCoordinates?: boolean; - /** Toast a failed save and swallow it (the desktop dialog); otherwise the save rejects. */ - toastSaveError?: boolean; /** Show a failed "use my location" under the field (the phone sheet) instead of as a toast. */ inlineLocateError?: boolean; /** Fill an empty weather field from the forecast once the place and day are known (the desktop dialog). */ @@ -81,7 +79,6 @@ export function useJourneyEntryForm({ blankVerdictRow = false, withTags = false, dirtyOnCoordinates = false, - toastSaveError = false, inlineLocateError = false, autoFillWeather = false, }: JourneyEntryFormOptions) { @@ -378,7 +375,6 @@ export function useJourneyEntryForm({ } onDone(); } catch (err) { - if (!toastSaveError) throw err; // Neither the page callback nor journeyStore toasts, so without this the // whole entry just fails to save with no sign of it. toast.error(getApiErrorMessage(err, t('journey.settings.saveFailed')));