fix: v0.10.1 — dispatch pickers independent per accepted contract (placeholder default, no first-eligible auto-fallback; stale-select fix via explicit placeholder option)
This commit is contained in:
@@ -22,10 +22,25 @@ function dispatchReason(p: PlaneState, contract: ContractState): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The plane a dispatch picker should act on for one accepted contract: the user's explicit pick
|
||||
// while it stays eligible, else NOTHING. v0.10.1 removed the "auto-track first eligible" fallback —
|
||||
// it made every row display the same plane by default (selections looked shared across contracts)
|
||||
// and left no way to show "nothing chosen". null = placeholder shown + Dispatch disabled; a poll
|
||||
// that makes the pick ineligible (plane flew / moved away) drops back to the placeholder too.
|
||||
export function dispatchSelection(
|
||||
planes: PlaneState[],
|
||||
contract: ContractState,
|
||||
selectedId: string | null,
|
||||
): PlaneState | null {
|
||||
if (selectedId === null) return null;
|
||||
return planes.find((p) => p.id === selectedId && dispatchReason(p, contract) === null) ?? null;
|
||||
}
|
||||
|
||||
// Dispatch controls for one accepted, not-yet-dispatched contract: a plane picker where only
|
||||
// grounded planes at the origin are enabled (others disabled with the reason), plus a dispatch
|
||||
// button bound to the selection. The effective selection is the user's pick while it stays
|
||||
// eligible, else auto-tracks the first eligible plane so a fresh contract needs one click.
|
||||
// button bound to the selection. Each row owns its selection in local state and defaults to the
|
||||
// "Select a plane…" placeholder — never preselecting — so rows stay visibly independent and a
|
||||
// dispatch can't fire on the wrong contract by accident.
|
||||
function DispatchControls({
|
||||
contract,
|
||||
planes,
|
||||
@@ -38,10 +53,7 @@ function DispatchControls({
|
||||
onDispatch: (planeId: string) => void;
|
||||
}) {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const effective =
|
||||
planes.find((p) => p.id === selectedId && dispatchReason(p, contract) === null) ??
|
||||
planes.find((p) => dispatchReason(p, contract) === null) ??
|
||||
null;
|
||||
const effective = dispatchSelection(planes, contract, selectedId);
|
||||
|
||||
return (
|
||||
<div className="dispatch-controls">
|
||||
@@ -51,6 +63,7 @@ function DispatchControls({
|
||||
value={effective?.id ?? ''}
|
||||
onChange={(e) => setSelectedId(e.target.value || null)}
|
||||
>
|
||||
<option value="">Select a plane…</option>
|
||||
{planes.map((p) => {
|
||||
const reason = dispatchReason(p, contract);
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { dispatchSelection } from './RightPane';
|
||||
import type { ContractState, PlaneState } from '@/lib/game/state';
|
||||
|
||||
// v0.10.1 — the per-contract dispatch picker rule: a row's effective selection is the user's
|
||||
// explicit pick while it stays eligible (available AND at the contract origin), else NOTHING
|
||||
// (placeholder). The old "auto-track first eligible" fallback made every accepted-contract row
|
||||
// display the same plane, so selections looked shared across contracts.
|
||||
|
||||
const contract: ContractState = {
|
||||
id: 'contract-a',
|
||||
originIcao: 'EDDF',
|
||||
originName: 'Frankfurt Main',
|
||||
destIcao: 'EDDM',
|
||||
destName: 'Munich',
|
||||
reward: 1000,
|
||||
cargoType: 'mail',
|
||||
cargoCount: 4,
|
||||
deadlineSimTime: 1_000_000,
|
||||
status: 'accepted',
|
||||
planeId: null,
|
||||
createdSimTime: 0,
|
||||
};
|
||||
|
||||
function plane(over: Partial<PlaneState> & Pick<PlaneState, 'id'>): PlaneState {
|
||||
return {
|
||||
model: 'atr72',
|
||||
status: 'available',
|
||||
fuel: 100,
|
||||
maxFuel: 100,
|
||||
locationIcao: 'EDDF',
|
||||
locationName: 'Frankfurt Main',
|
||||
contractId: null,
|
||||
departureSimTime: null,
|
||||
lat: 50,
|
||||
lon: 8.5,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe('dispatchSelection (per-contract dispatch picker rule)', () => {
|
||||
it('defaults to the placeholder — no auto-selection of the first eligible plane', () => {
|
||||
const planes = [plane({ id: 'p1' }), plane({ id: 'p2' })]; // both eligible at EDDF
|
||||
expect(dispatchSelection(planes, contract, null)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns the user pick while it stays eligible (available AND at the origin)', () => {
|
||||
const planes = [plane({ id: 'p1' }), plane({ id: 'p2', locationIcao: 'EDDM' })];
|
||||
expect(dispatchSelection(planes, contract, 'p1')?.id).toBe('p1');
|
||||
});
|
||||
|
||||
it('drops to the placeholder when a poll makes the pick ineligible (flew away)', () => {
|
||||
const planes = [plane({ id: 'p1', status: 'flying' })];
|
||||
expect(dispatchSelection(planes, contract, 'p1')).toBeNull();
|
||||
});
|
||||
|
||||
it('drops to the placeholder when a poll moves the pick off the origin', () => {
|
||||
const planes = [plane({ id: 'p1', locationIcao: 'EDDM' })];
|
||||
expect(dispatchSelection(planes, contract, 'p1')).toBeNull();
|
||||
});
|
||||
|
||||
it('unknown selection id resolves to the placeholder', () => {
|
||||
const planes = [plane({ id: 'p1' })];
|
||||
expect(dispatchSelection(planes, contract, 'no-such-plane')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user