diff --git a/client/scripts/eslint-baseline.json b/client/scripts/eslint-baseline.json index 492f6dc91..e02a8db56 100644 --- a/client/scripts/eslint-baseline.json +++ b/client/scripts/eslint-baseline.json @@ -1,6 +1,6 @@ { "src": { - "@typescript-eslint/no-non-null-assertion": 384, + "@typescript-eslint/no-non-null-assertion": 382, "@typescript-eslint/no-misused-promises": 849, "no-console": 88, "complexity": 221, diff --git a/client/src/components/Planner/DayPlanSidebar.tours.test.tsx b/client/src/components/Planner/DayPlanSidebar.tours.test.tsx index 2618560d1..042028f66 100644 --- a/client/src/components/Planner/DayPlanSidebar.tours.test.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tours.test.tsx @@ -201,9 +201,9 @@ describe('DayPlanSidebar', () => { const view = render(); const tourPreview = view.container.querySelector('[data-testid="trip-plan-tour-description"]') as HTMLElement; expect(tourPreview).toHaveTextContent(description); - // One line like the row's name, so hover controls make the description give way instead of wrapping. + // The description keeps to one line; the pills beside it wrap rather than run out of the row. expect(tourPreview).toHaveStyle({ whiteSpace: 'nowrap', maxHeight: '1.3em' }); - expect(tourPreview.parentElement).toHaveClass('flex-nowrap', 'overflow-hidden'); + expect(tourPreview.parentElement).toHaveClass('flex-wrap', 'overflow-hidden'); expect(screen.getByTestId('trip-plan-tour-walking-time')).toHaveTextContent('Walk: 1 h'); expect(screen.getByTestId('trip-plan-tour-breaks')).toHaveTextContent('Breaks: 35 min'); expect(screen.getByTestId('trip-plan-tour-planned-duration')).toHaveTextContent('Planned: 1 h 35 min'); diff --git a/client/src/pages/tripPlanner/useTourOverview.test.ts b/client/src/pages/tripPlanner/useTourOverview.test.ts index f9eaa9896..9cd2b0b5c 100644 --- a/client/src/pages/tripPlanner/useTourOverview.test.ts +++ b/client/src/pages/tripPlanner/useTourOverview.test.ts @@ -6,6 +6,13 @@ import { resetAllStores } from '../../../tests/helpers/store'; import type { Place } from '../../types'; import { useTourOverview } from './useTourOverview'; +const OPEN = { + activeTab: 'tour-planner', + tourPlanner: { mode: { type: 'neutral' } }, + wandererBrowser: { open: false }, +} as const; +const EDITING = { ...OPEN, tourPlanner: { mode: { type: 'new-draft' } } } as const; + const PLACES = [ { id: 1, lat: 46.6, lng: 12.1, route_geometry: '[[46.5,12.0],[46.7,12.2],[46.6,12.3]]' }, { id: 2, lat: 45.4, lng: 12.3, route_geometry: '[[45.43,12.32],[45.44,12.34]]' }, @@ -18,12 +25,12 @@ describe('useTourOverview', () => { beforeEach(() => resetAllStores()); it('FE-TP-TOUROVERVIEW-001: shows the tours with a line, not the other places', () => { - const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, true)); + const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, OPEN)); expect(result.current.tourPlaces.map((place) => place.id)).toEqual([1, 2]); }); it('FE-TP-TOUROVERVIEW-002: frames each line by its corners', () => { - const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, true)); + const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, OPEN)); expect(result.current.framePoints).toEqual([ [46.5, 12.0], [46.7, 12.3], @@ -34,7 +41,7 @@ describe('useTourOverview', () => { }); it('FE-TP-TOUROVERVIEW-003: shows nothing while a tour is open', () => { - const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, false)); + const { result } = renderHook(() => useTourOverview(5, PLACES, TOURS, EDITING)); expect(result.current).toEqual({ tourPlaces: [], framePoints: [], focusSuffix: '' }); }); }); diff --git a/client/src/pages/tripPlanner/useTourOverview.ts b/client/src/pages/tripPlanner/useTourOverview.ts index 95048355f..0960d3bd1 100644 --- a/client/src/pages/tripPlanner/useTourOverview.ts +++ b/client/src/pages/tripPlanner/useTourOverview.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import type { TourPlannerController } from '../../components/Tours/planner/useTourPlanner'; import { useTourMapVisibility } from '../../components/Tours/useTourMapVisibility'; import type { Place } from '../../types'; import { parseRenderableRouteGeometry } from '../../utils/routeGeometry'; @@ -7,15 +8,21 @@ import { parseRenderableRouteGeometry } from '../../utils/routeGeometry'; * The trip's tours on the Tours map while none is open, as the plan map shows them: * each tour's place, whose line and marker the map draws in the tour's colour, and the * corners of those lines for the camera to frame. A tour hidden from the map in the - * list stays off it here too. Empty while `showing` is off (a tour open, an import - * viewed, the wanderer browser up), so the map then shows that alone. + * list stays off it here too. Empty off the Tours tab and while a tour is open, an + * import viewed or the wanderer browser up, so the map then shows that alone. */ export function useTourOverview( tripId: number, places: readonly Place[], tourPlaceIds: ReadonlySet, - showing: boolean + view: { + activeTab: string; + tourPlanner: Pick; + wandererBrowser: { open: boolean }; + } ) { + const showing = + view.activeTab === 'tour-planner' && view.tourPlanner.mode.type === 'neutral' && !view.wandererBrowser.open; const { hiddenIds } = useTourMapVisibility(tripId, tourPlaceIds); const tourPlaces = useMemo( () => @@ -29,7 +36,7 @@ export function useTourOverview( tourPlaces, framePoints, // Part of the camera's key, so the map frames the tours again once they are loaded or one is shown or hidden. - focusSuffix: showing ? `:tours:${tourPlaces.map((place) => place.id).join(',')}` : '', + focusSuffix: tourPlaces.length ? `:tours:${tourPlaces.map((place) => place.id).join(',')}` : '', }; } diff --git a/client/src/pages/tripPlanner/useTripPlannerPage.ts b/client/src/pages/tripPlanner/useTripPlannerPage.ts index 099ac2910..d6c6b6156 100644 --- a/client/src/pages/tripPlanner/useTripPlannerPage.ts +++ b/client/src/pages/tripPlanner/useTripPlannerPage.ts @@ -42,11 +42,7 @@ export function useTripPlannerPage() { useAssistantEntityLinks({ planner, workspace }) const tourDetailOpenerRef = useRef<{ placeId: number; element: HTMLElement } | null>(null) - // With no tour open, the Tours map shows the trip's tours and frames them. - const tourOverview = useTourOverview( - tripId, places, planner.tourPlaceIds, - activeTab === 'tour-planner' && tourPlanner.mode.type === 'neutral' && !wandererBrowser.open, - ) + const tourOverview = useTourOverview(tripId, places, planner.tourPlaceIds, { activeTab, tourPlanner, wandererBrowser }) const { focusPoints, lastPlanMapViewportRef } = usePlannerFocusPoints(activeTab, places, tourPlanner, wandererBrowser, tourOverview.framePoints) // PAD-06: an obstacle or area the paddle checklist asks for moves the camera as a picked search result does. diff --git a/client/src/utils/routeGeometry.test.ts b/client/src/utils/routeGeometry.test.ts index e7ab19c26..fb1961ae7 100644 --- a/client/src/utils/routeGeometry.test.ts +++ b/client/src/utils/routeGeometry.test.ts @@ -31,9 +31,9 @@ describe('analyzeRouteGeometry', () => { const analysis = analyzeRouteGeometry(JSON.stringify([ [45.44, 12.32, 2], [45.441, 12.32, 9], [45.442, 12.32, 1], [45.443, 12.32, 4], [45.444, 12.32, 3], ])) - expect(analysis!.maxEle).toBe(9) - expect(analysis!.gain).toBe(7) - expect(analysis!.loss).toBe(8) + expect(analysis?.maxEle).toBe(9) + expect(analysis?.gain).toBe(7) + expect(analysis?.loss).toBe(8) }) it('maps route endpoints and interpolates focus between prepared samples', () => { diff --git a/server/tests/unit/nest/budget.foreign-custom-split.test.ts b/server/tests/unit/nest/budget.foreign-custom-split.test.ts index 41f7ac872..3ed87aebb 100644 --- a/server/tests/unit/nest/budget.foreign-custom-split.test.ts +++ b/server/tests/unit/nest/budget.foreign-custom-split.test.ts @@ -32,10 +32,16 @@ afterAll(async () => { }); async function seedQuarters() { - const users = []; - for (let i = 0; i < 4; i++) users.push((await makeUser(orm())).user); + const users = [ + (await makeUser(orm())).user, + (await makeUser(orm())).user, + (await makeUser(orm())).user, + (await makeUser(orm())).user, + ]; const trip = await makeTrip(orm(), users[0].id); - for (const u of users.slice(1)) await addTripMember(orm(), trip.id, u.id); + await addTripMember(orm(), trip.id, users[1].id); + await addTripMember(orm(), trip.id, users[2].id); + await addTripMember(orm(), trip.id, users[3].id); await budget.createBudgetItem(trip.id, { name: 'Sky Lagoon', currency: 'ISK', @@ -54,7 +60,7 @@ describe('a custom split in a foreign currency', () => { const cents = result.balances.map((b) => Math.round(b.balance * 100)); expect(cents.reduce((a, c) => a + c, 0)).toBe(0); // The payer has the lowest id, so the tie hands them one of the three rounded-up quarters. - expect(result.balances.find((b) => b.user_id === users[0].id)!.balance).toBe(748.13); + expect(result.balances.find((b) => b.user_id === users[0].id)?.balance).toBe(748.13); const owed = result.flows .filter((f) => f.to.user_id === users[0].id) .reduce((a, f) => a + Math.round(f.amount * 100), 0); diff --git a/shared/scripts/i18n-selfhost-wording.mjs b/shared/scripts/i18n-selfhost-wording.mjs index 91342eda1..1d882ae03 100644 --- a/shared/scripts/i18n-selfhost-wording.mjs +++ b/shared/scripts/i18n-selfhost-wording.mjs @@ -35,12 +35,6 @@ export const ALLOWED = [ 'settings.about.descriptionManaged instead, so neither reader is told the ' + 'wrong thing and self-hosters lose nothing.', }, - { - key: 'system_notice.release_notes.feature_dawarich_body', - because: - 'it calls Dawarich self-hosted, not TREK, which is what Dawarich is, and the ' + - 'release notice carrying it is shown only where the managed condition is false', - }, { key: 'system_notice.v3_thankyou.body', because: diff --git a/shared/src/tours/tours.metrics.ts b/shared/src/tours/tours.metrics.ts index 89fbbdb4b..c14e130d3 100644 --- a/shared/src/tours/tours.metrics.ts +++ b/shared/src/tours/tours.metrics.ts @@ -32,7 +32,8 @@ const ELEVATION_NOISE_M = 5; export function elevationGainLoss(heights: readonly number[]): { gain: number; loss: number } { let gain = 0; let loss = 0; - let ref = heights[0]; + let [ref] = heights; + if (ref === undefined) return { gain, loss }; for (const height of heights) { const delta = height - ref; if (delta >= ELEVATION_NOISE_M) gain += delta;