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.
This commit is contained in:
Maurice
2026-10-09 12:07:25 +02:00
parent f84924c56a
commit 6d2490a3ba
5 changed files with 36 additions and 15 deletions
+3 -6
View File
@@ -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<TourFilter>('all')
const [importing, setImporting] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const filterRef = useRef<HTMLDivElement>(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
@@ -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([])
})
})
@@ -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, unknown>) => 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<T extends Pick<TourListItem, 'planned'>>(tours: T[], filter: TourFilter): T[] {
return tours.filter(tr => {
if (filter === 'unplanned') return !tr.planned
if (filter === 'planned') return tr.planned
return true
})
}
@@ -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<TourFilter>('all')
const poolPlaces = useMemo(
// Places that are tours stay out of the Places pool while the addon is on.
@@ -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 (