feat: mail grouping with group headers in MailList, connected to GroupPanel
This commit is contained in:
@@ -9,11 +9,13 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import type { Mail } from '@/api/mail';
|
import type { Mail } from '@/api/mail';
|
||||||
import { decodeMimeHeader } from '@/api/mail';
|
import { decodeMimeHeader } from '@/api/mail';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { Loader2, Paperclip, Star } from 'lucide-react';
|
import { Loader2, Paperclip, Star, ChevronDown } from 'lucide-react';
|
||||||
import { formatSmartDate } from '@/utils/date';
|
import { formatSmartDate } from '@/utils/date';
|
||||||
|
import type { GroupedMails } from '@/components/mail/MailGroupPanel';
|
||||||
|
|
||||||
export interface MailListProps {
|
export interface MailListProps {
|
||||||
mails: Mail[];
|
mails: Mail[];
|
||||||
|
groupedMails?: GroupedMails[] | null;
|
||||||
selectedMailId: string | null;
|
selectedMailId: string | null;
|
||||||
onSelectMail: (mail: Mail) => void;
|
onSelectMail: (mail: Mail) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -30,6 +32,7 @@ function formatDate(dateStr: string): string {
|
|||||||
|
|
||||||
export function MailList({
|
export function MailList({
|
||||||
mails,
|
mails,
|
||||||
|
groupedMails,
|
||||||
selectedMailId,
|
selectedMailId,
|
||||||
onSelectMail,
|
onSelectMail,
|
||||||
loading,
|
loading,
|
||||||
@@ -161,7 +164,7 @@ export function MailList({
|
|||||||
{loading && mails.length > 0 && <Loader2 className="w-4 h-4 animate-spin text-secondary-400" />}
|
{loading && mails.length > 0 && <Loader2 className="w-4 h-4 animate-spin text-secondary-400" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mail list — infinite scroll, no pagination */}
|
{/* Mail list — infinite scroll with optional grouping */}
|
||||||
<ul
|
<ul
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="divide-y divide-secondary-100 flex-1 overflow-y-auto"
|
className="divide-y divide-secondary-100 flex-1 overflow-y-auto"
|
||||||
@@ -169,7 +172,38 @@ export function MailList({
|
|||||||
data-testid="mail-list-items"
|
data-testid="mail-list-items"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
>
|
>
|
||||||
{mails.map((mail) => renderMailItem(mail))}
|
{groupedMails && groupedMails.length > 0 ? (
|
||||||
|
groupedMails.map((group) => (
|
||||||
|
<li key={group.key} className="bg-secondary-50/50">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 bg-secondary-100/70 sticky top-0 z-10">
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-secondary-500" />
|
||||||
|
<span className="text-xs font-semibold text-secondary-700">{group.label}</span>
|
||||||
|
<span className="text-[10px] text-secondary-400">({group.mails.length})</span>
|
||||||
|
</div>
|
||||||
|
<ul className="divide-y divide-secondary-100">
|
||||||
|
{group.mails.map((mail) => renderMailItem(mail))}
|
||||||
|
</ul>
|
||||||
|
{group.subGroups && group.subGroups.length > 0 && (
|
||||||
|
<ul className="pl-3 border-l border-secondary-200">
|
||||||
|
{group.subGroups.map((sub) => (
|
||||||
|
<li key={sub.key}>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-secondary-50">
|
||||||
|
<ChevronDown className="w-3 h-3 text-secondary-400" />
|
||||||
|
<span className="text-[11px] font-medium text-secondary-600">{sub.label}</span>
|
||||||
|
<span className="text-[10px] text-secondary-400">({sub.mails.length})</span>
|
||||||
|
</div>
|
||||||
|
<ul className="divide-y divide-secondary-100">
|
||||||
|
{sub.mails.map((mail) => renderMailItem(mail))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
mails.map((mail) => renderMailItem(mail))
|
||||||
|
)}
|
||||||
{loading && mails.length > 0 && (
|
{loading && mails.length > 0 && (
|
||||||
<li className="flex items-center justify-center py-4">
|
<li className="flex items-center justify-center py-4">
|
||||||
<Loader2 className="w-5 h-5 animate-spin text-secondary-400" />
|
<Loader2 className="w-5 h-5 animate-spin text-secondary-400" />
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
|||||||
import { ArrowRight, Check, ChevronLeft, ExternalLink, Loader2, Plus, Redo2, Trash2, TrendingUp, Undo2 } from 'lucide-react';
|
import { ArrowRight, Check, ChevronLeft, ExternalLink, Loader2, Plus, Redo2, Trash2, TrendingUp, Undo2 } from 'lucide-react';
|
||||||
import { MailFilterPanel, type FilterState as MailFilterState, emptyFilterState as emptyMailFilterState, applyFilters as applyMailFilters } from '@/components/mail/MailFilterPanel';
|
import { MailFilterPanel, type FilterState as MailFilterState, emptyFilterState as emptyMailFilterState, applyFilters as applyMailFilters } from '@/components/mail/MailFilterPanel';
|
||||||
import { MailSortPanel, type SortState as MailSortState, emptySortState as emptyMailSortState, applySorting as applyMailSorting } from '@/components/mail/MailSortPanel';
|
import { MailSortPanel, type SortState as MailSortState, emptySortState as emptyMailSortState, applySorting as applyMailSorting } from '@/components/mail/MailSortPanel';
|
||||||
import { MailGroupPanel, type GroupState as MailGroupState, emptyGroupState as emptyMailGroupState } from '@/components/mail/MailGroupPanel';
|
import { MailGroupPanel, type GroupState as MailGroupState, emptyGroupState as emptyMailGroupState, applyGrouping as applyMailGrouping, type GroupedMails } from '@/components/mail/MailGroupPanel';
|
||||||
import type { Tag } from '@/api/tags';
|
import type { Tag } from '@/api/tags';
|
||||||
import { useSavedFilters } from '@/api/savedFilters';
|
import { useSavedFilters } from '@/api/savedFilters';
|
||||||
import {
|
import {
|
||||||
@@ -106,6 +106,12 @@ export function MailPage() {
|
|||||||
return result;
|
return result;
|
||||||
}, [mails, mailFilterState, mailSortState]);
|
}, [mails, mailFilterState, mailSortState]);
|
||||||
|
|
||||||
|
// Apply grouping after filter+sort
|
||||||
|
const groupedMails = useMemo(() => {
|
||||||
|
if (mailGroupState.conditions.length === 0) return null;
|
||||||
|
return applyMailGrouping(processedMails, mailGroupState);
|
||||||
|
}, [processedMails, mailGroupState]);
|
||||||
|
|
||||||
// Ref to track the current folder ID for async callbacks (prevents race conditions)
|
// Ref to track the current folder ID for async callbacks (prevents race conditions)
|
||||||
const selectedFolderIdRef = useRef<string | null>(null);
|
const selectedFolderIdRef = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -890,6 +896,7 @@ export function MailPage() {
|
|||||||
>
|
>
|
||||||
<MailList
|
<MailList
|
||||||
mails={processedMails}
|
mails={processedMails}
|
||||||
|
groupedMails={groupedMails}
|
||||||
selectedMailId={selectedMail?.id || null}
|
selectedMailId={selectedMail?.id || null}
|
||||||
onSelectMail={handleSelectMail}
|
onSelectMail={handleSelectMail}
|
||||||
loading={loadingMails}
|
loading={loadingMails}
|
||||||
@@ -960,6 +967,7 @@ export function MailPage() {
|
|||||||
</div>
|
</div>
|
||||||
<MailList
|
<MailList
|
||||||
mails={processedMails}
|
mails={processedMails}
|
||||||
|
groupedMails={groupedMails}
|
||||||
selectedMailId={selectedMail?.id || null}
|
selectedMailId={selectedMail?.id || null}
|
||||||
onSelectMail={handleSelectMail}
|
onSelectMail={handleSelectMail}
|
||||||
loading={loadingMails}
|
loading={loadingMails}
|
||||||
|
|||||||
Reference in New Issue
Block a user