fix(bookings): keep 'From mail' out of the card and list head

Behind the title stood the review mark, the 'From mail' pill and the two
actions, and the title was cut short. Where a booking came from is looked
up in its detail dialog, so the pill stays there only. The AirTrail note
sat above the mail pill; it is back above AirTrailPill.
This commit is contained in:
Maurice
2026-11-13 21:21:49 +01:00
parent dfbe23e5b8
commit a2cc7af695
5 changed files with 25 additions and 23 deletions
@@ -15,7 +15,7 @@ import { formatMoney } from '../../../utils/formatters'
import { parseMeta, displayTitle, type CostTotal } from './bookingsModel'
import type { BookingFacts } from './bookingFacts'
import {
AirTrailPill, MailPill, BOX, Block, Field, FileRows, ReviewPill, RoundAction, RouteBox, StatusDot, TravelerChips, TypeChip, WhenFields,
AirTrailPill, BOX, Block, Field, FileRows, ReviewPill, RoundAction, RouteBox, StatusDot, TravelerChips, TypeChip, WhenFields,
fs, toneOf, toneTint,
} from './bookingParts'
import { TransitLegs, transitSummary } from './transitParts'
@@ -66,7 +66,6 @@ export default function BookingCard(p: BookingCardProps) {
{!!r.needs_review && <ReviewPill />}
</span>
<AirTrailPill r={r} />
<MailPill r={r} />
{p.canEdit && <RoundAction label={t('common.edit')} onClick={p.onEdit}><Pencil size={12} strokeWidth={2} /></RoundAction>}
{p.canEdit && <RoundAction label={t('common.delete')} onClick={p.onDelete} danger><Trash2 size={12} strokeWidth={2} /></RoundAction>}
</div>
@@ -154,7 +154,7 @@ export default function BookingDetailDialog(p: BookingDetailDialogProps) {
)}
{!!r.needs_review && <ReviewPill pill onConfirm={p.canEdit ? () => void confirmReview() : undefined} />}
<AirTrailPill r={r} pill />
<MailPill r={r} pill />
<MailPill r={r} />
{r.confirmation_number && (
// The copy button rides in the pill's padding (-my-1), so this pill is as tall as its neighbours.
<span className={`${PILL} gap-1 pe-1`}>
@@ -7,7 +7,7 @@ import { formatMoney } from '../../../utils/formatters'
import { TransitLegChips } from '../transitDisplay'
import { displayTitle, parseMeta, type BookingGroup, type CostTotal } from './bookingsModel'
import type { BookingFacts } from './bookingFacts'
import { AirTrailPill, CountPill, MailPill, EYEBROW, ReviewPill, RoundAction, StatusDot, TravelerStack, TypeTile, fs } from './bookingParts'
import { AirTrailPill, CountPill, EYEBROW, ReviewPill, RoundAction, StatusDot, TravelerStack, TypeTile, fs } from './bookingParts'
export interface BookingsListProps {
groups: BookingGroup[]
@@ -102,7 +102,6 @@ function Row({ r, p, facts, files, costs, t, locale }: {
<span className="truncate font-semibold text-content" style={fs(13.5, 'body')}>{displayTitle(r)}</span>
{!!r.needs_review && <ReviewPill />}
<AirTrailPill r={r} />
<MailPill r={r} />
</div>
{transit ? (
<div className="mt-1"><TransitLegChips legs={Array.isArray(meta.transit?.legs) ? meta.transit.legs : []} size="sm" t={t} /></div>
@@ -1,11 +1,11 @@
// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-005
// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-006
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { buildTripFile } from '../../../../tests/helpers/factories';
import { buildReservation, buildTripFile } from '../../../../tests/helpers/factories';
import { downloadFile, openFile } from '../../../utils/fileDownload';
import type { BookingFacts } from './bookingFacts';
import { FileRows, ReviewPill, WhenFields } from './bookingParts';
import { FileRows, MailPill, ReviewPill, WhenFields } from './bookingParts';
vi.mock('../../../utils/fileDownload', () => ({
openFile: vi.fn(() => Promise.resolve()),
@@ -97,4 +97,11 @@ describe('FileRows', () => {
fireEvent.click(screen.getByRole('button'));
expect(onConfirm).toHaveBeenCalledOnce();
});
it('FE-PLANNER-BKPARTS-006: the mail pill names a booking from a forwarded mail, and nothing else', () => {
const { rerender } = render(<MailPill r={buildReservation({ external_source: 'mail' })} />);
expect(screen.getByText('From mail')).toBeInTheDocument();
rerender(<MailPill r={buildReservation({ external_source: 'import' })} />);
expect(screen.queryByText('From mail')).toBeNull();
});
});
@@ -205,30 +205,27 @@ export function ReviewPill({ pill = false, onConfirm }: { pill?: boolean; onConf
}
/**
* The AirTrail link of a booking. A multi-leg import is detached from sync by
* design (#1535), which is not the same as a flight removed upstream (#1646).
* Added from a forwarded booking mail (mail-ingest), in the detail dialog's
* head: where a person looks up where a booking came from. The card and the
* list leave it out, their head is the booking itself.
*/
/** Added from a forwarded booking mail (mail-ingest): said quietly, so the person knows where it came from. */
export function MailPill({ r, pill = false }: { r: Reservation; pill?: boolean }) {
export function MailPill({ r }: { r: Reservation }) {
const { t } = useTranslation()
if (r.external_source !== 'mail') return null
return (
<Tooltip label={t('mailInbox.fromMailHint')}>
{pill ? (
<span className={PILL} style={fs(12, 'body')}>
<Mail size={13} strokeWidth={2.2} className="text-content-faint" />
{t('mailInbox.fromMail')}
</span>
) : (
<span className="inline-flex flex-none items-center gap-1 rounded-full bg-surface-tertiary px-2 py-[2px] font-geist font-bold uppercase tracking-[.03em] text-content-faint" style={fs(9)}>
<Mail size={10} />
{t('mailInbox.fromMail')}
</span>
)}
<span className={PILL} style={fs(12, 'body')}>
<Mail size={13} strokeWidth={2.2} className="text-content-faint" />
{t('mailInbox.fromMail')}
</span>
</Tooltip>
)
}
/**
* The AirTrail link of a booking. A multi-leg import is detached from sync by
* design (#1535), which is not the same as a flight removed upstream (#1646).
*/
export function AirTrailPill({ r, pill = false }: { r: Reservation; pill?: boolean }) {
const { t } = useTranslation()
if (r.external_source !== 'airtrail') return null