feat: nested eml quick view

This commit is contained in:
rustmailer
2026-04-22 09:40:52 +08:00
parent c3a12eafb2
commit 5b884125f7
25 changed files with 109 additions and 37 deletions
@@ -21,7 +21,7 @@ import React from 'react'
import { SortingState } from '@tanstack/react-table'
import { AttachmentModel } from '@/api/attachment/api'
export type AttachmentDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'update-tags' | 'restore' | 'delete-mailbox'
export type AttachmentDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'update-tags' | 'restore' | 'delete-mailbox' | 'nested-eml'
interface AttachmentContextType {
open: AttachmentDialogType | null
+7
View File
@@ -32,6 +32,7 @@ import { AttachmentModel } from '@/api/attachment/api';
import { MailDisplayDrawer } from './mail-display-dialog';
import { EnvelopeDeleteDialog } from './delete-dialog';
import { RestoreMessageDialog } from './restore-message-dialog';
import { NestedEmailDialog } from './nested-email-dialog';
export default function AttachmentSearch() {
const { t } = useTranslation()
@@ -147,6 +148,12 @@ export default function AttachmentSearch() {
open={open === 'restore'}
onOpenChange={() => setOpen('restore')}
/>
<NestedEmailDialog
key="nested-eml-attachment-dialog"
open={open === 'nested-eml'}
onOpenChange={() => setOpen('nested-eml')}
/>
</AttachmentProvider>
</Main>
</>
@@ -165,7 +165,7 @@ export function AttachmentListTable({
accessorKey: "name",
header: t('attachment.name'),
cell: ({ row }) => {
const { name, content_type } = row.original;
const { name, content_type, is_message } = row.original;
const safeName = name ?? "n/a";
const shortContentType = content_type
@@ -179,9 +179,30 @@ export function AttachmentListTable({
contentType={content_type ?? ""}
className="h-4 w-4 mt-0.5"
/>
<LongText className='text-xs font-medium max-w-[320px] text-foreground/90'>
{is_message && <div className="group relative flex items-center w-full min-w-0 h-full px-2 overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
<span className="flex items-center">
<button
type="button"
title={t('attachment.viewEmbeddedEmail')}
onClick={(e) => {
e.stopPropagation();
setCurrentAttachment(row.original);
setOpen("nested-eml");
}}
className="hover:text-primary hover:underline transition-colors truncate"
>
<LongText>{safeName}</LongText>
</button>
</span>
</div>
</div>}
{!is_message && <LongText className='text-xs font-medium max-w-[320px] text-foreground/90'>
{safeName}
</LongText>
</LongText>}
</div>
<div className="flex items-center gap-1 ml-6.5 mt-1">
<span className="text-[10px] text-muted-foreground font-mono bg-muted px-1 py-0.5 rounded-sm">
@@ -278,7 +299,7 @@ export function AttachmentListTable({
if (isLoading) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 8 }).map((_, i) => (
{Array.from({ length: 30 }).map((_, i) => (
<div key={i} className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="h-3 w-3" />
<Skeleton className="h-3 w-3 rounded-full" />
@@ -27,6 +27,7 @@ import { useQuery } from '@tanstack/react-query';
import { Download, Loader, Mail } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getFileConfig } from './mail-message-view';
import { useAttachmentContext } from './context';
const MessageHeader = ({
envelope,
@@ -35,7 +36,7 @@ const MessageHeader = ({
}: {
envelope: EmailEnvelope,
attachments?: AttachmentInfo[],
onDownload: (nested_content_hash: string) => void
onDownload: (nested_content_hash: string, fileName: string) => void
}) => {
const { t } = useTranslation();
const displayAttachments = attachments || [];
@@ -118,7 +119,7 @@ const MessageHeader = ({
<Tooltip key={i}>
<TooltipTrigger asChild>
<button
onClick={() => onDownload(att.content_hash)}
onClick={() => onDownload(att.content_hash, att.filename)}
className="group flex items-center gap-2 px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg hover:bg-blue-50 hover:border-blue-200 transition-all text-slate-600 hover:text-blue-700"
>
<span className={`${color} p-0.5 rounded`}>{icon}</span>
@@ -144,12 +145,13 @@ const MessageHeader = ({
export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, fileName, content_hash }: any) {
export function NestedEmailDialog({ open, onOpenChange }: any) {
const { currentAttachment } = useAttachmentContext()
const { data, isLoading } = useQuery({
queryKey: ['nested-message', accountId, envelopeId, content_hash],
queryFn: () => load_nested_message(accountId, envelopeId, content_hash),
enabled: open && !!content_hash,
queryKey: ['nested-message', currentAttachment?.account_id!, currentAttachment?.envelope_id!, currentAttachment?.content_hash!],
queryFn: () => load_nested_message(currentAttachment?.account_id!, currentAttachment?.envelope_id!, currentAttachment?.content_hash!),
enabled: open && !!currentAttachment,
});
return (
@@ -158,7 +160,7 @@ export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, f
<div className="text-white px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium truncate max-w-[400px] opacity-90">{fileName}</span>
<span className="text-sm font-medium truncate max-w-[400px] opacity-90">{currentAttachment?.name}</span>
</div>
</div>
@@ -170,7 +172,13 @@ export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, f
<MessageHeader
envelope={data.envelope}
attachments={data.attachments}
onDownload={(nested_content_hash) => download_nested_attachment(accountId, envelopeId, content_hash, nested_content_hash)}
onDownload={(nested_content_hash, fileName) => download_nested_attachment(
currentAttachment?.account_id!,
currentAttachment?.envelope_id!,
currentAttachment?.content_hash!,
nested_content_hash,
fileName
)}
/>
<div className="mt-8 pt-8 border-t border-slate-100">