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;