Add popular requests pre-fill implementation plan
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
# Popular Requests Form Pre-fill Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make popular request clicks pre-fill the search form on the start page via query params, matching Angular behavior.
|
||||
|
||||
**Architecture:** Each start page reads query params via `useSearchParams()` from `@modern-js/runtime/router`. The click handler builds a URL with query params and navigates to the same page. The form initializes state from those params.
|
||||
|
||||
**Tech Stack:** React, Modern.js router (`useSearchParams`, `useNavigate`), existing `PopularRequest` types.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Modified files
|
||||
- `src/features/online-board/components/OnlineBoardStartPage.tsx` — Add click handler + query param reading + pass initial props to filter
|
||||
- `src/features/schedule/components/ScheduleStartPage.tsx` — Add click handler + query param reading + initialize form state from params
|
||||
|
||||
### No changes needed
|
||||
- `src/features/online-board/components/OnlineBoardFilter.tsx` — Already accepts `initialDeparture`, `initialArrival`, `initialDate`, `initialTab`, `initialFlightNumber` props
|
||||
- `src/features/popular-requests/` — No changes to any popular requests components
|
||||
|
||||
---
|
||||
|
||||
### Task 1: OnlineBoardStartPage — Pre-fill on Popular Request Click
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/features/online-board/components/OnlineBoardStartPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `src/features/online-board/components/OnlineBoardStartPage.test.tsx`:
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { buildPopularRequestQueryParams } from "./OnlineBoardStartPage.js";
|
||||
|
||||
describe("buildPopularRequestQueryParams", () => {
|
||||
it("returns tab=flight with carrier and flight for FlightNumber mode", () => {
|
||||
const params = buildPopularRequestQueryParams({
|
||||
mode: "FlightNumber",
|
||||
carrier: "SU",
|
||||
flightNumber: "0654",
|
||||
type: "Onlineboard",
|
||||
});
|
||||
expect(params.get("tab")).toBe("flight");
|
||||
expect(params.get("carrier")).toBe("SU");
|
||||
expect(params.get("flight")).toBe("0654");
|
||||
});
|
||||
|
||||
it("returns tab=route with departure for Departure mode", () => {
|
||||
const params = buildPopularRequestQueryParams({
|
||||
mode: "Departure",
|
||||
departure: "LED",
|
||||
type: "Onlineboard",
|
||||
});
|
||||
expect(params.get("tab")).toBe("route");
|
||||
expect(params.get("departure")).toBe("LED");
|
||||
expect(params.has("arrival")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns tab=route with arrival for Arrival mode", () => {
|
||||
const params = buildPopularRequestQueryParams({
|
||||
mode: "Arrival",
|
||||
arrival: "VKO",
|
||||
type: "Onlineboard",
|
||||
});
|
||||
expect(params.get("tab")).toBe("route");
|
||||
expect(params.get("arrival")).toBe("VKO");
|
||||
expect(params.has("departure")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns tab=route with both cities for Route mode", () => {
|
||||
const params = buildPopularRequestQueryParams({
|
||||
mode: "Route",
|
||||
departure: "LED",
|
||||
arrival: "KRR",
|
||||
type: "Onlineboard",
|
||||
});
|
||||
expect(params.get("tab")).toBe("route");
|
||||
expect(params.get("departure")).toBe("LED");
|
||||
expect(params.get("arrival")).toBe("KRR");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pnpm vitest run src/features/online-board/components/OnlineBoardStartPage.test.tsx`
|
||||
|
||||
Expected: FAIL — `buildPopularRequestQueryParams` is not exported.
|
||||
|
||||
- [ ] **Step 3: Implement the query param builder and wire up the component**
|
||||
|
||||
Edit `src/features/online-board/components/OnlineBoardStartPage.tsx`. Replace the entire file with:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* Online Board start page matching the Angular `online-board-start-page`
|
||||
* component DOM structure and CSS class names.
|
||||
*
|
||||
* Reads query params (set by popular request clicks) and passes them
|
||||
* as initial props to OnlineBoardFilter for form pre-fill.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { type FC, useCallback } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { PageLayout } from "@/ui/layout/PageLayout.js";
|
||||
import { PageTabs } from "@/ui/layout/PageTabs.js";
|
||||
import { SearchHistory } from "@/ui/layout/SearchHistory.js";
|
||||
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
|
||||
import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js";
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
import "./OnlineBoardStartPage.scss";
|
||||
|
||||
/** Build query params from a popular request for form pre-fill. */
|
||||
export function buildPopularRequestQueryParams(request: PopularRequest): URLSearchParams {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
switch (request.mode) {
|
||||
case "FlightNumber":
|
||||
params.set("tab", "flight");
|
||||
params.set("carrier", request.carrier);
|
||||
params.set("flight", request.flightNumber);
|
||||
break;
|
||||
case "Departure":
|
||||
params.set("tab", "route");
|
||||
params.set("departure", request.departure);
|
||||
break;
|
||||
case "Arrival":
|
||||
params.set("tab", "route");
|
||||
params.set("arrival", request.arrival);
|
||||
break;
|
||||
case "Route":
|
||||
case "RouteWithBack":
|
||||
params.set("tab", "route");
|
||||
params.set("departure", request.departure);
|
||||
params.set("arrival", request.arrival);
|
||||
break;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export const OnlineBoardStartPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const routeParams = useParams<{ lang: string }>();
|
||||
const lang = routeParams.lang ?? "ru";
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const handlePopularRequestClick = useCallback((request: PopularRequest) => {
|
||||
// Schedule-type requests navigate to the schedule page
|
||||
if (request.type === "Schedule") {
|
||||
const params = new URLSearchParams();
|
||||
if (request.mode === "Route" || request.mode === "RouteWithBack") {
|
||||
params.set("departure", request.departure);
|
||||
params.set("arrival", request.arrival);
|
||||
if (request.mode === "RouteWithBack") {
|
||||
params.set("return", "true");
|
||||
}
|
||||
}
|
||||
void navigate(`/${lang}/schedule?${params.toString()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const qp = buildPopularRequestQueryParams(request);
|
||||
void navigate(`/${lang}/onlineboard?${qp.toString()}`);
|
||||
}, [navigate, lang]);
|
||||
|
||||
// Read query params for filter pre-fill
|
||||
const initialTab = searchParams.get("tab") === "flight" ? "flight" as const : searchParams.has("tab") ? "route" as const : undefined;
|
||||
const initialFlightNumber = searchParams.get("carrier") && searchParams.get("flight")
|
||||
? `${searchParams.get("carrier")}${searchParams.get("flight")}`
|
||||
: undefined;
|
||||
const initialDeparture = searchParams.get("departure") ?? undefined;
|
||||
const initialArrival = searchParams.get("arrival") ?? undefined;
|
||||
|
||||
return (
|
||||
<div className="online-board-start-page" data-testid="online-board-start">
|
||||
<PageLayout
|
||||
headerLeft={
|
||||
<PageTabs viewType="onlineboard" />
|
||||
}
|
||||
title={
|
||||
<h1 className="text--white page-title">
|
||||
{t("BOARD.TITLE")}
|
||||
</h1>
|
||||
}
|
||||
breadcrumbs={[]}
|
||||
contentLeft={
|
||||
<>
|
||||
<OnlineBoardFilter
|
||||
initialTab={initialTab}
|
||||
initialFlightNumber={initialFlightNumber}
|
||||
initialDeparture={initialDeparture}
|
||||
initialArrival={initialArrival}
|
||||
/>
|
||||
<SearchHistory />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<section className="frame">
|
||||
<h2>{t("BOARD.BOARD-START")}</h2>
|
||||
|
||||
<div className="titles-container">
|
||||
<div className="title title1">
|
||||
<a>{t("BOARD.BOARD-START-TITLE1")}</a>
|
||||
<div>
|
||||
{t("BOARD.BOARD-START-TITLE1-DESCRIPTION")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="title title2">
|
||||
<a>{t("BOARD.BOARD-START-TITLE2")}</a>
|
||||
<div>
|
||||
{t("BOARD.BOARD-START-TITLE2-DESCRIPTION")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="title title3">
|
||||
<a>{t("BOARD.BOARD-START-TITLE3")}</a>
|
||||
<div>
|
||||
{t("BOARD.BOARD-START-TITLE3-DESCRIPTION")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="title title4">
|
||||
<a>{t("BOARD.BOARD-START-TITLE4")}</a>
|
||||
<div>
|
||||
{t("BOARD.BOARD-START-TITLE4-DESCRIPTION")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PopularRequestsPanel onRequestClick={handlePopularRequestClick} />
|
||||
</section>
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pnpm vitest run src/features/online-board/components/OnlineBoardStartPage.test.tsx`
|
||||
|
||||
Expected: PASS — all 4 tests pass.
|
||||
|
||||
- [ ] **Step 5: Run existing tests to check for regressions**
|
||||
|
||||
Run: `pnpm test`
|
||||
|
||||
Expected: All existing tests pass. No regressions.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/features/online-board/components/OnlineBoardStartPage.tsx src/features/online-board/components/OnlineBoardStartPage.test.tsx
|
||||
git commit -m "Wire popular request clicks to pre-fill OnlineBoard filter via query params"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: ScheduleStartPage — Pre-fill on Popular Request Click
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/features/schedule/components/ScheduleStartPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `src/features/schedule/components/ScheduleStartPage.test.tsx`:
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildSchedulePopularRequestQueryParams } from "./ScheduleStartPage.js";
|
||||
|
||||
describe("buildSchedulePopularRequestQueryParams", () => {
|
||||
it("returns departure and arrival for Route mode", () => {
|
||||
const params = buildSchedulePopularRequestQueryParams({
|
||||
mode: "Route",
|
||||
departure: "LED",
|
||||
arrival: "KRR",
|
||||
type: "Schedule",
|
||||
});
|
||||
expect(params.get("departure")).toBe("LED");
|
||||
expect(params.get("arrival")).toBe("KRR");
|
||||
expect(params.has("return")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns departure, arrival, and return=true for RouteWithBack mode", () => {
|
||||
const params = buildSchedulePopularRequestQueryParams({
|
||||
mode: "RouteWithBack",
|
||||
departure: "VKO",
|
||||
arrival: "KUF",
|
||||
type: "Schedule",
|
||||
});
|
||||
expect(params.get("departure")).toBe("VKO");
|
||||
expect(params.get("arrival")).toBe("KUF");
|
||||
expect(params.get("return")).toBe("true");
|
||||
});
|
||||
|
||||
it("returns empty params for non-route modes", () => {
|
||||
const params = buildSchedulePopularRequestQueryParams({
|
||||
mode: "Arrival",
|
||||
arrival: "VKO",
|
||||
type: "Onlineboard",
|
||||
});
|
||||
expect(params.toString()).toBe("");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pnpm vitest run src/features/schedule/components/ScheduleStartPage.test.tsx`
|
||||
|
||||
Expected: FAIL — `buildSchedulePopularRequestQueryParams` is not exported.
|
||||
|
||||
- [ ] **Step 3: Implement the query param builder and wire up the component**
|
||||
|
||||
Edit `src/features/schedule/components/ScheduleStartPage.tsx`. Make these changes:
|
||||
|
||||
**Add imports** — add `useSearchParams` to the router import:
|
||||
|
||||
```tsx
|
||||
import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router";
|
||||
```
|
||||
|
||||
**Add the exported builder function** — after the `addDays` helper, before the component:
|
||||
|
||||
```tsx
|
||||
/** Build query params from a popular request for schedule form pre-fill. */
|
||||
export function buildSchedulePopularRequestQueryParams(request: PopularRequest): URLSearchParams {
|
||||
const params = new URLSearchParams();
|
||||
if (request.mode === "Route" || request.mode === "RouteWithBack") {
|
||||
params.set("departure", request.departure);
|
||||
params.set("arrival", request.arrival);
|
||||
if (request.mode === "RouteWithBack") {
|
||||
params.set("return", "true");
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
```
|
||||
|
||||
**Read search params in the component** — after the `lang` line, add:
|
||||
|
||||
```tsx
|
||||
const [searchParams] = useSearchParams();
|
||||
```
|
||||
|
||||
**Initialize form state from query params** — change the `useState` calls:
|
||||
|
||||
```tsx
|
||||
const [departureAirport, setDepartureAirport] = useState<CitySuggestion | string>(searchParams.get("departure") ?? "");
|
||||
const [arrivalAirport, setArrivalAirport] = useState<CitySuggestion | string>(searchParams.get("arrival") ?? "");
|
||||
const [dateFrom, setDateFrom] = useState<Date | null>(today);
|
||||
const [dateTo, setDateTo] = useState<Date | null>(addDays(today, 7));
|
||||
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
|
||||
const [directOnly, setDirectOnly] = useState(false);
|
||||
const [isRoundTrip, setIsRoundTrip] = useState(searchParams.get("return") === "true");
|
||||
const [returnDateFrom, setReturnDateFrom] = useState<Date | null>(addDays(today, 7));
|
||||
const [returnDateTo, setReturnDateTo] = useState<Date | null>(addDays(today, 14));
|
||||
const [returnTimeRange, setReturnTimeRange] = useState<[number, number]>([0, 1440]);
|
||||
```
|
||||
|
||||
**Implement the click handler** — replace the empty callback:
|
||||
|
||||
```tsx
|
||||
const handlePopularRequestClick = useCallback((request: PopularRequest) => {
|
||||
// Onlineboard-type requests navigate to the onlineboard page
|
||||
if (request.type === "Onlineboard") {
|
||||
const params = new URLSearchParams();
|
||||
switch (request.mode) {
|
||||
case "FlightNumber":
|
||||
params.set("tab", "flight");
|
||||
params.set("carrier", request.carrier);
|
||||
params.set("flight", request.flightNumber);
|
||||
break;
|
||||
case "Departure":
|
||||
params.set("tab", "route");
|
||||
params.set("departure", request.departure);
|
||||
break;
|
||||
case "Arrival":
|
||||
params.set("tab", "route");
|
||||
params.set("arrival", request.arrival);
|
||||
break;
|
||||
case "Route":
|
||||
params.set("tab", "route");
|
||||
params.set("departure", request.departure);
|
||||
params.set("arrival", request.arrival);
|
||||
break;
|
||||
}
|
||||
void navigate(`/${lang}/onlineboard?${params.toString()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const qp = buildSchedulePopularRequestQueryParams(request);
|
||||
void navigate(`/${lang}/schedule?${qp.toString()}`);
|
||||
}, [navigate, lang]);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pnpm vitest run src/features/schedule/components/ScheduleStartPage.test.tsx`
|
||||
|
||||
Expected: PASS — all 3 tests pass.
|
||||
|
||||
- [ ] **Step 5: Run all tests**
|
||||
|
||||
Run: `pnpm test`
|
||||
|
||||
Expected: All tests pass, no regressions.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/features/schedule/components/ScheduleStartPage.tsx src/features/schedule/components/ScheduleStartPage.test.tsx
|
||||
git commit -m "Wire popular request clicks to pre-fill Schedule form via query params"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Manual Verification
|
||||
|
||||
- [ ] **Step 1: Start the React dev server**
|
||||
|
||||
Run: `pnpm dev`
|
||||
|
||||
- [ ] **Step 2: Navigate to Online Board start page and test popular requests**
|
||||
|
||||
Open `http://localhost:8080/ru/onlineboard` in browser.
|
||||
|
||||
Verify:
|
||||
- Click each popular request item
|
||||
- URL updates with query params (e.g., `?tab=flight&carrier=SU&flight=0654`)
|
||||
- Filter form is pre-filled with the correct values
|
||||
- Clicking "Search" from the pre-filled form navigates to the correct search results page
|
||||
|
||||
- [ ] **Step 3: Navigate to Schedule start page and test popular requests**
|
||||
|
||||
Open `http://localhost:8080/ru/schedule` in browser.
|
||||
|
||||
Verify:
|
||||
- Click a Route-type popular request
|
||||
- URL updates with query params (e.g., `?departure=LED&arrival=KRR`)
|
||||
- Departure and arrival fields are pre-filled
|
||||
- For RouteWithBack: round-trip checkbox is checked
|
||||
- Clicking "Search" navigates to schedule search results
|
||||
|
||||
- [ ] **Step 4: Run comparison pipeline to verify improvement**
|
||||
|
||||
Run: `pnpm compare:behavior`
|
||||
|
||||
Expected: Popular requests tests (spec 19) that previously failed should now pass for the React project.
|
||||
Reference in New Issue
Block a user