fix: implement draft editing functionality across email components and add localization keys
This commit is contained in:
@@ -468,6 +468,33 @@ export default function Home() {
|
||||
if (isMobile) setActiveView('viewer');
|
||||
};
|
||||
|
||||
const handleEditDraft = (email?: Email) => {
|
||||
const draft = email || selectedEmail;
|
||||
if (!draft) return;
|
||||
const bodyText = draft.bodyValues
|
||||
? Object.values(draft.bodyValues).map(v => v.value).join('\n')
|
||||
: '';
|
||||
const htmlBody = draft.htmlBody?.[0]?.partId && draft.bodyValues?.[draft.htmlBody[0].partId]
|
||||
? draft.bodyValues[draft.htmlBody[0].partId].value
|
||||
: undefined;
|
||||
setPendingDraft({
|
||||
to: draft.to?.map(a => a.email).filter(Boolean).join(', ') || '',
|
||||
cc: draft.cc?.map(a => a.email).filter(Boolean).join(', ') || '',
|
||||
bcc: draft.bcc?.map(a => a.email).filter(Boolean).join(', ') || '',
|
||||
subject: draft.subject || '',
|
||||
body: htmlBody || bodyText,
|
||||
showCc: (draft.cc?.length || 0) > 0,
|
||||
showBcc: (draft.bcc?.length || 0) > 0,
|
||||
selectedIdentityId: null,
|
||||
subAddressTag: '',
|
||||
mode: 'compose',
|
||||
draftId: draft.id,
|
||||
});
|
||||
setComposerMode('compose');
|
||||
setShowComposer(true);
|
||||
if (isMobile) setActiveView('viewer');
|
||||
};
|
||||
|
||||
const handleReplyAll = () => {
|
||||
setComposerMode('replyAll');
|
||||
setShowComposer(true);
|
||||
@@ -1370,6 +1397,9 @@ export default function Home() {
|
||||
selectEmail(email);
|
||||
await handleUndoSpam();
|
||||
}}
|
||||
onEditDraft={(email) => {
|
||||
handleEditDraft(email);
|
||||
}}
|
||||
className="flex-1 min-h-0"
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
@@ -1543,6 +1573,7 @@ export default function Home() {
|
||||
onNavigateNext={handleNavigateNext}
|
||||
onNavigatePrev={handleNavigatePrev}
|
||||
onShowShortcuts={() => setShowShortcutsModal(true)}
|
||||
onEditDraft={handleEditDraft}
|
||||
currentUserEmail={client?.["username"]}
|
||||
currentUserName={client?.["username"]?.split("@")[0]}
|
||||
currentMailboxRole={mailboxes.find(m => m.id === selectedMailbox)?.role}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Folder,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
EditIcon,
|
||||
} from "lucide-react";
|
||||
import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
|
||||
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
||||
@@ -60,6 +61,7 @@ interface EmailContextMenuProps {
|
||||
onMoveToMailbox?: (mailboxId: string) => void;
|
||||
onMarkAsSpam?: () => void;
|
||||
onUndoSpam?: () => void;
|
||||
onEditDraft?: () => void;
|
||||
// Batch actions
|
||||
onBatchMarkAsRead?: (read: boolean) => void;
|
||||
onBatchDelete?: () => void;
|
||||
@@ -126,12 +128,14 @@ export function EmailContextMenu({
|
||||
onBatchMoveToMailbox,
|
||||
onBatchMarkAsSpam,
|
||||
onBatchUndoSpam,
|
||||
onEditDraft,
|
||||
}: EmailContextMenuProps) {
|
||||
const t = useTranslations("context_menu");
|
||||
const tColor = useTranslations("email_viewer.color_tag");
|
||||
const emailKeywords = useSettingsStore((state) => state.emailKeywords);
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const isStarred = email.keywords?.$flagged;
|
||||
const isDraft = email.keywords?.['$draft'] === true;
|
||||
const currentColor = getCurrentColor(email.keywords);
|
||||
const showBatchActions = isMultiSelect && selectedCount > 1;
|
||||
const isInJunkFolder = currentMailboxRole === 'junk';
|
||||
@@ -188,6 +192,18 @@ export function EmailContextMenu({
|
||||
</ContextMenuHeader>
|
||||
)}
|
||||
|
||||
{/* Edit Draft - only for single draft emails */}
|
||||
{!showBatchActions && isDraft && onEditDraft && (
|
||||
<>
|
||||
<ContextMenuItem
|
||||
icon={EditIcon}
|
||||
label={t("edit_draft")}
|
||||
onClick={() => handleAction(onEditDraft)}
|
||||
/>
|
||||
<ContextMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Single email actions - Reply, Reply All, Forward */}
|
||||
{!showBatchActions && (
|
||||
<>
|
||||
|
||||
@@ -37,6 +37,7 @@ interface EmailListProps {
|
||||
onMoveToMailbox?: (emailId: string, mailboxId: string) => void;
|
||||
onMarkAsSpam?: (email: Email) => void;
|
||||
onUndoSpam?: (email: Email) => void;
|
||||
onEditDraft?: (email: Email) => void;
|
||||
}
|
||||
|
||||
export function EmailList({
|
||||
@@ -57,6 +58,7 @@ export function EmailList({
|
||||
onMarkAsSpam,
|
||||
onUndoSpam,
|
||||
onMoveToMailbox,
|
||||
onEditDraft,
|
||||
}: EmailListProps) {
|
||||
const t = useTranslations('email_list');
|
||||
const { client } = useAuthStore();
|
||||
@@ -467,6 +469,7 @@ export function EmailList({
|
||||
onMoveToMailbox={(mailboxId) => onMoveToMailbox?.(contextMenu.data!.id, mailboxId)}
|
||||
onMarkAsSpam={() => onMarkAsSpam?.(contextMenu.data!)}
|
||||
onUndoSpam={() => onUndoSpam?.(contextMenu.data!)}
|
||||
onEditDraft={() => onEditDraft?.(contextMenu.data!)}
|
||||
onBatchMarkAsRead={(read) => client && batchMarkAsRead(client, read)}
|
||||
onBatchDelete={() => client && batchDelete(client)}
|
||||
onBatchMoveToMailbox={(mailboxId) => client && batchMoveToMailbox(client, mailboxId)}
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
Upload,
|
||||
Moon,
|
||||
HelpCircle,
|
||||
EditIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { Attachment as PostalMimeAttachment } from 'postal-mime';
|
||||
@@ -112,6 +113,7 @@ interface EmailViewerProps {
|
||||
onNavigateNext?: () => void;
|
||||
onNavigatePrev?: () => void;
|
||||
onShowShortcuts?: () => void;
|
||||
onEditDraft?: () => void;
|
||||
currentUserEmail?: string;
|
||||
currentUserName?: string;
|
||||
currentMailboxRole?: string;
|
||||
@@ -815,6 +817,7 @@ export function EmailViewer({
|
||||
onNavigateNext,
|
||||
onNavigatePrev,
|
||||
onShowShortcuts,
|
||||
onEditDraft,
|
||||
currentUserEmail,
|
||||
currentUserName,
|
||||
currentMailboxRole,
|
||||
@@ -840,6 +843,9 @@ export function EmailViewer({
|
||||
// Detect if current mailbox is Junk folder
|
||||
const isInJunkFolder = currentMailboxRole === 'junk';
|
||||
|
||||
// Detect if the email is a draft
|
||||
const isDraft = email?.keywords?.['$draft'] === true;
|
||||
|
||||
// Color options for email tags (from user-defined keyword settings)
|
||||
const colorOptions = emailKeywords.map((kw) => ({
|
||||
name: kw.label,
|
||||
@@ -2632,6 +2638,19 @@ export function EmailViewer({
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
{isDraft && onEditDraft && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onEditDraft}
|
||||
className="sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
||||
title={t('tooltips.edit_draft')}
|
||||
>
|
||||
<EditIcon className="w-4 h-4" />
|
||||
<span className="text-sm">{t('edit_draft')}</span>
|
||||
</Button>
|
||||
)}
|
||||
{!isDraft && (<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -2668,6 +2687,7 @@ export function EmailViewer({
|
||||
<Forward className="w-4 h-4" />
|
||||
{showToolbarLabels && <span className="hidden sm:inline text-sm">{t('forward')}</span>}
|
||||
</Button>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
{/* Right: Organize actions — order: archive, delete, move, star, tag, spam, read state, print, view source */}
|
||||
@@ -3988,6 +4008,29 @@ export function EmailViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Draft Banner */}
|
||||
{isDraft && (
|
||||
<div className="border-b border-border bg-amber-50 dark:bg-amber-950/30">
|
||||
<div className="max-w-4xl mx-auto px-6 py-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-400">
|
||||
<File className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{t('draft_banner')}</span>
|
||||
</div>
|
||||
{onEditDraft && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onEditDraft}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<EditIcon className="w-3.5 h-3.5" />
|
||||
{t('edit_draft')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SmimePassphraseDialog
|
||||
isOpen={smimeUnlockDialogOpen}
|
||||
onClose={() => {
|
||||
@@ -4077,8 +4120,8 @@ export function EmailViewer({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Reply Section */}
|
||||
<div className={cn(
|
||||
{/* Quick Reply Section - hidden for drafts */}
|
||||
{!isDraft && (<div className={cn(
|
||||
"mt-6 mx-6 mb-6 bg-background rounded-lg shadow-sm border transition-all",
|
||||
isQuickReplyFocused || quickReplyText ? "border-primary" : "border-border"
|
||||
)}>
|
||||
@@ -4172,7 +4215,7 @@ export function EmailViewer({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4240,6 +4283,17 @@ export function EmailViewer({
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span className="text-[10px] font-medium leading-tight">{t('previous')}</span>
|
||||
</button>
|
||||
{isDraft && onEditDraft ? (
|
||||
<button
|
||||
onClick={onEditDraft}
|
||||
className="flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] text-primary active:text-primary/80 transition-colors duration-150"
|
||||
aria-label={t('tooltips.edit_draft')}
|
||||
>
|
||||
<EditIcon className="w-5 h-5" />
|
||||
<span className="text-[10px] font-medium leading-tight">{t('edit_draft')}</span>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onReply?.()}
|
||||
className="flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] text-muted-foreground active:text-foreground transition-colors duration-150"
|
||||
@@ -4264,6 +4318,7 @@ export function EmailViewer({
|
||||
<Forward className="w-5 h-5" />
|
||||
<span className="text-[10px] font-medium leading-tight">{t('forward')}</span>
|
||||
</button>
|
||||
</>)}
|
||||
<button
|
||||
onClick={onNavigateNext}
|
||||
disabled={!onNavigateNext}
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": ".eml importieren",
|
||||
"keyboard_shortcuts": "Tastaturkürzel (?)",
|
||||
"email_source": "E-Mail-Quelltext",
|
||||
"draft_banner": "Diese Nachricht ist ein Entwurf",
|
||||
"edit_draft": "Bearbeiten",
|
||||
"copy_source": "In Zwischenablage kopieren",
|
||||
"source_copied": "Quelltext in Zwischenablage kopiert",
|
||||
"attachments": "Anhänge",
|
||||
@@ -296,7 +298,8 @@
|
||||
"unstar": "Markierung entfernen (s)",
|
||||
"compose": "Verfassen (c)",
|
||||
"previous": "Vorherige E-Mail",
|
||||
"next": "Nächste E-Mail"
|
||||
"next": "Nächste E-Mail",
|
||||
"edit_draft": "Entwurf bearbeiten"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "Spam melden",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "Kein Spam",
|
||||
"color_tag": "Label",
|
||||
"remove_color": "Label entfernen",
|
||||
"items_selected": "{count} E-Mails ausgewählt"
|
||||
"items_selected": "{count} E-Mails ausgewählt",
|
||||
"edit_draft": "Entwurf bearbeiten"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Tastaturkürzel",
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": "Import .eml",
|
||||
"keyboard_shortcuts": "Keyboard shortcuts (?)",
|
||||
"email_source": "Email Source",
|
||||
"draft_banner": "This message is a draft",
|
||||
"edit_draft": "Edit",
|
||||
"copy_source": "Copy to clipboard",
|
||||
"source_copied": "Source copied to clipboard",
|
||||
"attachments": "Attachments",
|
||||
@@ -298,7 +300,8 @@
|
||||
"unstar": "Unstar (s)",
|
||||
"compose": "Compose (c)",
|
||||
"previous": "Previous email",
|
||||
"next": "Next email"
|
||||
"next": "Next email",
|
||||
"edit_draft": "Edit draft"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "Report spam",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "Not spam",
|
||||
"color_tag": "Label",
|
||||
"remove_color": "Remove Label",
|
||||
"items_selected": "{count} emails selected"
|
||||
"items_selected": "{count} emails selected",
|
||||
"edit_draft": "Edit Draft"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": "Importar .eml",
|
||||
"keyboard_shortcuts": "Atajos de teclado (?)",
|
||||
"email_source": "Código Fuente del Correo",
|
||||
"draft_banner": "Este mensaje es un borrador",
|
||||
"edit_draft": "Editar",
|
||||
"copy_source": "Copiar al portapapeles",
|
||||
"source_copied": "Código fuente copiado al portapapeles",
|
||||
"attachments": "Archivos adjuntos",
|
||||
@@ -296,7 +298,8 @@
|
||||
"unstar": "Quitar estrella (s)",
|
||||
"compose": "Redactar (c)",
|
||||
"previous": "Correo anterior",
|
||||
"next": "Correo siguiente"
|
||||
"next": "Correo siguiente",
|
||||
"edit_draft": "Editar borrador"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "Reportar spam",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "No es spam",
|
||||
"color_tag": "Etiqueta",
|
||||
"remove_color": "Eliminar etiqueta",
|
||||
"items_selected": "{count} correos seleccionados"
|
||||
"items_selected": "{count} correos seleccionados",
|
||||
"edit_draft": "Editar borrador"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Atajos de Teclado",
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": "Importer un .eml",
|
||||
"keyboard_shortcuts": "Raccourcis clavier (?)",
|
||||
"email_source": "Source de l'email",
|
||||
"draft_banner": "Ce message est un brouillon",
|
||||
"edit_draft": "Modifier",
|
||||
"copy_source": "Copier dans le presse-papiers",
|
||||
"source_copied": "Source copiée dans le presse-papiers",
|
||||
"attachments": "Pièces jointes",
|
||||
@@ -296,7 +298,8 @@
|
||||
"unstar": "Ne plus suivre (s)",
|
||||
"compose": "Rédiger (c)",
|
||||
"previous": "E-mail précédent",
|
||||
"next": "E-mail suivant"
|
||||
"next": "E-mail suivant",
|
||||
"edit_draft": "Modifier le brouillon"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "Signaler comme spam",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "Pas un spam",
|
||||
"color_tag": "Étiquette",
|
||||
"remove_color": "Supprimer l'étiquette",
|
||||
"items_selected": "{count} emails sélectionnés"
|
||||
"items_selected": "{count} emails sélectionnés",
|
||||
"edit_draft": "Modifier le brouillon"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Raccourcis clavier",
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": "Importa .eml",
|
||||
"keyboard_shortcuts": "Scorciatoie da tastiera (?)",
|
||||
"email_source": "Sorgente del messaggio",
|
||||
"draft_banner": "Questo messaggio è una bozza",
|
||||
"edit_draft": "Modifica",
|
||||
"copy_source": "Copia negli appunti",
|
||||
"source_copied": "Sorgente copiata negli appunti",
|
||||
"attachments": "Allegati",
|
||||
@@ -296,7 +298,8 @@
|
||||
"unstar": "Rimuovi stella (s)",
|
||||
"compose": "Scrivi (c)",
|
||||
"previous": "Email precedente",
|
||||
"next": "Email successiva"
|
||||
"next": "Email successiva",
|
||||
"edit_draft": "Modifica bozza"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "Segnala come spam",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "Non spam",
|
||||
"color_tag": "Etichetta",
|
||||
"remove_color": "Rimuovi etichetta",
|
||||
"items_selected": "{count} messaggi selezionati"
|
||||
"items_selected": "{count} messaggi selezionati",
|
||||
"edit_draft": "Modifica bozza"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Scorciatoie da tastiera",
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": ".emlをインポート",
|
||||
"keyboard_shortcuts": "キーボードショートカット (?)",
|
||||
"email_source": "メールソース",
|
||||
"draft_banner": "このメッセージは下書きです",
|
||||
"edit_draft": "編集",
|
||||
"copy_source": "クリップボードにコピー",
|
||||
"source_copied": "ソースをクリップボードにコピーしました",
|
||||
"attachments": "添付ファイル",
|
||||
@@ -296,7 +298,8 @@
|
||||
"unstar": "スター解除 (s)",
|
||||
"compose": "新規作成 (c)",
|
||||
"previous": "前のメール",
|
||||
"next": "次のメール"
|
||||
"next": "次のメール",
|
||||
"edit_draft": "下書きを編集"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "迷惑メールを報告",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "迷惑メールでない",
|
||||
"color_tag": "ラベル",
|
||||
"remove_color": "ラベルを削除",
|
||||
"items_selected": "{count}件のメールを選択"
|
||||
"items_selected": "{count}件のメールを選択",
|
||||
"edit_draft": "下書きを編集"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "キーボードショートカット",
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": ".eml importeren",
|
||||
"keyboard_shortcuts": "Sneltoetsen (?)",
|
||||
"email_source": "E-mailbron",
|
||||
"draft_banner": "Dit bericht is een concept",
|
||||
"edit_draft": "Bewerken",
|
||||
"copy_source": "Kopiëren naar klembord",
|
||||
"source_copied": "Bron gekopieerd naar klembord",
|
||||
"attachments": "Bijlagen",
|
||||
@@ -296,7 +298,8 @@
|
||||
"unstar": "Ster verwijderen (s)",
|
||||
"compose": "Opstellen (c)",
|
||||
"previous": "Vorige e-mail",
|
||||
"next": "Volgende e-mail"
|
||||
"next": "Volgende e-mail",
|
||||
"edit_draft": "Concept bewerken"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "Spam melden",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "Geen spam",
|
||||
"color_tag": "Label",
|
||||
"remove_color": "Label verwijderen",
|
||||
"items_selected": "{count} e-mails geselecteerd"
|
||||
"items_selected": "{count} e-mails geselecteerd",
|
||||
"edit_draft": "Concept bewerken"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Sneltoetsen",
|
||||
|
||||
@@ -195,6 +195,8 @@
|
||||
"import_email": "Importar .eml",
|
||||
"keyboard_shortcuts": "Atalhos de teclado (?)",
|
||||
"email_source": "Código-fonte do E-mail",
|
||||
"draft_banner": "Esta mensagem é um rascunho",
|
||||
"edit_draft": "Editar",
|
||||
"copy_source": "Copiar para a área de transferência",
|
||||
"source_copied": "Código-fonte copiado para a área de transferência",
|
||||
"attachments": "Anexos",
|
||||
@@ -296,7 +298,8 @@
|
||||
"unstar": "Remover favorito (s)",
|
||||
"compose": "Compor (c)",
|
||||
"previous": "E-mail anterior",
|
||||
"next": "Próximo e-mail"
|
||||
"next": "Próximo e-mail",
|
||||
"edit_draft": "Editar rascunho"
|
||||
},
|
||||
"spam": {
|
||||
"button_title": "Reportar spam",
|
||||
@@ -1298,7 +1301,8 @@
|
||||
"not_spam": "Não é spam",
|
||||
"color_tag": "Etiqueta",
|
||||
"remove_color": "Remover etiqueta",
|
||||
"items_selected": "{count} e-mails selecionados"
|
||||
"items_selected": "{count} e-mails selecionados",
|
||||
"edit_draft": "Editar rascunho"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Atalhos de Teclado",
|
||||
|
||||
Reference in New Issue
Block a user