feat: mail list font size, remove Termin toolbar, rename Flagge to Markierung with symbol picker

- Mail list: title and subject font text-xs -> text-sm
- Remove Termin (create-event) toolbar button
- Rename Flagge -> Markierung with select dropdown (star, flag, bookmark, important, question)
- Backend: add flag_type column to Mail model + migration 0006
- Backend: flag_type in MailFlagsUpdate schema, update_flags route, mail_to_response
- Frontend: flag_type in Mail interface, FlagUpdatePayload, MailList symbol display
This commit is contained in:
Agent Zero
2026-07-20 10:39:16 +02:00
parent 5fa3f0540b
commit 9da04057b9
8 changed files with 53 additions and 27 deletions
@@ -0,0 +1,2 @@
-- Add flag_type column to mails table for multiple symbol types
ALTER TABLE mails ADD COLUMN IF NOT EXISTS flag_type VARCHAR(32);
+1
View File
@@ -130,6 +130,7 @@ class Mail(Base, TenantMixin):
body_html_sanitized: Mapped[str] = mapped_column(Text, nullable=False, default="") body_html_sanitized: Mapped[str] = mapped_column(Text, nullable=False, default="")
is_seen: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_seen: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_flagged: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_flagged: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
flag_type: Mapped[str | None] = mapped_column(String(32), nullable=True, default=None)
is_draft: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_draft: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_answered: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_answered: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_forwarded: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_forwarded: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
+2
View File
@@ -1290,6 +1290,8 @@ async def update_flags(
mail.is_seen = data.is_seen mail.is_seen = data.is_seen
if data.is_flagged is not None: if data.is_flagged is not None:
mail.is_flagged = data.is_flagged mail.is_flagged = data.is_flagged
if data.flag_type is not None:
mail.flag_type = data.flag_type if data.flag_type else None
if data.is_draft is not None: if data.is_draft is not None:
mail.is_draft = data.is_draft mail.is_draft = data.is_draft
if data.is_answered is not None: if data.is_answered is not None:
+2
View File
@@ -145,6 +145,7 @@ class MailForwardRequest(BaseModel):
class MailFlagsUpdate(BaseModel): class MailFlagsUpdate(BaseModel):
is_seen: bool | None = None is_seen: bool | None = None
is_flagged: bool | None = None is_flagged: bool | None = None
flag_type: str | None = None
is_draft: bool | None = None is_draft: bool | None = None
is_answered: bool | None = None is_answered: bool | None = None
is_forwarded: bool | None = None is_forwarded: bool | None = None
@@ -166,6 +167,7 @@ class MailResponse(BaseModel):
body_html_sanitized: str body_html_sanitized: str
is_seen: bool is_seen: bool
is_flagged: bool is_flagged: bool
flag_type: str | None = None
is_draft: bool is_draft: bool
is_answered: bool is_answered: bool
is_forwarded: bool is_forwarded: bool
+1
View File
@@ -1562,6 +1562,7 @@ def mail_to_response(
"date": mail.received_at.isoformat() if mail.received_at else (mail.sent_at.isoformat() if mail.sent_at else None), "date": mail.received_at.isoformat() if mail.received_at else (mail.sent_at.isoformat() if mail.sent_at else None),
"is_seen": mail.is_seen, "is_seen": mail.is_seen,
"is_flagged": mail.is_flagged, "is_flagged": mail.is_flagged,
"flag_type": mail.flag_type,
"is_draft": mail.is_draft, "is_draft": mail.is_draft,
"is_answered": mail.is_answered, "is_answered": mail.is_answered,
"is_forwarded": mail.is_forwarded, "is_forwarded": mail.is_forwarded,
+2
View File
@@ -65,6 +65,7 @@ export interface Mail {
date: string; date: string;
is_seen: boolean; is_seen: boolean;
is_flagged: boolean; is_flagged: boolean;
flag_type?: string | null;
is_draft: boolean; is_draft: boolean;
is_answered: boolean; is_answered: boolean;
has_attachments: boolean; has_attachments: boolean;
@@ -263,6 +264,7 @@ export interface ForwardPayload {
export interface FlagUpdatePayload { export interface FlagUpdatePayload {
is_seen?: boolean; is_seen?: boolean;
is_flagged?: boolean; is_flagged?: boolean;
flag_type?: string | null;
is_draft?: boolean; is_draft?: boolean;
is_answered?: boolean; is_answered?: boolean;
is_forwarded?: boolean; is_forwarded?: boolean;
+14 -5
View File
@@ -173,9 +173,18 @@ export function MailList({
<span className="w-2 h-2 rounded-full bg-primary-500 flex-shrink-0" aria-label={t('mail.unread')} /> <span className="w-2 h-2 rounded-full bg-primary-500 flex-shrink-0" aria-label={t('mail.unread')} />
)} )}
{mail.is_flagged && ( {mail.is_flagged && (
<svg className="w-4 h-4 text-warning-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24" aria-label={t('mail.flagged')}> <span className="flex-shrink-0" aria-label={t('mail.flagged')}>
<path 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" /> {mail.flag_type === 'star' && <span className="text-sm"></span>}
</svg> {mail.flag_type === 'flag' && <span className="text-sm">🚩</span>}
{mail.flag_type === 'bookmark' && <span className="text-sm">🔖</span>}
{mail.flag_type === 'important' && <span className="text-sm"></span>}
{mail.flag_type === 'question' && <span className="text-sm"></span>}
{(!mail.flag_type || mail.flag_type === '') && (
<svg className="w-4 h-4 text-warning-500" fill="currentColor" viewBox="0 0 24 24">
<path 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>
)}
</span>
)} )}
{mail.has_attachments && ( {mail.has_attachments && (
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
@@ -184,12 +193,12 @@ export function MailList({
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className={clsx('text-xs truncate', !mail.is_seen ? 'text-secondary-900 font-semibold' : 'text-secondary-700')}> <span className={clsx('text-sm truncate', !mail.is_seen ? 'text-secondary-900 font-semibold' : 'text-secondary-700')}>
{decodeMimeHeader(mail.from_name) || mail.from_address} {decodeMimeHeader(mail.from_name) || mail.from_address}
</span> </span>
<span className="text-xs text-secondary-400 flex-shrink-0">{formatDate(mail.date)}</span> <span className="text-xs text-secondary-400 flex-shrink-0">{formatDate(mail.date)}</span>
</div> </div>
<p className={clsx('text-xs truncate mt-0.5', !mail.is_seen ? 'text-secondary-800' : 'text-secondary-600')}> <p className={clsx('text-sm truncate mt-0.5', !mail.is_seen ? 'text-secondary-800' : 'text-secondary-600')}>
{decodeMimeHeader(mail.subject) || t('mail.noSubject')} {decodeMimeHeader(mail.subject) || t('mail.noSubject')}
</p> </p>
{mail.labels && mail.labels.length > 0 && ( {mail.labels && mail.labels.length > 0 && (
+29 -22
View File
@@ -340,6 +340,21 @@ export function MailPage() {
[toast], [toast],
); );
// Handle set flag type (symbol picker)
const handleSetFlagType = useCallback(
async (mail: Mail, flagType: string) => {
try {
const isFlagged = flagType !== '';
await updateFlags(mail.id, { is_flagged: isFlagged, flag_type: flagType || null });
setSelectedMail((prev) => (prev ? { ...prev, is_flagged: isFlagged, flag_type: flagType || null } : prev));
setMails((prev) => prev.map((m) => (m.id === mail.id ? { ...m, is_flagged: isFlagged, flag_type: flagType || null } : m)));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
},
[toast],
);
// Handle toggle select for bulk operations // Handle toggle select for bulk operations
const handleToggleSelect = useCallback((mailId: string) => { const handleToggleSelect = useCallback((mailId: string) => {
setSelectedMailIds((prev) => { setSelectedMailIds((prev) => {
@@ -532,29 +547,21 @@ export function MailPage() {
{ {
id: 'flag', id: 'flag',
plugin: 'mail', plugin: 'mail',
label: 'Flagge', label: 'Markierung',
group: 'mail-actions', group: 'mail-actions',
disabled: !hasMail, disabled: !hasMail,
active: selectedMailFlagged, type: 'select' as const,
icon: ( selectValue: selectedMail?.flag_type || '',
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> selectOptions: [
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" /> { value: '', label: 'Keine' },
</svg> { value: 'star', label: '⭐ Stern' },
), { value: 'flag', label: '🚩 Flagge' },
onClick: () => selectedMail && handleToggleFlag(selectedMail), { value: 'bookmark', label: '🔖 Lesezeichen' },
}, { value: 'important', label: '❗ Wichtig' },
{ { value: 'question', label: '❓ Frage' },
id: 'create-event', ],
plugin: 'mail', onSelect: (value: string) => selectedMail && handleSetFlagType(selectedMail, value),
label: 'Termin', onClick: () => {},
group: 'mail-actions',
disabled: !hasMail,
icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
),
onClick: () => selectedMail && handleCreateEvent(selectedMail),
}, },
{ {
id: 'delete', id: 'delete',
@@ -667,7 +674,7 @@ export function MailPage() {
} }
registerItems('mail', items); registerItems('mail', items);
return () => unregisterPlugin('mail'); return () => unregisterPlugin('mail');
}, [selectedMailId, selectedMailFlagged, selectedMailIds, handleSearch, handleCompose, handleReply, handleForward, handleToggleFlag, handleCreateEvent, handleDeleteMail, handleBulkDelete, handleBulkMarkRead, handleBulkMarkUnread, t]); }, [selectedMailId, selectedMailFlagged, selectedMailIds, handleSearch, handleCompose, handleReply, handleForward, handleToggleFlag, handleSetFlagType, handleDeleteMail, handleBulkDelete, handleBulkMarkRead, handleBulkMarkUnread, t]);
if (loadingAccounts) { if (loadingAccounts) {
return ( return (