Airport popup Relocate works for flying/repairing planes via queue (ISS-096, v0.49.0)
CI / build + unit + e2e (pull_request) Successful in 12m56s
CI / build + unit + e2e (pull_request) Successful in 12m56s
The airport popup's Relocate row now lists ALL planes instead of only grounded ones; the click handler picks the endpoint by the selected plane's current status — available → /fly (unchanged), flying/repairing → /queue so the destination is appended to the flight queue and chains on landing. No-planes fallback reworded to 'No planes to relocate.' E2E +1: relocation.spec.ts dispatches the starter on a leg via API, slows the sim to 1x, opens the popup for a different airport, selects the flying plane and clicks Relocate here — asserts the queue contains the clicked ICAO while the plane still flies its original destination.
This commit is contained in:
+14
-9
@@ -231,21 +231,22 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
|
||||
// deterministic generated weather — no gameplay effects per PLAN.md.
|
||||
const w = weatherFor(p.icao);
|
||||
const localTime = approxLocalTime(p.lon, Date.now());
|
||||
// ISS-010 (v0.18.0): Relocate row — grounded planes from the latest snapshot can be
|
||||
// repositioned to this airport via POST /api/planes/{id}/fly (pure repositioning).
|
||||
// ISS-010 (v0.18.0): Relocate row — every plane from the latest snapshot can be sent to
|
||||
// this airport: grounded ones fly immediately (POST /api/planes/{id}/fly), flying/repairing
|
||||
// ones get the leg appended to their queue (POST /api/planes/{id}/queue, ISS-096 v0.49.0).
|
||||
const st = stateRef.current;
|
||||
const groundedPlanes = st ? st.planes.filter((pl) => pl.status === 'available') : [];
|
||||
const planes = st ? st.planes : [];
|
||||
const planeLabel = (model: string) => st?.catalog.find((c) => c.model === model)?.label ?? model;
|
||||
const relocateHtml =
|
||||
groundedPlanes.length > 0
|
||||
planes.length > 0
|
||||
? `<select data-testid="airport-popup-plane" aria-label="Plane to relocate here">` +
|
||||
`<option value="">Select a plane\u2026</option>` +
|
||||
groundedPlanes.map(
|
||||
planes.map(
|
||||
(pl) =>
|
||||
`<option value="${escapeHtml(pl.id)}">${escapeHtml(planeLabel(pl.model))} \u2014 ${escapeHtml(pl.locationIcao)}</option>`,
|
||||
).join('') +
|
||||
`</select><button type="button" data-testid="airport-popup-relocate" disabled>Relocate here</button>`
|
||||
: `<span data-testid="airport-popup-no-planes">No grounded planes to relocate.</span>`;
|
||||
: `<span data-testid="airport-popup-no-planes">No planes to relocate.</span>`;
|
||||
const popup = new maplibregl.Popup({ offset: 10 })
|
||||
.setLngLat(f.geometry.coordinates as [number, number])
|
||||
.setHTML(
|
||||
@@ -263,7 +264,7 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
|
||||
const el = popup.getElement();
|
||||
const select = el?.querySelector<HTMLSelectElement>('[data-testid="airport-popup-plane"]');
|
||||
const button = el?.querySelector<HTMLButtonElement>('[data-testid="airport-popup-relocate"]');
|
||||
// The status span is always in the popup HTML (even with no grounded planes), so it
|
||||
// The status span is always in the popup HTML (even with no planes), so it
|
||||
// exists whenever the select/button do.
|
||||
const statusEl = el?.querySelector<HTMLElement>('[data-testid="airport-popup-relocate-status"]');
|
||||
if (select && button && statusEl) {
|
||||
@@ -276,7 +277,11 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
|
||||
button.disabled = true;
|
||||
statusEl.textContent = 'Relocating\u2026';
|
||||
try {
|
||||
const res = await fetch(`/api/planes/${planeId}/fly`, {
|
||||
// ISS-096 (v0.49.0): endpoint follows the SELECTED plane's current status — grounded
|
||||
// planes fly now, flying/repairing ones get the leg queued for after landing.
|
||||
const planeStatus = stateRef.current?.planes.find((pl) => pl.id === planeId)?.status ?? 'available';
|
||||
const action = planeStatus === 'available' ? 'fly' : 'queue';
|
||||
const res = await fetch(`/api/planes/${planeId}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ destIcao: p.icao }),
|
||||
@@ -285,7 +290,7 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
|
||||
const data = (await res.json()) as StateSnapshot & { error?: string };
|
||||
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
||||
applySnapshotRef.current(data); // shared shell state updates immediately, like the panes' runAction
|
||||
statusEl.textContent = 'Relocation started.';
|
||||
statusEl.textContent = action === 'fly' ? 'Relocation started.' : 'Added to queue.';
|
||||
} catch (err) {
|
||||
statusEl.textContent = err instanceof Error ? err.message : String(err);
|
||||
button.disabled = false; // allow retry / picking another plane
|
||||
|
||||
Reference in New Issue
Block a user