mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
fix: print an invitation with its date, time, place and organiser
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(<>
|
||||
<EmailViewer email={htmlMail()} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" />
|
||||
<InvitationPrintSync emailId="e1" print={card} />
|
||||
</>);
|
||||
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(<EmailViewer email={htmlMail()} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" />);
|
||||
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(<EmailViewer email={htmlMail()} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" />);
|
||||
expect(printCopy()).toBeNull();
|
||||
|
||||
@@ -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 && (
|
||||
<InvitationPrintSync
|
||||
emailId={email.id}
|
||||
print={{
|
||||
heading: bannerTitle,
|
||||
rows: ([
|
||||
[t('change_title'), summary.title ?? ''],
|
||||
[t('change_time'), summary.start ? formatSchedule(summary) : ''],
|
||||
[t('change_location'), summary.location ?? ''],
|
||||
[t('organizer_label'), summary.organizer ? (summary.organizerEmail && summary.organizerEmail !== summary.organizer ? `${summary.organizer} <${summary.organizerEmail}>` : summary.organizer) : ''],
|
||||
] as Array<[string, string]>).filter(([, value]) => value),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Avatar-style icon */}
|
||||
<div className={cn(
|
||||
"w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 shadow-sm",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ReaderAttachments, type AttachmentDragProps } from "./reader-attachment
|
||||
import { editAccountOf, useEditAttachment } from "@/hooks/use-edit-attachment";
|
||||
import { ReaderViewDialog } from "./reader-view-dialog";
|
||||
import { MessagePrint, type MessagePrintFields } from "./message-print";
|
||||
import { useInvitationPrint } from "@/lib/print/invitation-print";
|
||||
import { usePrintListener, usePrinting } from "@/components/print/print-root";
|
||||
import { ReadingMask } from "./reading-mask";
|
||||
import { collectReferencedCids, isEmbeddedInBody, isReportPart, normalizeCid, rewriteCidReferences } from "@/lib/attachment-visibility";
|
||||
@@ -3798,6 +3799,7 @@ export function EmailViewer({
|
||||
? effectiveEmailContent.html.replace(/<details>/g, '<details open>')
|
||||
: 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;
|
||||
|
||||
@@ -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 <style>: a re-sanitized HTML body, or the escaped plain-text one. */
|
||||
bodyHtml: string;
|
||||
plain: boolean;
|
||||
/** An invitation's card: what, when, where and who, as the reader shows it. */
|
||||
invitation?: InvitationPrint | null;
|
||||
}
|
||||
|
||||
// The reader's image rule: cap only images that set no max-width of their own.
|
||||
@@ -66,6 +69,19 @@ export function MessagePrint({ fields }: { fields: MessagePrintFields }) {
|
||||
))}
|
||||
</dl>
|
||||
</header>
|
||||
{fields.invitation && fields.invitation.rows.length > 0 && (
|
||||
<section className="pr-invitation" data-message-print-invitation="">
|
||||
<h2 className="pr-invitation-heading">{fields.invitation.heading}</h2>
|
||||
<dl className="pr-message-meta">
|
||||
{fields.invitation.rows.map(([label, value]) => (
|
||||
<div key={label}>
|
||||
<dt>{label}:</dt>
|
||||
<dd dir="auto">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
)}
|
||||
<div className="pr-message-body" data-message-print-body="" ref={fillBody} />
|
||||
</article>
|
||||
</PrintRoot>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* What an invitation's card says, for the printout of its message. The card works its
|
||||
* details out itself (components/email/calendar-invitation-banner.tsx); the print copy
|
||||
* (components/email/message-print.tsx) only shows the header and the body, so without
|
||||
* this a printed invitation lost when and where, the part people print it for.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface InvitationPrint {
|
||||
heading: string;
|
||||
rows: Array<[label: string, value: string]>;
|
||||
}
|
||||
|
||||
const useInvitations = create<Record<string, InvitationPrint>>(() => ({}));
|
||||
|
||||
export function setInvitationPrint(emailId: string, print: InvitationPrint | null): void {
|
||||
useInvitations.setState((state) => {
|
||||
const next = { ...state };
|
||||
if (print) next[emailId] = print;
|
||||
else delete next[emailId];
|
||||
return next;
|
||||
}, true);
|
||||
}
|
||||
|
||||
export function useInvitationPrint(emailId: string | undefined): InvitationPrint | null {
|
||||
return useInvitations((state) => (emailId ? state[emailId] ?? null : null));
|
||||
}
|
||||
|
||||
/** Rendered by the card while its details are known; takes them away again when it goes. */
|
||||
export function InvitationPrintSync({ emailId, print }: { emailId: string; print: InvitationPrint }): null {
|
||||
const key = JSON.stringify(print);
|
||||
useEffect(() => {
|
||||
setInvitationPrint(emailId, JSON.parse(key) as InvitationPrint);
|
||||
return () => setInvitationPrint(emailId, null);
|
||||
}, [emailId, key]);
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user