fix: implement draft editing functionality across email components and add localization keys

This commit is contained in:
Linus Rath
2026-03-18 18:39:46 +01:00
parent 6457b27125
commit bb72ac92ae
12 changed files with 156 additions and 19 deletions
+16
View File
@@ -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 && (
<>
+3
View File
@@ -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)}
+58 -3
View File
@@ -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}