OnlineBoard search: render Купить/Онлайн регистрация in expanded row
Angular's board search results expansion shows [Купить] [Онлайн регистрация] [Детали рейса]. React only rendered Details. Added a `renderActions` prop on FlightCard/FlightList so the feature layer can inject extra buttons without the ui layer importing from features. OnlineBoardSearchPage wires it to FlightActions with showShare=false (the row already has a dedicated share icon). Visibility rules fall through to canBuyTicket / canRegister (same as BoardDetailsHeader), so cancelled/past flights still hide the Buy button and carriers without a registrationUrl still hide the Online Registration button — matching Angular's per-flight gating. Integration test mocks useAppSettings to avoid requiring the real ApiClientProvider in flight-search.test.tsx.
This commit is contained in:
@@ -18,6 +18,7 @@ import { useNavigate } from "@modern-js/runtime/router";
|
||||
import { useLocale } from "@/i18n/useLocale.js";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { FlightList } from "@/ui/flights/FlightList.js";
|
||||
import { FlightActions } from "./BoardDetailsHeader/FlightActions.js";
|
||||
import { findClosestFlightId } from "../closestFlight.js";
|
||||
import { PageLayout } from "@/ui/layout/PageLayout.js";
|
||||
import { PageTabs } from "@/ui/layout/PageTabs.js";
|
||||
@@ -514,6 +515,18 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
||||
onFlightClick={handleFlightClick}
|
||||
initialCurrentFlightId={initialCurrentFlightId}
|
||||
direction={params.type}
|
||||
renderActions={(flight) => (
|
||||
// Mirrors Angular's board search expansion: each row
|
||||
// shows [Купить] [Онлайн регистрация] alongside the
|
||||
// Details button. Visibility falls through to the
|
||||
// same canBuyTicket / canRegister rules we use in the
|
||||
// BoardDetailsHeader.
|
||||
<FlightActions
|
||||
flight={flight}
|
||||
locale={language}
|
||||
showShare={false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -46,6 +46,13 @@ export interface FlightCardProps {
|
||||
* pages to render the per-leg route diagram.
|
||||
*/
|
||||
renderExpandedBody?: (flight: ISimpleFlight) => ReactNode;
|
||||
/**
|
||||
* Extra action buttons rendered before the default share + details
|
||||
* buttons in the expanded actions row. Used by the online-board
|
||||
* search page to surface the same Купить / Онлайн регистрация
|
||||
* buttons Angular shows on the search results expansion.
|
||||
*/
|
||||
renderActions?: (flight: ISimpleFlight) => ReactNode;
|
||||
}
|
||||
|
||||
/** Extract the primary leg from a flight (first leg for multi-leg) */
|
||||
@@ -116,6 +123,7 @@ export const FlightCard: FC<FlightCardProps> = ({
|
||||
onViewDetails,
|
||||
direction = "route",
|
||||
renderExpandedBody,
|
||||
renderActions,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { language } = useLocale();
|
||||
@@ -415,6 +423,12 @@ export const FlightCard: FC<FlightCardProps> = ({
|
||||
>
|
||||
<img src="/assets/img/share.svg" alt="" aria-hidden="true" />
|
||||
</button>
|
||||
{/* Extra actions (Купить / Онлайн регистрация). Angular
|
||||
renders Buy + Registration + Details here on every
|
||||
expanded board row; the caller wires up FlightActions
|
||||
via renderActions so the visibility rules live with
|
||||
the feature instead of leaking into the ui layer. */}
|
||||
{renderActions?.(flight)}
|
||||
<button
|
||||
type="button"
|
||||
className="flight-card__details-btn"
|
||||
|
||||
@@ -33,6 +33,13 @@ export interface FlightListProps {
|
||||
* default time/transition rows.
|
||||
*/
|
||||
renderExpandedBody?: (flight: ISimpleFlight) => ReactNode;
|
||||
/**
|
||||
* Extra action buttons rendered in the default expanded-body actions
|
||||
* row, right before the share/details buttons. Threads down to each
|
||||
* FlightCard for the online-board search page where Angular shows
|
||||
* Купить / Онлайн регистрация alongside Детали рейса.
|
||||
*/
|
||||
renderActions?: (flight: ISimpleFlight) => ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,6 +56,7 @@ export const FlightList: FC<FlightListProps> = ({
|
||||
initialCurrentFlightId,
|
||||
direction = "route",
|
||||
renderExpandedBody,
|
||||
renderActions,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const cardRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
@@ -102,6 +110,7 @@ export const FlightList: FC<FlightListProps> = ({
|
||||
? { onViewDetails: () => onFlightClick(flight) }
|
||||
: {})}
|
||||
{...(renderExpandedBody ? { renderExpandedBody } : {})}
|
||||
{...(renderActions ? { renderActions } : {})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -64,6 +64,24 @@ vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||
useDictionaries: () => ({ dictionaries: null, loading: false, error: null }),
|
||||
}));
|
||||
|
||||
// FlightActions (rendered inside each expanded flight row) pulls the
|
||||
// buy/registration window lengths from useAppSettings, which itself
|
||||
// needs the ApiClientProvider. Integration tests don't wire the real
|
||||
// API client, so mock useAppSettings with the Angular defaults.
|
||||
vi.mock("@/shared/hooks/useAppSettings.js", () => ({
|
||||
useAppSettings: () => ({
|
||||
onlineboardSearchFrom: 1,
|
||||
onlineboardSearchTo: 7,
|
||||
scheduleSearchFrom: 1,
|
||||
scheduleSearchTo: 330,
|
||||
flightStatusAvailableFromHours: 2,
|
||||
buyTicketMinHours: 2,
|
||||
buyTicketMaxHours: 72,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user