From 18a16d2aa28231ef050ad8b8372fe734deb89339 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 7 Nov 2026 23:19:51 +0100 Subject: [PATCH] fix(mobile): forms on the shared form sheet, and the details the review found Expense, payment, booking, transport, day note, place edit, new trip, journey entry, collab note, poll, task, packing item and packing import use the form sheet: one header with its icon tile, a body that scrolls with a fade, the footer, and full height with the actions beside the title on a short window. Create New Trip loses its nested section cards. Details: - Add payment gets its icon tile and the currency inside the amount field. - The expense's category field has the same fill as every other field. - Day and date fields keep their year at phone width; From and To stack in a narrow form. - The place sheet's category chip sits under the name, uncut, the photo on the cards' leading edge, labels in the sheets' small capitals. - Stars no longer keep a hover preview after a tap. - A hotel booking shows its stay's check-in and check-out when it has none of its own, instead of the grey hint reading as a saved time. - The collab note's title field is labelled Title (common.title). - Shorter airport hint in German and Russian; long hints end in an ellipsis. - The journey story grows with its lines (useAutoGrow). - Member avatars show the member's initial, yours too, and turn over on a selected chip. - The trip's More sheet scrolls, dims a short window and is marked in the dock while open; long titles wrap to two lines. --- .../Planner/reservationFormModel.test.ts | 40 ++++ .../Planner/reservationFormModel.ts | 10 +- .../src/components/shared/StarRating.test.tsx | 14 +- client/src/components/shared/StarRating.tsx | 8 +- .../screens/dashboard/MNewTripSheet.tsx | 208 +++++++++--------- .../screens/journey/MJourneyEntrySheet.tsx | 58 ++--- .../screens/notifications/MNotifications.tsx | 4 +- client/src/mobile/screens/trip/MTripShell.tsx | 8 +- .../mobile/screens/trip/sheets/MCostSheet.tsx | 30 +-- .../mobile/screens/trip/sheets/MMehrSheet.tsx | 12 +- .../mobile/screens/trip/sheets/MNoteSheet.tsx | 8 +- .../screens/trip/sheets/MPlaceEditSheet.tsx | 8 +- .../screens/trip/sheets/MPlaceSheet.tsx | 38 ++-- .../screens/trip/sheets/MReservationSheet.tsx | 19 +- .../trip/sheets/MTransportFormSheet.tsx | 22 +- .../mobile/screens/trip/tabs/MCollabNotes.tsx | 10 +- .../mobile/screens/trip/tabs/MCollabPolls.tsx | 8 +- .../screens/trip/tabs/MCostsPayment.tsx | 71 +++--- .../mobile/screens/trip/tabs/MCostsTab.tsx | 6 +- .../screens/trip/tabs/MPackItemSheet.tsx | 8 +- .../screens/trip/tabs/MPackingImportSheet.tsx | 8 +- .../mobile/screens/trip/tabs/MTaskSheet.tsx | 8 +- .../mobile/dashboard/MNewTripSheet.test.tsx | 12 + .../journey/MJourneyEntrySheet.test.tsx | 4 +- .../unit/mobile/trip/MCollabNotes.test.tsx | 3 + .../unit/mobile/trip/MCostSheet.test.tsx | 11 +- .../tests/unit/mobile/trip/MCostsTab.test.tsx | 3 +- .../unit/mobile/trip/MPlaceSheet.test.tsx | 13 +- .../unit/mobile/trip/MTripShell.test.tsx | 15 ++ shared/src/i18n/ar/common.ts | 1 + shared/src/i18n/az/common.ts | 1 + shared/src/i18n/br/common.ts | 1 + shared/src/i18n/ca/common.ts | 1 + shared/src/i18n/cs/common.ts | 1 + shared/src/i18n/de/airport.ts | 2 +- shared/src/i18n/de/common.ts | 1 + shared/src/i18n/en/common.ts | 1 + shared/src/i18n/es/common.ts | 1 + shared/src/i18n/et/common.ts | 1 + shared/src/i18n/fr/common.ts | 1 + shared/src/i18n/gr/common.ts | 1 + shared/src/i18n/he/common.ts | 1 + shared/src/i18n/hu/common.ts | 1 + shared/src/i18n/id/common.ts | 1 + shared/src/i18n/it/common.ts | 1 + shared/src/i18n/ja/common.ts | 1 + shared/src/i18n/ko/common.ts | 1 + shared/src/i18n/nl/common.ts | 1 + shared/src/i18n/pl/common.ts | 1 + shared/src/i18n/ru/airport.ts | 2 +- shared/src/i18n/ru/common.ts | 1 + shared/src/i18n/sk/common.ts | 1 + shared/src/i18n/sv/common.ts | 1 + shared/src/i18n/th/common.ts | 1 + shared/src/i18n/tr/common.ts | 1 + shared/src/i18n/uk/common.ts | 1 + shared/src/i18n/vi/common.ts | 1 + shared/src/i18n/zh-TW/common.ts | 1 + shared/src/i18n/zh/common.ts | 1 + 59 files changed, 427 insertions(+), 272 deletions(-) diff --git a/client/src/components/Planner/reservationFormModel.test.ts b/client/src/components/Planner/reservationFormModel.test.ts index e980949bc..82958d0de 100644 --- a/client/src/components/Planner/reservationFormModel.test.ts +++ b/client/src/components/Planner/reservationFormModel.test.ts @@ -112,6 +112,46 @@ describe('reservation form fields', () => { }); }); + it('FE-PLANNER-RESFORM-017: a hotel booking without its own times shows the ones its stay keeps', () => { + // The stay's check-in and check-out live on the accommodation; a booking that + // never had them written into its own metadata showed the "15:00" hint as if + // it were the saved time. + const acc = { + id: 9, + place_id: 4, + start_day_id: 2, + end_day_id: 3, + check_in: '15:00', + check_in_end: '22:00', + check_out: '11:00', + } as Accommodation; + const res = buildReservation({ + type: 'hotel', + accommodation_id: 9, + metadata: JSON.stringify({ price: '2184' }), + } as Partial); + expect(reservationFieldsFrom(res, [acc], [], false)).toMatchObject({ + meta_check_in_time: '15:00', + meta_check_out_time: '11:00', + }); + expect(reservationFieldsFrom(res, [acc], [], true)).toMatchObject({ meta_check_in_end_time: '22:00' }); + // The booking's own times still win. + const own = buildReservation({ + type: 'hotel', + accommodation_id: 9, + metadata: JSON.stringify({ check_in_time: '14:00', check_out_time: '10:00' }), + } as Partial); + expect(reservationFieldsFrom(own, [acc], [], false)).toMatchObject({ + meta_check_in_time: '14:00', + meta_check_out_time: '10:00', + }); + // Nothing anywhere stays empty, so the hint shows as a hint. + expect(reservationFieldsFrom(buildReservation({ type: 'hotel' }), [], [], false)).toMatchObject({ + meta_check_in_time: '', + meta_check_out_time: '', + }); + }); + it('FE-PLANNER-RESFORM-004: an import under review resolves its stay days and keeps its source details', () => { const pf = { title: 'Inn', diff --git a/client/src/components/Planner/reservationFormModel.ts b/client/src/components/Planner/reservationFormModel.ts index 47b83760c..0f5cfb864 100644 --- a/client/src/components/Planner/reservationFormModel.ts +++ b/client/src/components/Planner/reservationFormModel.ts @@ -97,9 +97,13 @@ export function reservationFieldsFrom( url: res.url || '', place_id: res.place_id || '', accommodation_id: res.accommodation_id || '', - meta_check_in_time: meta.check_in_time || '', - ...(withCheckInEnd ? { meta_check_in_end_time: meta.check_in_end_time || '' } : {}), - meta_check_out_time: meta.check_out_time || '', + // The stay's own times when the booking carries none: a hotel booked before + // the times were written on it, or whose stay was edited from the plan, + // keeps them on the accommodation, and the empty field showed the + // "15:00" hint as if it were the saved check-in. + meta_check_in_time: meta.check_in_time || acc?.check_in || '', + ...(withCheckInEnd ? { meta_check_in_end_time: meta.check_in_end_time || acc?.check_in_end || '' } : {}), + meta_check_out_time: meta.check_out_time || acc?.check_out || '', hotel_place_id: acc?.place_id || '', hotel_start_day: acc?.start_day_id || '', hotel_end_day: acc?.end_day_id || '', diff --git a/client/src/components/shared/StarRating.test.tsx b/client/src/components/shared/StarRating.test.tsx index c02fe01b5..03f217bae 100644 --- a/client/src/components/shared/StarRating.test.tsx +++ b/client/src/components/shared/StarRating.test.tsx @@ -70,13 +70,23 @@ describe('Stars', () => { const filled = () => Array.from(container.querySelectorAll('span > span')).filter(o => (o as HTMLElement).style.width).length expect(filled()).toBe(1) - fireEvent.mouseEnter(screen.getByRole('radio', { name: '4' })) + fireEvent.pointerEnter(screen.getByRole('radio', { name: '4' }), { pointerType: 'mouse' }) expect(filled()).toBe(4) - fireEvent.mouseLeave(container.querySelector('[role="radiogroup"]')!) + fireEvent.pointerLeave(container.querySelector('[role="radiogroup"]')!, { pointerType: 'mouse' }) expect(filled()).toBe(1) }) + it('FE-W4STAR-016: a finger previews nothing, so a tap cannot leave the stars filled', () => { + // A tap fires an enter with no leave after it: the preview used to stick, + // and an unrated place showed five full stars beside "Not rated yet". + const { container } = render( {}} />) + const filled = () => Array.from(container.querySelectorAll('span > span')).filter(o => (o as HTMLElement).style.width).length + + fireEvent.pointerEnter(screen.getByRole('radio', { name: '5' }), { pointerType: 'touch' }) + expect(filled()).toBe(0) + }) + it('FE-W4STAR-008: focus previews and blur clears the preview', () => { const { container } = render( {}} />) const filled = () => Array.from(container.querySelectorAll('span > span')).filter(o => (o as HTMLElement).style.width).length diff --git a/client/src/components/shared/StarRating.tsx b/client/src/components/shared/StarRating.tsx index a693c386c..d3117ecf6 100644 --- a/client/src/components/shared/StarRating.tsx +++ b/client/src/components/shared/StarRating.tsx @@ -29,7 +29,7 @@ export function Stars({ value, size = 15, onRate, myRating = null, ariaLabel }: role={onRate ? 'radiogroup' : undefined} aria-label={ariaLabel} style={{ display: 'inline-flex', gap: 2, cursor: onRate ? 'pointer' : 'default' }} - onMouseLeave={() => setHover(null)} + onPointerLeave={() => setHover(null)} > {[1, 2, 3, 4, 5].map(i => { // Fractional fill for averages: full below, empty above, clipped between. @@ -52,7 +52,11 @@ export function Stars({ value, size = 15, onRate, myRating = null, ariaLabel }: role="radio" aria-checked={myRating === i} aria-label={`${i}`} - onMouseEnter={() => setHover(i)} + // A preview for a pointer that hovers (a mouse, a trackpad, a pen held + // above the screen). A finger has no hover: the enter it fires on a + // tap has no leave after it, so the preview stuck and an unrated + // place showed five full stars beside "Not rated yet". + onPointerEnter={e => { if (e.pointerType !== 'touch') setHover(i) }} onFocus={() => setHover(i)} onBlur={() => setHover(null)} onClick={() => onRate(myRating === i ? null : i)} diff --git a/client/src/mobile/screens/dashboard/MNewTripSheet.tsx b/client/src/mobile/screens/dashboard/MNewTripSheet.tsx index e58fc7bd1..af1261743 100644 --- a/client/src/mobile/screens/dashboard/MNewTripSheet.tsx +++ b/client/src/mobile/screens/dashboard/MNewTripSheet.tsx @@ -5,14 +5,13 @@ import { useCanDo } from '../../../store/permissionsStore' import { useSettingsStore } from '../../../store/settingsStore' import { useAuthStore } from '../../../store/authStore' import MChip from '../../components/MChip' -import { CustomDatePicker } from '../../../components/shared/CustomDateTimePicker' import CustomSelect from '../../../components/shared/CustomSelect' import { currenciesWith, SYMBOLS } from '../../../components/Budget/BudgetPanel.constants' import type { DashboardTrip } from '../../../pages/dashboard/dashboardModel' import { MAX_TRIP_DAYS, type Trip, type TripCreateRequest } from '@trek/shared' import MSheet from '../../components/MSheet' -import { FIELD_AREA_CLS } from '../trip/sheets/PlSheetChrome' -import { Eyebrow, SHEET_FIELD, SHEET_PRIMARY, SHEET_ROUND_PRIMARY, SHEET_SECONDARY, SheetSection, TileHeader } from '../trip/sheets/MTripSheetUi' +import { Eyebrow, FIELD_AREA_CLS, FIELD_CLS, FormSheetBody, FormSheetFooter, FormSheetHeader, SheetDateField } from '../trip/sheets/PlSheetChrome' +import { SHEET_FIELD, SHEET_ROUND_PRIMARY, SHEET_SECONDARY } from '../trip/sheets/MTripSheetUi' import MDayImpactList from '../../components/MDayImpactList' import MConfirmSheet from '../settings/MConfirmSheet' import { useTripRangeGuard, type RangeRemoval } from '../../../hooks/useTripRangeGuard' @@ -31,9 +30,6 @@ interface MNewTripSheetProps { onArchive?: () => void } -/** A date picker in the sheets' round field. */ -const DATE_FIELD = 'flex h-[42px] items-center rounded-full border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-[14px]' - /** The reminder presets the desktop form offers; anything else is a custom number of days. */ const REMINDER_PRESETS = [0, 1, 3, 9] @@ -151,108 +147,100 @@ export default function MNewTripSheet({ open, trip: tripProp, onClose, onSave, o return ( <> - -
- } - title={isEditing ? t('dashboard.editTrip') : t('dashboard.createTrip')} - sub={isEditing ? trip?.title : undefined} - onClose={onClose} - closeLabel={t('common.cancel')} - /> -
+ + -
+ {/* Flat fields under their labels, as in every other form sheet: no + cards inside the card. */} + {error && ( -
+
{error}
)} - - { if (canEditTrip) { setError(''); setTitle(e.target.value) } }} - readOnly={!canEditTrip} - placeholder={t('dashboard.tripTitlePlaceholder')} - aria-label={t('dashboard.tripTitle')} - className={`${SHEET_FIELD} w-full flex-none font-semibold`} - /> - {t('dashboard.tripDescription')} -