From 6d2490a3ba0bb0d12bb94cb5f683e1c717ba776d Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 12:07:25 +0200 Subject: [PATCH] refactor(tours): share the planned tours filter between desktop and phone The All, Unplanned and Planned filter of the tours list was copied into the phone selection list. It now lives in filterTours next to the other tour presentation helpers, and both lists call it. --- client/src/components/Tours/ToursSidebar.tsx | 9 +++------ .../components/Tours/tourPresentation.test.ts | 17 ++++++++++++++++- client/src/components/Tours/tourPresentation.ts | 12 ++++++++++++ .../screens/trip/places/MPlacesBrowser.tsx | 3 ++- .../screens/trip/places/MToursSelectionList.tsx | 10 +++------- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/client/src/components/Tours/ToursSidebar.tsx b/client/src/components/Tours/ToursSidebar.tsx index b8d21dee5..aba251dde 100644 --- a/client/src/components/Tours/ToursSidebar.tsx +++ b/client/src/components/Tours/ToursSidebar.tsx @@ -12,6 +12,7 @@ import { NEUTRAL_TINT, fs } from '../shared/DialogShell' import { Tooltip } from '../shared/Tooltip' import EmptyState from '../shared/EmptyState' import { TourDayMenu } from './tourParts' +import { filterTours, type TourFilter } from './tourPresentation' const MAX_FILE_BYTES = 10 * 1024 * 1024 @@ -44,7 +45,7 @@ export default function ToursSidebar({ tripId, days, tours, loading = false, sel const { canEdit, canAssign } = useTourPermissions({ tripId, canEdit: editPermission, canAssign: assignPermission }) const toast = useToast() const loadTrip = useTripStore(s => s.loadTrip) - const [filter, setFilter] = useState<'all' | 'unplanned' | 'planned'>('all') + const [filter, setFilter] = useState('all') const [importing, setImporting] = useState(false) const fileInputRef = useRef(null) const filterRef = useRef(null) @@ -95,11 +96,7 @@ export default function ToursSidebar({ tripId, days, tours, loading = false, sel unplanned: tours.filter(tr => !tr.planned).length, planned: tours.filter(tr => tr.planned).length, } - const filtered = tours.filter(tr => { - if (filter === 'unplanned') return !tr.planned - if (filter === 'planned') return tr.planned - return true - }) + const filtered = filterTours(tours, filter) useEffect(() => { if (!filterOpen) return diff --git a/client/src/components/Tours/tourPresentation.test.ts b/client/src/components/Tours/tourPresentation.test.ts index 31f7e209c..1efac2686 100644 --- a/client/src/components/Tours/tourPresentation.test.ts +++ b/client/src/components/Tours/tourPresentation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { hikeSourceBadgeLabel, tourSource } from './tourPresentation' +import { filterTours, hikeSourceBadgeLabel, tourSource } from './tourPresentation' describe('tourPresentation', () => { it('uses the source abstraction for TREK, GPX import, and wanderer badges', () => { @@ -12,4 +12,19 @@ describe('tourPresentation', () => { expect(tourSource({ wanderer_ref: 'wanderer:123' })).toBe('wanderer') expect(hikeSourceBadgeLabel({ wanderer_ref: 'wanderer:123' }, t)).toBe('tours.badge.wanderer') }) + + it('filters tours by All, Unplanned and Planned, keeping their order', () => { + const tours = [ + { id: 1, planned: true }, + { id: 2, planned: false }, + { id: 3, planned: true }, + { id: 4, planned: false }, + ] + + expect(filterTours(tours, 'all')).toEqual(tours) + expect(filterTours(tours, 'all')).not.toBe(tours) + expect(filterTours(tours, 'unplanned').map(tour => tour.id)).toEqual([2, 4]) + expect(filterTours(tours, 'planned').map(tour => tour.id)).toEqual([1, 3]) + expect(filterTours([], 'planned')).toEqual([]) + }) }) diff --git a/client/src/components/Tours/tourPresentation.ts b/client/src/components/Tours/tourPresentation.ts index c26294ff8..5ff5e0f52 100644 --- a/client/src/components/Tours/tourPresentation.ts +++ b/client/src/components/Tours/tourPresentation.ts @@ -2,6 +2,9 @@ import type { TourListItem } from '@trek/shared' export type TourSource = 'trek' | 'gpx' | 'wanderer' +/** All / Unplanned / Planned, the same three states as the Places pool. */ +export type TourFilter = 'all' | 'unplanned' | 'planned' + type Translate = (key: string, params?: Record) => string /** @@ -20,3 +23,12 @@ export function hikeSourceBadgeLabel( ): string { return t(`tours.badge.${tourSource(tour)}`) } + +/** The tours a list shows under the All / Unplanned / Planned filter, in their order. */ +export function filterTours>(tours: T[], filter: TourFilter): T[] { + return tours.filter(tr => { + if (filter === 'unplanned') return !tr.planned + if (filter === 'planned') return tr.planned + return true + }) +} diff --git a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx index 7fdae169b..8097f0f20 100644 --- a/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx +++ b/client/src/mobile/screens/trip/places/MPlacesBrowser.tsx @@ -20,6 +20,7 @@ import MPlacesBulkCategorySheet from './MPlacesBulkCategorySheet' import MPlacesSaveToCollectionSheet from './MPlacesSaveToCollectionSheet' import MPlacesToursModeSwitch from './MPlacesToursModeSwitch' import MToursSelectionList from './MToursSelectionList' +import type { TourFilter } from '../../../../components/Tours/tourPresentation' import { filterPool, firstPlannedDayNumbers, plannedPlaceIds } from './placesBrowserModel' import { MCategoryFilterList, MRatingFloorChips, SquareCheck } from './MPlacesFilterControls' import { countActivePlacesFilters } from '../../../../utils/placesFilter' @@ -46,7 +47,7 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps) // Top level of the places browser, mirroring the desktop right add-panel's // Places <-> Tours switch placement. const [toursMode, setToursMode] = useState(false) - const [toursFilter, setToursFilter] = useState<'all' | 'unplanned' | 'planned'>('all') + const [toursFilter, setToursFilter] = useState('all') const poolPlaces = useMemo( // Places that are tours stay out of the Places pool while the addon is on. diff --git a/client/src/mobile/screens/trip/places/MToursSelectionList.tsx b/client/src/mobile/screens/trip/places/MToursSelectionList.tsx index c711b7418..661b2abbd 100644 --- a/client/src/mobile/screens/trip/places/MToursSelectionList.tsx +++ b/client/src/mobile/screens/trip/places/MToursSelectionList.tsx @@ -2,7 +2,7 @@ import { ArrowDown, ArrowUp, Mountain, AlertTriangle, CalendarDays } from 'lucid import type { TourListItem } from '@trek/shared' import { useSettingsStore } from '../../../../store/settingsStore' import { formatDistance, formatElevation } from '../../../../utils/units' -import { hikeSourceBadgeLabel } from '../../../../components/Tours/tourPresentation' +import { filterTours, hikeSourceBadgeLabel, type TourFilter } from '../../../../components/Tours/tourPresentation' import type { TripPlanner, MTripShellApi } from '../MTripShell' import { useTourPermissions, type TourPermissionProps } from '../../../../components/Tours/useTourPermissions' @@ -10,7 +10,7 @@ interface MToursSelectionListProps extends TourPermissionProps { planner: TripPlanner shell: MTripShellApi /** All / Unplanned / Planned — same three states as the Places pool. */ - filter: 'all' | 'unplanned' | 'planned' + filter: TourFilter } /** @@ -28,11 +28,7 @@ export default function MToursSelectionList({ planner, shell, filter, canAssign: const distanceUnit = useSettingsStore(s => s.settings.distance_unit) const tours: TourListItem[] = planner.tours - const filtered = tours.filter(tr => { - if (filter === 'unplanned') return !tr.planned - if (filter === 'planned') return tr.planned - return true - }) + const filtered = filterTours(tours, filter) if (filtered.length === 0) { return (