diff --git a/client/src/components/Planner/usePlacesSidebar.test.tsx b/client/src/components/Planner/usePlacesSidebar.test.tsx index 3ec5b6ca2..07aaf764b 100644 --- a/client/src/components/Planner/usePlacesSidebar.test.tsx +++ b/client/src/components/Planner/usePlacesSidebar.test.tsx @@ -188,12 +188,9 @@ describe('usePlacesSidebar filtering', () => { expect(names()).toEqual(['GPX Track']); }); - it('FE-PLANNER-PSHOOK-008: a tracks filter without any track falls back to "all"', async () => { - seedStore(useTripStore, { placesFilter: 'tracks' }); - render(); - await waitFor(() => expect(useTripStore.getState().placesFilter).toBe('all')); - expect(names()).toEqual(['Plain Spot']); - }); + // FE-PLANNER-PSHOOK-008 (a tracks filter without any track falls back to "all") + // moved to useTripPlanner (FE-TP-HOOK-177): the fallback now lives there, so it + // also holds while no places list is mounted. it('FE-PLANNER-PSHOOK-009: a category filter keeps only that category', () => { const tagged = buildPlace({ name: 'Tagged', category_id: 4 }); diff --git a/client/src/components/Planner/usePlacesSidebar.ts b/client/src/components/Planner/usePlacesSidebar.ts index cc5843c07..6a6c9abf5 100644 --- a/client/src/components/Planner/usePlacesSidebar.ts +++ b/client/src/components/Planner/usePlacesSidebar.ts @@ -259,8 +259,9 @@ export function usePlacesSidebar(props: PlacesSidebarProps) { const pickFilter = (next: PlacesFilter) => { setFilter(next); setSelectedIds(new Set()) } // Alle geplanten Ort-IDs abrufen (einem Tag zugewiesen) + // Whether to offer the "Tracks" tab. useTripPlanner moves the filter back to + // "all" once the last track is gone, whichever screen is in front. const hasTracks = useMemo(() => places.some(p => p.route_geometry), [places]) - useEffect(() => { if (filter === 'tracks' && !hasTracks) setFilter('all') }, [hasTracks, filter]) const plannedIds = useMemo( () => plannedPlaceIds({ assignments, accommodations, reservations }), diff --git a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx index db8d6a21b..fb9fe9252 100644 --- a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx +++ b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx @@ -63,10 +63,9 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps) if (shell.browseFromEdit) setFilter('unplanned') }, [shell.browseFromEdit, setFilter]) + // Whether to offer the "Tracks" chip; useTripPlanner moves the filter back to + // "all" once the last track is gone. const hasTracks = useMemo(() => places.some(p => p.route_geometry), [places]) - useEffect(() => { - if (filter === 'tracks' && !hasTracks) setFilter('all') - }, [filter, hasTracks, setFilter]) // A hotel is linked through its stay and a venue through its booking; neither is // ever dragged onto a day, and the pool used to call both unplanned (#2072). diff --git a/client/src/pages/tripPlanner/useTripPlanner.test.tsx b/client/src/pages/tripPlanner/useTripPlanner.test.tsx index c4ea889cc..33cf5c255 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.test.tsx +++ b/client/src/pages/tripPlanner/useTripPlanner.test.tsx @@ -1,4 +1,4 @@ -// FE-TP-HOOK-001 to FE-TP-HOOK-176 +// FE-TP-HOOK-001 to FE-TP-HOOK-177 import React from 'react' import { renderHook, act, waitFor } from '@testing-library/react' import { TranslationProvider } from '../../i18n/TranslationContext' @@ -644,6 +644,23 @@ describe('useTripPlanner — map derivations', () => { expect(result.current.mapPlaces.map(p => p.id)).toEqual([2]) }) + it('FE-TP-HOOK-177: losing the last track moves the "Tracks" filter back to all', async () => { + // The phone map has no places list mounted, so this hook is what keeps the + // filter from pointing at a pool no control offers any more. + seedTrip({ + places: [geo(1), geo(2, { route_geometry: '[[1,2]]' })], + placesFilter: 'tracks', + }) + + const { result } = await renderPlanner() + expect(useTripStore.getState().placesFilter).toBe('tracks') + + act(() => { useTripStore.setState({ places: [geo(1)] }) }) + + await waitFor(() => expect(useTripStore.getState().placesFilter).toBe('all')) + expect(result.current.mapPlaces.map(p => p.id)).toEqual([1]) + }) + it('FE-TP-HOOK-027: the category filter honours the uncategorized bucket', async () => { seedTrip({ places: [geo(1, { category_id: 3 }), geo(2, { category_id: null })], diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts index bc65e3ac5..25b85e4c4 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.ts +++ b/client/src/pages/tripPlanner/useTripPlanner.ts @@ -768,6 +768,17 @@ export function useTripPlanner() { }) }, [places, placesCategoryFilter, placesFilter, placesRatingFilter, assignments, expandedDayIds, selectedDayId, days, tripAccommodations, reservations, compactUnplanned]) + // The "Tracks" pool is only offered while a place carries a track. When the last + // one goes (deleted here or by a collaborator), fall back to "all" — here, because + // this hook is mounted whatever is on screen: the lists that used to do it are not + // mounted while the phone map is in front, and the map would sit empty under a + // "Tracks" filter no control offers any more. + const setPlacesFilter = useTripStore((s) => s.setPlacesFilter) + const hasTracks = useMemo(() => places.some(p => p.route_geometry), [places]) + useEffect(() => { + if (placesFilter === 'tracks' && !hasTracks) setPlacesFilter('all') + }, [placesFilter, hasTracks, setPlacesFilter]) + const { route, routeWalking, routeSegments, routeVias, routeInfo, setRoute, setRouteInfo, updateRouteForDay } = useRouteCalculation({ assignments } as any, selectedDayId, routeShown, routeProfile, tripAccommodations) // Road trip mode already draws the whole trip its own way, so the overview stands // down there rather than drawing a second set of lines over it. diff --git a/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx b/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx index a2e352d6a..a2eebd252 100644 --- a/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx +++ b/client/tests/unit/mobile/trip/MPlacesBrowser.test.tsx @@ -124,11 +124,11 @@ describe('MPlacesBrowser', () => { expect(screen.queryByText('Louvre')).not.toBeInTheDocument() }) - it('FE-MOB-PBROW-007: hides the tracks chip and falls back to all when the last track is gone', () => { - seedStore(useTripStore, { placesFilter: 'tracks' }) + // The fall back to "all" once the last track is gone lives in useTripPlanner + // (FE-TP-HOOK-177), which stays mounted while this browser is not. + it('FE-MOB-PBROW-007: hides the tracks chip when no place carries a track', () => { renderBrowser(makePlanner({ places: [LOUVRE, EIFFEL] } as Partial)) expect(screen.queryByRole('button', { name: 'places.filterTracks' })).not.toBeInTheDocument() - expect(useTripStore.getState().placesFilter).toBe('all') expect(screen.getByText('places.count:2')).toBeInTheDocument() })