✨(print) add Print button in messages context menu (#518)

This commit is contained in:
Sylvain Zimmer
2026-02-04 00:38:50 +01:00
committed by GitHub
parent 796a4b7a35
commit 6fe2db7be2
5 changed files with 87 additions and 0 deletions
@@ -349,6 +349,7 @@
"Please enter a valid email address.": "Please enter a valid email address.",
"Prefix can only contain letters, numbers, dots, underscores and hyphens.": "Prefix can only contain letters, numbers, dots, underscores and hyphens.",
"Prefix is required.": "Prefix is required.",
"Print": "Print",
"Read": "Read",
"Read state": "Read state",
"Recurring": "Recurring",
@@ -349,6 +349,7 @@
"Please enter a valid email address.": "Veuillez saisir une adresse email valide.",
"Prefix can only contain letters, numbers, dots, underscores and hyphens.": "Le préfixe ne peut contenir que des lettres, chiffres, points, tirets bas et tirets.",
"Prefix is required.": "Le préfixe est requis.",
"Print": "Imprimer",
"Read": "Lu",
"Read state": "État de lecture",
"Recurring": "Récurrent",
@@ -274,6 +274,7 @@
"Please enter a valid email address.": "Vul een correct email-adres in.",
"Prefix can only contain letters, numbers, dots, underscores and hyphens.": "Voorvoegsel kan alleen letters, cijfers, punten, onderstrepingstekens en koppeltekens bevatten.",
"Prefix is required.": "Voorvoegsel is vereist.",
"Print": "Afdrukken",
"Read": "Lees",
"Read state": "Lees status",
"Redirection": "Omleiding",
@@ -5,6 +5,7 @@ import { DropdownMenu, Icon, IconType } from "@gouvfr-lasuite/ui-kit";
import { getMessagesEmlRetrieveUrl } from "@/features/api/gen/messages/messages";
import { getRequestUrl } from "@/features/api/utils";
import { useMailboxContext } from "@/features/providers/mailbox";
import usePrint from "@/features/message/use-print";
import useRead from "@/features/message/use-read";
import useTrash from "@/features/message/use-trash";
import { ThreadMessageActionsProps } from "./types";
@@ -25,6 +26,7 @@ const ThreadMessageActions = ({
const { unselectThread, selectedThread, messages } = useMailboxContext();
const { markAsUnread, markAsRead } = useRead();
const { markAsTrashed } = useTrash();
const { print } = usePrint();
const hasSiblingMessages = useMemo(() => {
if (!selectedThread) return false;
@@ -80,6 +82,11 @@ const ThreadMessageActions = ({
icon: <Icon type={IconType.FILLED} name="mark_email_unread" />,
callback: () => toggleReadStateFrom(true)
}]),
{
label: t('Print'),
icon: <Icon type={IconType.FILLED} name="print" />,
callback: () => print(message)
},
{
label: t('Download raw email'),
icon: <Icon type={IconType.FILLED} name="download" />,
@@ -0,0 +1,77 @@
import { useCallback } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { useTranslation } from "react-i18next";
import { Message } from "@/features/api/gen/models";
const formatContact = (c: { name?: string | null; email: string }) =>
c.name ? `${c.name} <${c.email}>` : c.email;
/**
* Hook to print a message in a new browser window
*/
const usePrint = () => {
const { t, i18n } = useTranslation();
const print = useCallback((message: Message) => {
const iframe = document
.querySelector(`#thread-message-${message.id} .thread-message__body`) as HTMLIFrameElement | null;
const bodyHtml = iframe?.contentDocument?.body?.innerHTML;
if (!bodyHtml) return;
const date = new Date(message.sent_at ?? message.created_at);
const formattedDate = date.toLocaleString(i18n.resolvedLanguage, {
dateStyle: 'full',
timeStyle: 'short',
});
const headers = [
{ label: t('From'), value: formatContact(message.sender) },
{ label: t('To'), value: message.to.map((r) => formatContact(r.contact)).join(', ') },
...(message.cc.length ? [{ label: t('Cc'), value: message.cc.map((r) => formatContact(r.contact)).join(', ') }] : []),
...(message.bcc.length ? [{ label: t('Bcc'), value: message.bcc.map((r) => formatContact(r.contact)).join(', ') }] : []),
{ label: t('Date'), value: formattedDate },
{ label: t('Subject'), value: message.subject ?? '' },
];
const html = '<!DOCTYPE html>' + renderToStaticMarkup(
<html>
<head>
<meta charSet="utf-8" />
<title>{message.subject ?? ''}</title>
<style>{`
body { font-family: system-ui, -apple-system, sans-serif; margin: 2em; color: #000; }
table { font-size: 14px; border-collapse: collapse; }
hr { border: none; border-top: 1px solid #ccc; margin: 16px 0; }
.body { font-size: 14px; }
.body img { max-width: 100%; }
@media print { body { margin: 0; } }
`}</style>
</head>
<body>
<table>
{headers.map((h, i) => (
<tr key={i}>
<td style={{ fontWeight: 'bold', padding: '2px 12px 2px 0', whiteSpace: 'nowrap', verticalAlign: 'top' }}>{h.label}</td>
<td style={{ padding: '2px 0' }}>{h.value}</td>
</tr>
))}
</table>
<hr />
<div className="body" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
</body>
</html>
);
const printWindow = window.open('');
if (!printWindow) return;
printWindow.document.write(html);
printWindow.document.close();
printWindow.addEventListener('afterprint', () => printWindow.close());
printWindow.onload = () => printWindow.print();
}, [t, i18n.resolvedLanguage]);
return { print };
};
export default usePrint;