fix(mail): remove duplicate toolbar, fix iframe height, move attachments into scrollable body
- Remove duplicate action bar from MailDetail (actions are in global plugin toolbar) - Change iframe sandbox from "" to "allow-same-origin" so height auto-resize works - Move attachments inside the scrollable body container so they are visible - Remove unused imports (clsx, Badge) and unused destructured props
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* Mail detail reading pane.
|
||||
* Shows mail headers, sanitized HTML body, attachments, reply/forward/create-event actions.
|
||||
* Shows mail headers, sanitized HTML body, and attachments.
|
||||
* Actions (reply/forward/delete/etc.) are in the global plugin toolbar.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
import type { Mail, MailAttachment } from '@/api/mail';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
|
||||
export interface MailDetailProps {
|
||||
@@ -44,15 +43,8 @@ function formatBytes(bytes: number): string {
|
||||
export function MailDetail({
|
||||
mail,
|
||||
loading,
|
||||
onReply,
|
||||
onForward,
|
||||
onCreateEvent,
|
||||
onToggleFlag,
|
||||
onDownloadAttachment,
|
||||
downloadingAttachmentId,
|
||||
onDelete,
|
||||
onMove,
|
||||
onEditDraft,
|
||||
}: MailDetailProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -102,90 +94,6 @@ export function MailDetail({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full" data-testid="mail-detail">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-1 sm:gap-2 px-3 py-2 md:px-4 md:py-3 border-b border-secondary-200 overflow-x-auto" data-testid="mail-detail-toolbar">
|
||||
{mail.is_draft && onEditDraft ? (
|
||||
<Button variant="secondary" size="sm" onClick={() => onEditDraft(mail)} icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
}>
|
||||
{t('mail.editDraft')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="secondary" size="sm" onClick={() => onReply(mail)} icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
}>
|
||||
{t('mail.reply')}
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => onForward(mail)} icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10H11a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6" />
|
||||
</svg>
|
||||
}>
|
||||
{t('mail.forward')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleFlag(mail)}
|
||||
aria-label={t('mail.toggleFlag')}
|
||||
icon={
|
||||
<svg className={clsx('w-4 h-4', mail.is_flagged ? 'text-warning-500' : 'text-secondary-400')} fill={mail.is_flagged ? 'currentColor' : 'none'} viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
{mail.is_flagged ? t('mail.unflag') : t('mail.flag')}
|
||||
</Button>
|
||||
{onMove && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMove(mail)}
|
||||
icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
{t('mail.move')}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onCreateEvent(mail)}
|
||||
icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
{t('mail.createEvent')}
|
||||
</Button>
|
||||
{onDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(mail)}
|
||||
icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
{t('mail.delete')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
<div className="px-3 py-2 md:px-4 md:py-3 border-b border-secondary-200" data-testid="mail-detail-headers">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 sm:gap-4">
|
||||
@@ -222,51 +130,53 @@ export function MailDetail({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-3 py-3 md:px-4 md:py-4" data-testid="mail-detail-body">
|
||||
{safeHtml ? (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={safeHtml}
|
||||
sandbox=""
|
||||
onLoad={handleIframeLoad}
|
||||
className="w-full border-0"
|
||||
data-testid="mail-html-body"
|
||||
title={t('mail.subject')}
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-sm text-secondary-800" data-testid="mail-text-body">{mail.body_text}</pre>
|
||||
{/* Body + Attachments — scrollable together */}
|
||||
<div className="flex-1 overflow-y-auto" data-testid="mail-detail-body">
|
||||
<div className="px-3 py-3 md:px-4 md:py-4">
|
||||
{safeHtml ? (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={safeHtml}
|
||||
sandbox="allow-same-origin"
|
||||
onLoad={handleIframeLoad}
|
||||
className="w-full border-0"
|
||||
data-testid="mail-html-body"
|
||||
title={t('mail.subject')}
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-sm text-secondary-800" data-testid="mail-text-body">{mail.body_text}</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
{mail.attachments && mail.attachments.length > 0 && (
|
||||
<div className="px-3 py-3 md:px-4 md:py-4 border-t border-secondary-200" data-testid="mail-detail-attachments">
|
||||
<h3 className="text-sm font-semibold text-secondary-700 mb-2">{t('mail.attachments')}</h3>
|
||||
<ul className="space-y-1 md:space-y-2">
|
||||
{mail.attachments.map((att) => (
|
||||
<li key={att.id} className="flex items-center gap-3 p-2 rounded-md hover:bg-secondary-50 min-h-touch">
|
||||
<svg className="w-5 h-5 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-secondary-800 truncate">{att.filename}</p>
|
||||
<p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onDownloadAttachment(mail.id, att)}
|
||||
isLoading={downloadingAttachmentId === att.id}
|
||||
data-testid={`download-attachment-${att.id}`}
|
||||
>
|
||||
{t('mail.download')}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
{mail.attachments && mail.attachments.length > 0 && (
|
||||
<div className="px-3 py-2 md:px-4 md:py-3 border-t border-secondary-200" data-testid="mail-detail-attachments">
|
||||
<h3 className="text-sm font-semibold text-secondary-700 mb-2">{t('mail.attachments')}</h3>
|
||||
<ul className="space-y-1 md:space-y-2">
|
||||
{mail.attachments.map((att) => (
|
||||
<li key={att.id} className="flex items-center gap-3 p-2 rounded-md hover:bg-secondary-50 min-h-touch">
|
||||
<svg className="w-5 h-5 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-secondary-800 truncate">{att.filename}</p>
|
||||
<p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onDownloadAttachment(mail.id, att)}
|
||||
isLoading={downloadingAttachmentId === att.id}
|
||||
data-testid={`download-attachment-${att.id}`}
|
||||
>
|
||||
{t('mail.download')}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user