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:
@@ -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);
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user