From 75d23eb6aaa567d301701ea85307892d9fe8a0ef Mon Sep 17 00:00:00 2001 From: jubnl Date: Thu, 16 Apr 2026 15:27:13 +0200 Subject: [PATCH] fix(journey): keep page mounted during in-place journey refetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadJourney previously set loading=true unconditionally, causing the JourneyDetailPage guard (if loading || !current) to unmount the entire page tree on every background refetch — entry saves, settings saves, trip link/unlink, contributor invite, delete, and WS realtime events all triggered the full-page spinner flash. Now loading is only toggled on cold loads (current?.id !== id). Warm refreshes replace current silently so the hero, sidebar, map, and timeline stay mounted throughout. Closes #673. --- client/src/store/journeyStore.test.ts | 41 +++++++++++++++++++++++++++ client/src/store/journeyStore.ts | 5 ++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/client/src/store/journeyStore.test.ts b/client/src/store/journeyStore.test.ts index 7b1f6760..564969ec 100644 --- a/client/src/store/journeyStore.test.ts +++ b/client/src/store/journeyStore.test.ts @@ -314,6 +314,47 @@ describe('journeyStore', () => { expect(storedEntry?.photos[0].id).toBe(201); }); + // ── loadJourney silent refresh ─────────────────────────────────────────── + + it('FE-STORE-JOURNEY-016: loadJourney does not set loading when refreshing same journey', async () => { + const existing = buildJourneyDetail({ id: 5, title: 'Old' }); + useJourneyStore.setState({ current: existing, loading: false }); + + const loadingValues: boolean[] = []; + const unsub = useJourneyStore.subscribe(s => loadingValues.push(s.loading)); + + const refreshed = buildJourneyDetail({ id: 5, title: 'Refreshed' }); + server.use( + http.get('/api/journeys/5', () => HttpResponse.json(refreshed)) + ); + + await useJourneyStore.getState().loadJourney(5); + unsub(); + + expect(loadingValues.every(v => v === false)).toBe(true); + expect(useJourneyStore.getState().current?.title).toBe('Refreshed'); + }); + + it('FE-STORE-JOURNEY-017: loadJourney sets loading on cold load (different journey)', async () => { + const existing = buildJourneyDetail({ id: 5 }); + useJourneyStore.setState({ current: existing, loading: false }); + + const loadingValues: boolean[] = []; + const unsub = useJourneyStore.subscribe(s => loadingValues.push(s.loading)); + + const other = buildJourneyDetail({ id: 99 }); + server.use( + http.get('/api/journeys/99', () => HttpResponse.json(other)) + ); + + await useJourneyStore.getState().loadJourney(99); + unsub(); + + expect(loadingValues).toContain(true); + expect(useJourneyStore.getState().current?.id).toBe(99); + expect(useJourneyStore.getState().loading).toBe(false); + }); + // ── clear ──────────────────────────────────────────────────────────────── it('FE-STORE-JOURNEY-015: clear resets state', () => { diff --git a/client/src/store/journeyStore.ts b/client/src/store/journeyStore.ts index e0a8f22b..c0464133 100644 --- a/client/src/store/journeyStore.ts +++ b/client/src/store/journeyStore.ts @@ -124,7 +124,8 @@ export const useJourneyStore = create((set, get) => ({ }, loadJourney: async (id) => { - set({ loading: true, notFound: false }) + const cold = get().current?.id !== id + if (cold) set({ loading: true, notFound: false }) try { const data = await journeyApi.get(id) set({ current: data }) @@ -134,7 +135,7 @@ export const useJourneyStore = create((set, get) => ({ } throw err } finally { - set({ loading: false }) + if (cold) set({ loading: false }) } },