fix(planner): a refresh of the trip no longer wipes an open transport or booking form

The transport form re-seeded itself whenever the trip's costs arrived as a
new array, and the booking form whenever days, places or stays did. A
socket reconnect re-reads the whole trip, so behind a proxy that drops idle
sockets every 15 to 30 seconds everything typed so far was wiped. Both forms
now re-seed only for what opens a different form; the data is read when the
form is seeded, as before.

Fixes #3069
This commit is contained in:
Maurice
2026-11-12 15:39:20 +01:00
parent 80130f7946
commit ac7094df05
4 changed files with 39 additions and 4 deletions
@@ -1,4 +1,4 @@
// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-109
// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-110
import { render, screen, waitFor, fireEvent, within, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
@@ -1829,4 +1829,13 @@ describe('ReservationModal', () => {
expect(onSave.mock.calls[0][0].confirmation_number).toBe('PNR-NEW');
});
});
it('FE-PLANNER-RESMODAL-110: a refresh of the trip while the dialog is open keeps what the user typed (#3069)', async () => {
const { rerender } = render(<ReservationModal {...defaultProps} />);
const title = screen.getByPlaceholderText(/e\.g\. Lufthansa/i) as HTMLInputElement;
await userEvent.type(title, 'Le Jules Verne');
// A socket reconnect re-reads the trip: days, places and stays arrive as new arrays.
rerender(<ReservationModal {...defaultProps} days={[]} places={[]} accommodations={[]} />);
expect(title).toHaveValue('Le Jules Verne');
});
});
@@ -132,8 +132,12 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
setForm({ ...EMPTY_FORM, assignment_id: defaultAssignmentId ?? '' })
setPendingFiles([])
}
// Keyed only on what opens a different form. Days, places and stays are read
// when the form is seeded; a refresh of the trip while the dialog is open (a
// socket reconnect re-reads it) hands in new arrays of the same data and must
// not reseed it and wipe what the user is typing (#3069).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reservation, prefill, isOpen, selectedDayId, defaultAssignmentId, days, places, accommodations])
}, [reservation, prefill, isOpen, selectedDayId, defaultAssignmentId])
const handleSubmit = async (e?: { preventDefault?: () => void }) => {
e?.preventDefault?.()
@@ -1,4 +1,4 @@
// FE-PLANNER-TRANSMODAL-001 to FE-PLANNER-TRANSMODAL-083
// FE-PLANNER-TRANSMODAL-001 to FE-PLANNER-TRANSMODAL-085
import { render, screen, waitFor, fireEvent, within, act } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
@@ -18,6 +18,7 @@ import {
buildAssignment,
buildReservation,
buildTripFile,
buildBudgetItem,
} from '../../../tests/helpers/factories';
import { TransportModal } from './TransportModal';
import type { Day, Reservation } from '../../types';
@@ -1725,4 +1726,22 @@ describe('TransportModal', () => {
const legs = onSave.mock.calls[0][0].metadata.legs as { confirmation_number?: string }[];
expect(legs.map(l => l.confirmation_number ?? null)).toEqual([null, 'SEG-TWO']);
});
it('FE-PLANNER-TRANSMODAL-084: a store refresh while the dialog is open keeps what the user typed (#3069)', async () => {
render(<TransportModal {...defaultProps} />);
const title = screen.getByPlaceholderText(/e\.g\. Lufthansa/i) as HTMLInputElement;
await userEvent.type(title, 'LH 400');
// A socket reconnect re-reads the trip: the costs arrive as a new array.
act(() => { useTripStore.setState({ budgetItems: [buildBudgetItem()] }); });
expect(title).toHaveValue('LH 400');
});
it('FE-PLANNER-TRANSMODAL-085: opening a different transport still fills the form with it', () => {
const first = buildReservation({ title: 'First flight', type: 'flight' });
const second = buildReservation({ title: 'Second flight', type: 'flight' });
const { rerender } = render(<TransportModal {...defaultProps} reservation={first} />);
expect(screen.getByDisplayValue('First flight')).toBeInTheDocument();
rerender(<TransportModal {...defaultProps} reservation={second} />);
expect(screen.getByDisplayValue('Second flight')).toBeInTheDocument();
});
});
@@ -135,7 +135,10 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
meta_seat: meta.seat || '',
}),
})
}, [isOpen, reservation, prefill, selectedDayId, budgetItems])
// Keyed only on what opens a different form. A store refresh while the dialog
// is open (a socket reconnect re-reads the trip, a fellow traveller's cost) must
// not reseed it and wipe what the user is typing (#3069).
}, [isOpen, reservation, prefill, selectedDayId])
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault()