Resolve popular-request airport codes to city codes before prefill
Popular-requests API returns mixed airport (SVO) and city (MOW) IATA codes. Clicking a "Шереметьево → Санкт-Петербург" entry used to paste SVO into the departure field, leaving a specific airport pinned even though the visible label already resolves to the owning city name. Both start pages now route request.departure/arrival through getCityCodeByAirportCode(dictionaries, code), so the filter form seeds with MOW instead of SVO (and falls back to the raw code when dictionaries aren't loaded yet). buildOnlineBoardPrefillState takes an optional dictionaries arg for the same reason. ScheduleStartPage.test mocks @/shared/dictionaries/index.js to preserve the existing assertions (which expect unresolved codes).
This commit is contained in:
@@ -26,8 +26,25 @@ import {
|
||||
readAndClearTransientPrefill,
|
||||
writeTransientPrefill,
|
||||
} from "@/shared/state/transientPrefill.js";
|
||||
import {
|
||||
useDictionaries,
|
||||
getCityCodeByAirportCode,
|
||||
} from "@/shared/dictionaries/index.js";
|
||||
import type { IDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import "./OnlineBoardStartPage.scss";
|
||||
|
||||
/**
|
||||
* Normalize a popular-request IATA code to its owning city code so that
|
||||
* clicking "Шереметьево - Санкт-Петербург" (SVO → LED) lands on the same
|
||||
* Moscow → St Petersburg prefill as any MOW-coded entry. Falls back to
|
||||
* the original code when the airport has no parent city or when the
|
||||
* dictionaries aren't loaded yet.
|
||||
*/
|
||||
function toCityCode(code: string, dictionaries: IDictionaries | null): string {
|
||||
if (!dictionaries) return code;
|
||||
return getCityCodeByAirportCode(dictionaries, code) ?? code;
|
||||
}
|
||||
|
||||
export const ONLINE_BOARD_PREFILL_SLOT = "online-board";
|
||||
export const SCHEDULE_PREFILL_SLOT = "schedule";
|
||||
|
||||
@@ -49,6 +66,7 @@ export interface OnlineBoardPrefillState {
|
||||
|
||||
export function buildOnlineBoardPrefillState(
|
||||
request: PopularRequest,
|
||||
dictionaries: IDictionaries | null = null,
|
||||
): OnlineBoardPrefillState {
|
||||
switch (request.mode) {
|
||||
case "FlightNumber":
|
||||
@@ -57,15 +75,15 @@ export function buildOnlineBoardPrefillState(
|
||||
flightNumber: `${request.carrier}${request.flightNumber}`,
|
||||
};
|
||||
case "Departure":
|
||||
return { tab: "route", departure: request.departure };
|
||||
return { tab: "route", departure: toCityCode(request.departure, dictionaries) };
|
||||
case "Arrival":
|
||||
return { tab: "route", arrival: request.arrival };
|
||||
return { tab: "route", arrival: toCityCode(request.arrival, dictionaries) };
|
||||
case "Route":
|
||||
case "RouteWithBack":
|
||||
return {
|
||||
tab: "route",
|
||||
departure: request.departure,
|
||||
arrival: request.arrival,
|
||||
departure: toCityCode(request.departure, dictionaries),
|
||||
arrival: toCityCode(request.arrival, dictionaries),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -73,7 +91,8 @@ export function buildOnlineBoardPrefillState(
|
||||
export const OnlineBoardStartPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { locale } = useLocale();
|
||||
const { locale, language } = useLocale();
|
||||
const { dictionaries } = useDictionaries(language);
|
||||
|
||||
// Read-and-clear any prefill the previous page wrote. Stored in
|
||||
// useState (with a one-shot initializer) so React strict mode's
|
||||
@@ -97,13 +116,17 @@ export const OnlineBoardStartPage: FC = () => {
|
||||
|
||||
const handlePopularRequestClick = useCallback(
|
||||
(request: PopularRequest) => {
|
||||
// Schedule-type requests navigate to the schedule feature
|
||||
// Schedule-type requests navigate to the schedule feature. City
|
||||
// codes come off the popular-request API as airport codes in some
|
||||
// cases (SVO/LED instead of MOW/LED); resolve them to owning city
|
||||
// so the destination form pre-fills with a city rather than an
|
||||
// airport pin.
|
||||
if (request.type === "Schedule") {
|
||||
const state =
|
||||
request.mode === "Route" || request.mode === "RouteWithBack"
|
||||
? {
|
||||
departure: request.departure,
|
||||
arrival: request.arrival,
|
||||
departure: toCityCode(request.departure, dictionaries),
|
||||
arrival: toCityCode(request.arrival, dictionaries),
|
||||
withReturn: request.mode === "RouteWithBack",
|
||||
}
|
||||
: {};
|
||||
@@ -115,10 +138,10 @@ export const OnlineBoardStartPage: FC = () => {
|
||||
// Onlineboard request — same page. Update local prefill +
|
||||
// remount the filter via key bump so its useState initializers
|
||||
// see the new initial* props.
|
||||
setPrefill(buildOnlineBoardPrefillState(request));
|
||||
setPrefill(buildOnlineBoardPrefillState(request, dictionaries));
|
||||
setFilterKey((n) => n + 1);
|
||||
},
|
||||
[navigate, locale],
|
||||
[navigate, locale, dictionaries],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -67,6 +67,14 @@ vi.mock("@/ui/layout/SearchHistory.js", () => ({
|
||||
SearchHistory: () => <div data-testid="search-history" />,
|
||||
}));
|
||||
|
||||
// Dictionaries aren't needed for the popular-request behavior the test
|
||||
// exercises — return nulls so toCityCode() falls back to the raw code
|
||||
// (test assertions expect the unresolved SVO/LED codes).
|
||||
vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||
useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
|
||||
getCityCodeByAirportCode: () => undefined,
|
||||
}));
|
||||
|
||||
describe("ScheduleStartPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -29,9 +29,19 @@ import {
|
||||
readAndClearTransientPrefill,
|
||||
writeTransientPrefill,
|
||||
} from "@/shared/state/transientPrefill.js";
|
||||
import {
|
||||
useDictionaries,
|
||||
getCityCodeByAirportCode,
|
||||
} from "@/shared/dictionaries/index.js";
|
||||
import type { IDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import { buildScheduleUrl } from "../url.js";
|
||||
import "./ScheduleStartPage.scss";
|
||||
|
||||
function toCityCode(code: string, dictionaries: IDictionaries | null): string {
|
||||
if (!dictionaries) return code;
|
||||
return getCityCodeByAirportCode(dictionaries, code) ?? code;
|
||||
}
|
||||
|
||||
function minutesToTime(minutes: number): string {
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = minutes % 60;
|
||||
@@ -67,7 +77,8 @@ export interface SchedulePrefillState {
|
||||
export const ScheduleStartPage: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { locale } = useLocale();
|
||||
const { locale, language } = useLocale();
|
||||
const { dictionaries } = useDictionaries(language);
|
||||
|
||||
// One-shot read of any prefill the previous page wrote.
|
||||
const [prefill] = useState<SchedulePrefillState>(
|
||||
@@ -166,10 +177,13 @@ export const ScheduleStartPage: FC = () => {
|
||||
|
||||
const handlePopularRequestClick = useCallback(
|
||||
(request: PopularRequest) => {
|
||||
// Popular-request entries sometimes carry airport codes (SVO, LED)
|
||||
// instead of city codes (MOW, LED). Normalize to the owning city
|
||||
// so clicks land on the city filter, not on a specific airport.
|
||||
if (request.type === "Onlineboard") {
|
||||
writeTransientPrefill(
|
||||
ONLINE_BOARD_PREFILL_SLOT,
|
||||
buildOnlineBoardPrefillState(request),
|
||||
buildOnlineBoardPrefillState(request, dictionaries),
|
||||
);
|
||||
navigate(`/${locale}/onlineboard`);
|
||||
return;
|
||||
@@ -179,15 +193,15 @@ export const ScheduleStartPage: FC = () => {
|
||||
const state: SchedulePrefillState =
|
||||
request.mode === "Route" || request.mode === "RouteWithBack"
|
||||
? {
|
||||
departure: request.departure,
|
||||
arrival: request.arrival,
|
||||
departure: toCityCode(request.departure, dictionaries),
|
||||
arrival: toCityCode(request.arrival, dictionaries),
|
||||
withReturn: request.mode === "RouteWithBack",
|
||||
}
|
||||
: {};
|
||||
writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state);
|
||||
navigate(`/${locale}/schedule`);
|
||||
},
|
||||
[navigate, locale],
|
||||
[navigate, locale, dictionaries],
|
||||
);
|
||||
|
||||
const scheduleFilter = (
|
||||
|
||||
Reference in New Issue
Block a user