fix(tours): clear the stage spread's lint, read the vias after render and test against the reloaded vias

The vias the spread keeps on their legs are taken over in a layout effect
instead of during render, the route redraw takes the planner's async
updateRouteForDay, and the menu items for whole or stages move out of the
button component into their own module. Two leftovers of the old day pills
in tourParts go. The hook test's reanchor reloads the vias like the real
one does, and the detail test finds the button by its label, Add to Day.
This commit is contained in:
Maurice
2026-10-20 22:44:42 +02:00
parent cb5c5ab4a1
commit 112017a9e8
7 changed files with 97 additions and 80 deletions
@@ -57,8 +57,8 @@ import { PaddleWindSection } from './paddle/PaddleWindCard';
import { TourProvenanceBlock } from './provenance/TourProvenanceSection';
import { RunHeadPills } from './run/RunHeadPills';
import { SkiHeadPill } from './ski/skiMarks';
import { stageKeysOfTour, type TourDayHow } from './stages/stageSpread';
import { TourDayAddButton } from './stages/TourDayAddButton';
import { stageKeysOfTour, type TourDayHow } from './stages/stageSpread';
import { tourDayRowIcon } from './tourDayRowIcon';
import { tourDaysOf } from './tourDays';
import { heightsUnknown, movingTimeLabel, withoutHeights } from './tourFacts';
@@ -1,54 +1,9 @@
import { CalendarDays, CalendarRange, ChevronDown, Plus } from 'lucide-react';
import { ChevronDown, Plus } from 'lucide-react';
import type { MouseEvent } from 'react';
import { useTranslation } from '../../../i18n';
import type { Day } from '../../../types';
import { ContextMenu, useContextMenu } from '../../shared/ContextMenu';
import { DialogButton } from '../../shared/DialogShell';
import { dayMenuLabel } from '../tourDays';
import type { TourDayAssignment } from '../useTourDayAssignment';
import { stagesThatFit, tourDayChoices } from './stageSpread';
type Translate = ReturnType<typeof useTranslation>['t'];
type MenuItems = Parameters<ReturnType<typeof useContextMenu>['open']>[1];
interface TourDayHowInput {
placeId: number;
dayId: number;
days: Day[];
/** The stages of the multi-day tour. */
stageCount: number;
assignment: TourDayAssignment;
}
/**
* MDT-02: the two ways a multi-day tour goes on a day, as menu items: whole,
* or a stage a day from it on, under the day they act on, so the step after
* the day menu still says which day was picked. Whole is greyed out on a day
* that holds the tour already, and says so; the stages say how many fit when
* the trip ends before them.
*/
export function tourDayHowItems(input: TourDayHowInput, t: Translate, locale: string): MenuItems {
const { placeId, dayId, days, stageCount, assignment } = input;
const facts = {
count: stageCount,
fit: stagesThatFit(days, dayId, stageCount),
wholeFree: !assignment.isOnDay(placeId, dayId),
};
const ways = tourDayChoices(facts, t).map((choice) => {
const disabled = choice.disabled || !assignment.isAvailable(placeId, dayId, choice.how);
return {
label: choice.title,
// Greyed out with no reason reads as broken; the reason goes right under it.
hint: choice.disabled && choice.how === 'whole' ? choice.hint : undefined,
icon: choice.how === 'whole' ? CalendarDays : CalendarRange,
disabled,
onClick: () => void assignment.assign(placeId, dayId, choice.how),
};
});
const at = days.findIndex((day) => day.id === dayId);
if (at < 0) return ways;
return [{ label: dayMenuLabel(days[at], at, t, locale), heading: true }, ...ways];
}
import { tourDayHowItems, type TourDayHowInput } from './tourDayHowItems';
/**
* The detail's "Add to day" for the day selected in the plan. A day tour goes
@@ -68,13 +68,13 @@ describe('the detail button and the dialog', () => {
it('FE-TOUR-STAGE-SPREAD-019: the detail adds a day tour at once and offers a multi-day tour both ways', async () => {
const props = { placeId: 81, dayId: 4, days: DAYS, assignment };
const { unmount } = render(<TourDayAddButton {...props} stageCount={0} />);
await userEvent.click(screen.getByRole('button', { name: 'Add to day' }));
await userEvent.click(screen.getByRole('button', { name: 'Add to Day' }));
expect(assignment.assign).toHaveBeenCalledWith(81, 4);
unmount();
vi.mocked(assignment.isOnDay).mockReturnValue(true);
render(<TourDayAddButton {...props} stageCount={3} />);
await userEvent.click(screen.getByRole('button', { name: 'Add to day' }));
await userEvent.click(screen.getByRole('button', { name: 'Add to Day' }));
const whole = screen.getByRole('button', { name: /^Whole tour on this day/ });
expect(whole).toBeDisabled();
expect(whole).toHaveTextContent('This day already holds the tour.');
@@ -0,0 +1,49 @@
import { CalendarDays, CalendarRange } from 'lucide-react';
import type { useTranslation } from '../../../i18n';
import type { Day } from '../../../types';
import type { useContextMenu } from '../../shared/ContextMenu';
import { dayMenuLabel } from '../tourDays';
import type { TourDayAssignment } from '../useTourDayAssignment';
import { stagesThatFit, tourDayChoices } from './stageSpread';
type Translate = ReturnType<typeof useTranslation>['t'];
type MenuItems = Parameters<ReturnType<typeof useContextMenu>['open']>[1];
export interface TourDayHowInput {
placeId: number;
dayId: number;
days: Day[];
/** The stages of the multi-day tour. */
stageCount: number;
assignment: TourDayAssignment;
}
/**
* MDT-02: the two ways a multi-day tour goes on a day, as menu items: whole,
* or a stage a day from it on, under the day they act on, so the step after
* the day menu still says which day was picked. Whole is greyed out on a day
* that holds the tour already, and says so; the stages say how many fit when
* the trip ends before them.
*/
export function tourDayHowItems(input: TourDayHowInput, t: Translate, locale: string): MenuItems {
const { placeId, dayId, days, stageCount, assignment } = input;
const facts = {
count: stageCount,
fit: stagesThatFit(days, dayId, stageCount),
wholeFree: !assignment.isOnDay(placeId, dayId),
};
const ways = tourDayChoices(facts, t).map((choice) => {
const disabled = choice.disabled || !assignment.isAvailable(placeId, dayId, choice.how);
return {
label: choice.title,
// Greyed out with no reason reads as broken; the reason goes right under it.
hint: choice.disabled && choice.how === 'whole' ? choice.hint : undefined,
icon: choice.how === 'whole' ? CalendarDays : CalendarRange,
disabled,
onClick: () => void assignment.assign(placeId, dayId, choice.how),
};
});
const at = days.findIndex((day) => day.id === dayId);
if (at < 0) return ways;
return [{ label: dayMenuLabel(days[at], at, t, locale), heading: true }, ...ways];
}
+2 -5
View File
@@ -9,7 +9,7 @@ import { ContextMenu, useContextMenu } from '../shared/ContextMenu'
import { DialogButton, fs } from '../shared/DialogShell'
import { Tooltip } from '../shared/Tooltip'
import { BOX, Eyebrow } from '../Planner/bookings/bookingParts'
import { SoftPill, WhiteBadge } from '../Planner/planParts'
import { WhiteBadge } from '../Planner/planParts'
import { hikeSourceBadgeLabel, tourSource } from './tourPresentation'
import { type TourDay, dayMenuLabel } from './tourDays'
import { formatDate } from '../../utils/formatters'
@@ -18,7 +18,7 @@ import type { TourDayAssignment } from './useTourDayAssignment'
import { useRailFold } from './railFold'
import { tourDayRowIcon } from './tourDayRowIcon'
import { SkiCornerMark } from './ski/skiMarks'
import { tourDayHowItems } from './stages/TourDayAddButton'
import { tourDayHowItems } from './stages/tourDayHowItems'
/**
* A tour's tile in a list: its T-scale grade in place of the mountain, named in
@@ -87,9 +87,6 @@ export function TourListCard({ icon,
)
}
/** How many days a list row names before the rest fold into one "+n" pill. */
const SHOWN_TOUR_DAYS = 3
/** The days a tour is on as one white badge of a plan row: "Days 2, 4", each day named in full in the tooltip. */
export function TourDaysBadge({ tourDays }: { tourDays: readonly TourDay[] }) {
const { t, locale } = useTranslation()