diff --git a/client/src/components/Planner/ReservationModal.test.tsx b/client/src/components/Planner/ReservationModal.test.tsx index beedadaee..98e2348d6 100644 --- a/client/src/components/Planner/ReservationModal.test.tsx +++ b/client/src/components/Planner/ReservationModal.test.tsx @@ -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(); + 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(); + expect(title).toHaveValue('Le Jules Verne'); + }); }); diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx index 92c2a6301..f70448c96 100644 --- a/client/src/components/Planner/ReservationModal.tsx +++ b/client/src/components/Planner/ReservationModal.tsx @@ -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?.() diff --git a/client/src/components/Planner/TransportModal.test.tsx b/client/src/components/Planner/TransportModal.test.tsx index c6ca7d0c6..ea2e70f0b 100644 --- a/client/src/components/Planner/TransportModal.test.tsx +++ b/client/src/components/Planner/TransportModal.test.tsx @@ -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(); + 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(); + expect(screen.getByDisplayValue('First flight')).toBeInTheDocument(); + rerender(); + expect(screen.getByDisplayValue('Second flight')).toBeInTheDocument(); + }); }); diff --git a/client/src/components/Planner/TransportModal.tsx b/client/src/components/Planner/TransportModal.tsx index ccd0290a2..858eaa82b 100644 --- a/client/src/components/Planner/TransportModal.tsx +++ b/client/src/components/Planner/TransportModal.tsx @@ -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()