From 2fcb7cd544eafa39474d05704c9671f208b52770 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 07:05:31 +0100 Subject: [PATCH] fix: print an invitation with its date, time, place and organiser --- app/print.css | 21 ++++++++++ .../__tests__/viewer-print-copy.test.tsx | 20 ++++++++++ .../email/calendar-invitation-banner.tsx | 16 ++++++++ components/email/email-viewer.tsx | 4 ++ components/email/message-print.tsx | 16 ++++++++ lib/print/invitation-print.ts | 39 +++++++++++++++++++ 6 files changed, 116 insertions(+) create mode 100644 lib/print/invitation-print.ts diff --git a/app/print.css b/app/print.css index 9cf992ad5..2a754311b 100644 --- a/app/print.css +++ b/app/print.css @@ -738,3 +738,24 @@ margin: 0; overflow-wrap: anywhere; } + +/* An invitation's card: when and where, in a box above the body. */ +.pr-invitation { + margin: 4mm 0 0; + padding: 2.5mm 3mm; + border: 0.3mm solid var(--pr-rule, #bbb); + border-radius: 1.5mm; + break-inside: avoid; +} + +.pr-invitation-heading { + margin: 0; + font-size: 8pt; + font-weight: 600; + color: var(--pr-muted); +} + +.pr-invitation .pr-message-meta { + font-size: 10pt; + color: var(--pr-ink); +} diff --git a/components/email/__tests__/viewer-print-copy.test.tsx b/components/email/__tests__/viewer-print-copy.test.tsx index 8df96af39..d5e020f5d 100644 --- a/components/email/__tests__/viewer-print-copy.test.tsx +++ b/components/email/__tests__/viewer-print-copy.test.tsx @@ -61,6 +61,26 @@ function printCopy(): HTMLElement | null { } describe('Ctrl+P on an open message', () => { + it('prints an invitation with its card: when, where and who', async () => { + const { InvitationPrintSync } = await import('@/lib/print/invitation-print'); + const card = { heading: 'Calendar Invitation', rows: [['Time', 'Thu 5 Nov, 13:00 to 14:00'], ['Location', 'Room 4'], ['Organized by', 'Anna Becker']] as Array<[string, string]> }; + const view = render(<> + + + ); + act(() => { window.dispatchEvent(new Event('beforeprint')); }); + const box = printCopy()!.querySelector('[data-message-print-invitation]')!; + expect(box.textContent).toContain('Calendar Invitation'); + expect(box.textContent).toContain('Thu 5 Nov, 13:00 to 14:00'); + expect(box.textContent).toContain('Room 4'); + expect(box.textContent).toContain('Anna Becker'); + act(() => { window.dispatchEvent(new Event('afterprint')); }); + // The card gone (another message opened): the next printout has no box. + view.rerender(); + act(() => { window.dispatchEvent(new Event('beforeprint')); }); + expect(printCopy()!.querySelector('[data-message-print-invitation]')).toBeNull(); + }, 30_000); + it('prints the message alone, next to the app', () => { render(); expect(printCopy()).toBeNull(); diff --git a/components/email/calendar-invitation-banner.tsx b/components/email/calendar-invitation-banner.tsx index 5a14eb71d..0f29cd8b0 100644 --- a/components/email/calendar-invitation-banner.tsx +++ b/components/email/calendar-invitation-banner.tsx @@ -20,6 +20,7 @@ import { ChevronUp, } from '@/components/icons'; import { useTranslations, useFormatter } from 'next-intl'; +import { InvitationPrintSync } from '@/lib/print/invitation-print'; import { useRouter } from '@/i18n/navigation'; import { isDocumentRTL } from '@/i18n/direction'; import { useAuthStore } from '@/stores/auth-store'; @@ -939,6 +940,21 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp tabIndex={isCollapsed ? 0 : undefined} aria-expanded={isCollapsed ? false : undefined} > + {/* When and where, for the printout of this message: its print copy has no card. */} + {summary && ( + ` : summary.organizer) : ''], + ] as Array<[string, string]>).filter(([, value]) => value), + }} + /> + )} {/* Avatar-style icon */}
/g, '
') : sanitizeEmailHtml(effectiveEmailContent.html), plain, + invitation: invitationPrint, }; }; @@ -3895,6 +3897,8 @@ ${printWatermark ? printWatermark.html : ''} () => printing && !!viewerRootEl && isShownOnScreen(viewerRootEl), [printing, viewerRootEl], ); + // An invitation's when and where, from its card. + const invitationPrint = useInvitationPrint(email?.id); const printCopyFields = printShown && !devices.noPrint && !isLoading && !!email?.bodyValues ? messagePrintFields() : null; diff --git a/components/email/message-print.tsx b/components/email/message-print.tsx index 53cfd7c44..43aff624d 100644 --- a/components/email/message-print.tsx +++ b/components/email/message-print.tsx @@ -3,6 +3,7 @@ import { useCallback } from "react"; import { useTranslations } from "next-intl"; import { PrintRoot } from "@/components/print/print-root"; +import type { InvitationPrint } from "@/lib/print/invitation-print"; export interface MessagePrintFields { subject: string; @@ -13,6 +14,8 @@ export interface MessagePrintFields { /** Sanitized body HTML without