feat: mail phase 3 - sorting (date/from/subject, asc/desc)

- Add sort_by and sort_order query params to list_mails endpoint
- Add deleted_at IS NULL filter to exclude soft-deleted mails
- Frontend: sort state in Mail.tsx, sort header bar in MailList
- Sort dropdown (Datum/Absender/Betreff) + asc/desc toggle
- fetchMails API accepts optional sortBy/sortOrder params
- i18n: 6 new sort keys (de/en)
This commit is contained in:
Agent Zero
2026-07-15 20:17:27 +02:00
parent ed1eec87dc
commit 4e100e9d33
6 changed files with 101 additions and 9 deletions
+5 -3
View File
@@ -406,9 +406,11 @@ export function deleteFolder(folderId: string): Promise<void> {
// ─── Mails ──────────────────────────────────────────────────────────────────
export function fetchMails(folderId: string, page: number): Promise<MailListResult> {
const qs = new URLSearchParams({ folder_id: folderId, page: String(page) }).toString();
return apiGet<MailListResult>(`/mail?${qs}`);
export function fetchMails(folderId: string, page: number, sortBy?: string, sortOrder?: string): Promise<MailListResult> {
const params = new URLSearchParams({ folder_id: folderId, page: String(page) });
if (sortBy) params.set('sort_by', sortBy);
if (sortOrder) params.set('sort_order', sortOrder);
return apiGet<MailListResult>(`/mail?${params.toString()}`);
}
export function getMail(mailId: string): Promise<Mail> {
+50
View File
@@ -22,6 +22,9 @@ export interface MailListProps {
selectedMailIds: Set<string>;
onToggleSelect: (mailId: string) => void;
onSelectAll: () => void;
sortBy?: 'date' | 'from' | 'subject';
sortOrder?: 'asc' | 'desc';
onSortChange?: (sortBy: 'date' | 'from' | 'subject', sortOrder: 'asc' | 'desc') => void;
}
function formatDate(dateStr: string): string {
@@ -45,11 +48,24 @@ export function MailList({
selectedMailIds,
onToggleSelect,
onSelectAll,
sortBy = 'date',
sortOrder = 'desc',
onSortChange,
}: MailListProps) {
const { t } = useTranslation();
const totalPages = Math.ceil(total / pageSize);
const allSelected = mails.length > 0 && mails.every((m) => selectedMailIds.has(m.id));
const handleSortFieldChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newSortBy = e.target.value as 'date' | 'from' | 'subject';
onSortChange?.(newSortBy, sortOrder);
};
const handleSortOrderToggle = () => {
const newOrder = sortOrder === 'asc' ? 'desc' : 'asc';
onSortChange?.(sortBy, newOrder);
};
if (loading) {
return (
<div className="flex items-center justify-center py-12" data-testid="mail-list-loading">
@@ -90,6 +106,40 @@ export function MailList({
</span>
)}
</div>
{/* Sort header */}
{onSortChange && (
<div className="flex items-center gap-2 px-3 py-1.5 md:px-4 border-b border-secondary-200 bg-secondary-50" data-testid="mail-sort-bar">
<label htmlFor="mail-sort-by" className="text-xs text-secondary-600">
{t('mail.sortBy')}:
</label>
<select
id="mail-sort-by"
value={sortBy}
onChange={handleSortFieldChange}
className="text-xs rounded border border-secondary-300 bg-white px-1.5 py-0.5 text-secondary-700 focus:outline-none focus:ring-1 focus:ring-primary-500"
aria-label={t('mail.sortBy')}
data-testid="mail-sort-by"
>
<option value="date">{t('mail.sortDate')}</option>
<option value="from">{t('mail.sortFrom')}</option>
<option value="subject">{t('mail.sortSubject')}</option>
</select>
<button
onClick={handleSortOrderToggle}
className="inline-flex items-center gap-1 text-xs rounded border border-secondary-300 bg-white px-1.5 py-0.5 text-secondary-700 hover:bg-secondary-100 focus:outline-none focus:ring-1 focus:ring-primary-500"
aria-label={sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}
title={sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}
data-testid="mail-sort-order"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
{sortOrder === 'asc'
? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
: <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />}
</svg>
<span>{sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}</span>
</button>
</div>
)}
<ul className="divide-y divide-secondary-100" role="list">
{mails.map((mail) => (
<li key={mail.id} role="listitem" className={clsx(selectedMailIds.has(mail.id) && 'bg-primary-50')}>
+7 -1
View File
@@ -553,6 +553,12 @@
"saveDraft": "Entwurf speichern",
"editDraft": "Entwurf bearbeiten",
"draftSaved": "Entwurf gespeichert.",
"draft": "Entwurf"
"draft": "Entwurf",
"sortBy": "Sortieren nach",
"sortDate": "Datum",
"sortFrom": "Absender",
"sortSubject": "Betreff",
"sortAsc": "Aufsteigend",
"sortDesc": "Absteigend"
}
}
+7 -1
View File
@@ -553,6 +553,12 @@
"saveDraft": "Save Draft",
"editDraft": "Edit Draft",
"draftSaved": "Draft saved.",
"draft": "Draft"
"draft": "Draft",
"sortBy": "Sort by",
"sortDate": "Date",
"sortFrom": "From",
"sortSubject": "Subject",
"sortAsc": "Ascending",
"sortDesc": "Descending"
}
}
+18 -2
View File
@@ -78,6 +78,8 @@ export function MailPage() {
const [activeView, setActiveView] = useState<'folders' | 'list' | 'detail'>('folders');
const [selectedMailIds, setSelectedMailIds] = useState<Set<string>>(new Set());
const [showMoveDropdown, setShowMoveDropdown] = useState(false);
const [sortBy, setSortBy] = useState<'date' | 'from' | 'subject'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
// Load accounts
const loadAccounts = useCallback(async () => {
@@ -150,7 +152,7 @@ export function MailPage() {
setMails(result.mails);
setMailsTotal(result.total);
} else {
const result = await fetchMails(selectedFolderId, mailsPage);
const result = await fetchMails(selectedFolderId, mailsPage, sortBy, sortOrder);
setMails(result.mails);
setMailsTotal(result.total);
}
@@ -160,7 +162,7 @@ export function MailPage() {
setMails([]);
}
setLoadingMails(false);
}, [selectedFolderId, mailsPage, searchQuery]);
}, [selectedFolderId, mailsPage, searchQuery, sortBy, sortOrder]);
useEffect(() => {
loadMails();
@@ -722,6 +724,13 @@ export function MailPage() {
selectedMailIds={selectedMailIds}
onToggleSelect={handleToggleSelect}
onSelectAll={handleSelectAll}
sortBy={sortBy}
sortOrder={sortOrder}
onSortChange={(by, order) => {
setSortBy(by);
setSortOrder(order);
setMailsPage(1);
}}
/>
</ResizablePanel>
@@ -792,6 +801,13 @@ export function MailPage() {
selectedMailIds={selectedMailIds}
onToggleSelect={handleToggleSelect}
onSelectAll={handleSelectAll}
sortBy={sortBy}
sortOrder={sortOrder}
onSortChange={(by, order) => {
setSortBy(by);
setSortOrder(order);
setMailsPage(1);
}}
/>
</div>
)}