feat: mail UI improvements - folder mapping, display name, resize perf, list header
- Add configurable folder mapping (sent/drafts/spam/trash) per account - Migration 0005_folder_mapping.sql with 4 new columns on mail_accounts - Backend: send_mail uses account.sent_folder_imap_name if set - Backend: _build_folder_hierarchy respects folder_mapping for is_standard - Frontend: MailSettings shows folder mapping editor per account - Frontend: MailSettings allows editing display_name for existing accounts - Frontend: MailFolderTree shows display_name || email, removes MailIcon - Frontend: ResizablePanel isDragging state with contain:strict + pointer-events:none - Frontend: MailList merges select-all and sort controls into one line
This commit is contained in:
@@ -91,56 +91,57 @@ export function MailList({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full" data-testid="mail-list" role="listbox" aria-label={t('mail.selectMail')}>
|
||||
{/* Select-all header */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 md:px-4 md:py-2 border-b border-secondary-200 bg-secondary-50 flex-shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
onChange={onSelectAll}
|
||||
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
|
||||
aria-label={t('mail.selectAll')}
|
||||
data-testid="mail-select-all"
|
||||
/>
|
||||
{selectedMailIds.size > 0 && (
|
||||
<span className="text-xs text-secondary-600">
|
||||
{t('mail.selectedCount', { count: selectedMailIds.size })}
|
||||
</span>
|
||||
{/* Select-all + Sort header in one row */}
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2 md:px-4 md:py-2 border-b border-secondary-200 bg-secondary-50 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
onChange={onSelectAll}
|
||||
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
|
||||
aria-label={t('mail.selectAll')}
|
||||
data-testid="mail-select-all"
|
||||
/>
|
||||
{selectedMailIds.size > 0 && (
|
||||
<span className="text-xs text-secondary-600">
|
||||
{t('mail.selectedCount', { count: selectedMailIds.size })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{onSortChange && (
|
||||
<div className="flex items-center gap-2" 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>
|
||||
)}
|
||||
</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 flex-shrink-0" 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>
|
||||
)}
|
||||
{/* Mail list — scrolls, pagination stays fixed */}
|
||||
<ul className="divide-y divide-secondary-100 flex-1 overflow-y-auto" role="list">
|
||||
{mails.map((mail) => (
|
||||
|
||||
Reference in New Issue
Block a user