feat: mail phase 2 - delete, move, drafts
- Add deleted_at field to Mail/MailAccount/MailFolder models
- Add DELETE /{mail_id} endpoint (soft-delete + IMAP EXPUNGE)
- Add POST /{mail_id}/move endpoint (IMAP UID MOVE + folder_id update)
- Add POST /drafts and PUT /drafts/{id} endpoints (save/edit drafts)
- Add imap_delete_mail() and imap_move_mail() service functions
- Add save_draft() and update_draft() service functions
- Frontend: deleteMail, moveMail, saveDraft, updateDraft API functions
- ComposeModal: draft mode with Save Draft button
- MailDetail: delete/move buttons, edit-draft for drafts
- Mail.tsx: bulk delete/move, move dropdown, draft handlers
- i18n: 13 new keys (de/en)
This commit is contained in:
@@ -19,6 +19,9 @@ export interface MailDetailProps {
|
||||
onToggleFlag: (mail: Mail) => void;
|
||||
onDownloadAttachment: (mailId: string, attachment: MailAttachment) => void;
|
||||
downloadingAttachmentId: string | null;
|
||||
onDelete?: (mail: Mail) => void;
|
||||
onMove?: (mail: Mail) => void;
|
||||
onEditDraft?: (mail: Mail) => void;
|
||||
}
|
||||
|
||||
function formatFullDate(dateStr: string): string {
|
||||
@@ -47,6 +50,9 @@ export function MailDetail({
|
||||
onToggleFlag,
|
||||
onDownloadAttachment,
|
||||
downloadingAttachmentId,
|
||||
onDelete,
|
||||
onMove,
|
||||
onEditDraft,
|
||||
}: MailDetailProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -98,20 +104,32 @@ export function MailDetail({
|
||||
<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">
|
||||
<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>
|
||||
{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"
|
||||
@@ -125,6 +143,20 @@ export function MailDetail({
|
||||
>
|
||||
{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"
|
||||
@@ -138,6 +170,20 @@ export function MailDetail({
|
||||
>
|
||||
{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 */}
|
||||
|
||||
Reference in New Issue
Block a user