feat: bulk actions for email lists
Select multiple rows (checkbox per row) in FolderView, SearchResults, and the Senders email panel. A toolbar appears with mark read/unread, star, archive, and trash, applied to the whole selection via CleanupService.ExecuteAsync (already scoped to UserId). Shared via useSelection hook and BulkToolbar component to avoid duplicating selection state across the three list views. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SearchApi } from '../api/client.js';
|
||||
import EmailRow from '../components/EmailRow.jsx';
|
||||
import BulkToolbar from '../components/BulkToolbar.jsx';
|
||||
import useSelection from '../hooks/useSelection.js';
|
||||
|
||||
const FOLDER_META = {
|
||||
inbox: { icon: '📥', label: 'Inbox' },
|
||||
@@ -43,6 +45,7 @@ export default function FolderView() {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { selected, toggle, clear, removeIds, selectedIds } = useSelection();
|
||||
|
||||
// Reset when the folder changes
|
||||
useEffect(() => {
|
||||
@@ -51,7 +54,8 @@ export default function FolderView() {
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
setError(null);
|
||||
}, [slug]);
|
||||
clear();
|
||||
}, [slug, clear]);
|
||||
|
||||
// Fetch a page and append results
|
||||
const fetchPage = useCallback((p) => {
|
||||
@@ -98,6 +102,17 @@ export default function FolderView() {
|
||||
|
||||
{error && <div className="fv-error">{error}</div>}
|
||||
|
||||
<BulkToolbar
|
||||
selectedIds={selectedIds}
|
||||
onClear={clear}
|
||||
onDone={(action, ids) => {
|
||||
if (action === 'trash' || action === 'archive') {
|
||||
setEmails((prev) => prev.filter((x) => !ids.includes(x.id)));
|
||||
removeIds(ids);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{!loading && !error && emails.length === 0 && (
|
||||
<div className="fv-empty">No emails in this folder.</div>
|
||||
)}
|
||||
@@ -109,6 +124,8 @@ export default function FolderView() {
|
||||
<EmailRow
|
||||
key={e.id}
|
||||
email={e}
|
||||
selected={selected.has(e.id)}
|
||||
onToggleSelect={toggle}
|
||||
onRemove={(id) => setEmails((prev) => prev.filter((x) => x.id !== id))}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { SearchApi } from '../api/client.js';
|
||||
import EmailRow from '../components/EmailRow.jsx';
|
||||
import BulkToolbar from '../components/BulkToolbar.jsx';
|
||||
import useSelection from '../hooks/useSelection.js';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
@@ -16,6 +18,7 @@ export default function SearchResults() {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { selected, toggle, clear, removeIds, selectedIds } = useSelection();
|
||||
|
||||
useEffect(() => {
|
||||
setEmails([]);
|
||||
@@ -23,7 +26,8 @@ export default function SearchResults() {
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
setError(null);
|
||||
}, [q]);
|
||||
clear();
|
||||
}, [q, clear]);
|
||||
|
||||
const fetchPage = useCallback((p) => {
|
||||
if (!q.trim()) return;
|
||||
@@ -70,6 +74,17 @@ export default function SearchResults() {
|
||||
<div className="fv-empty">No results for "{q}".</div>
|
||||
)}
|
||||
|
||||
<BulkToolbar
|
||||
selectedIds={selectedIds}
|
||||
onClear={clear}
|
||||
onDone={(action, ids) => {
|
||||
if (action === 'trash' || action === 'archive') {
|
||||
setEmails((prev) => prev.filter((x) => !ids.includes(x.id)));
|
||||
removeIds(ids);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{emails.length > 0 && (
|
||||
<table className="email-list">
|
||||
<tbody>
|
||||
@@ -77,6 +92,8 @@ export default function SearchResults() {
|
||||
<EmailRow
|
||||
key={e.id}
|
||||
email={e}
|
||||
selected={selected.has(e.id)}
|
||||
onToggleSelect={toggle}
|
||||
onRemove={(id) => setEmails((prev) => prev.filter((x) => x.id !== id))}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useCallback, useRef, useState } from 'react';
|
||||
import { AnalyticsApi, SearchApi, EmailApi } from '../api/client.js';
|
||||
import BulkToolbar from '../components/BulkToolbar.jsx';
|
||||
import useSelection from '../hooks/useSelection.js';
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -123,7 +125,7 @@ function EmailDetail({ email: summary, onBack }) {
|
||||
|
||||
// ── EmailListRow (row inside sender email list) ───────────────────────────────
|
||||
|
||||
function EmailListRow({ email: initial, onRemove, onOpen }) {
|
||||
function EmailListRow({ email: initial, onRemove, onOpen, selected, onToggleSelect }) {
|
||||
const [email, setEmail] = useState(initial);
|
||||
const [acting, setActing] = useState(false);
|
||||
|
||||
@@ -150,6 +152,9 @@ function EmailListRow({ email: initial, onRemove, onOpen }) {
|
||||
title="View email"
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<td className="el-select" onClick={(e) => e.stopPropagation()}>
|
||||
<input type="checkbox" checked={!!selected} onChange={() => onToggleSelect(email.id)} />
|
||||
</td>
|
||||
<td className="el-unread">{email.isUnread && <span className="unread-dot" />}</td>
|
||||
<td className="el-subject">
|
||||
<span className="el-subj-text">{email.subject || '(no subject)'}</span>
|
||||
@@ -195,6 +200,7 @@ function SenderEmails({ sender }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openEmail, setOpenEmail] = useState(null);
|
||||
const sentinelRef = useRef(null);
|
||||
const { selected, toggle, clear, removeIds, selectedIds } = useSelection();
|
||||
|
||||
// Reset when sender changes
|
||||
useEffect(() => {
|
||||
@@ -203,7 +209,8 @@ function SenderEmails({ sender }) {
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
setOpenEmail(null);
|
||||
}, [sender.senderId]);
|
||||
clear();
|
||||
}, [sender.senderId, clear]);
|
||||
|
||||
const fetchPage = useCallback((p) => {
|
||||
setLoading(true);
|
||||
@@ -245,6 +252,17 @@ function SenderEmails({ sender }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BulkToolbar
|
||||
selectedIds={selectedIds}
|
||||
onClear={clear}
|
||||
onDone={(action, ids) => {
|
||||
if (action === 'trash' || action === 'archive') {
|
||||
setEmails((prev) => prev.filter((x) => !ids.includes(x.id)));
|
||||
removeIds(ids);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{emails.length > 0 && (
|
||||
<table className="email-list">
|
||||
<tbody>
|
||||
@@ -252,6 +270,8 @@ function SenderEmails({ sender }) {
|
||||
<EmailListRow
|
||||
key={e.id}
|
||||
email={e}
|
||||
selected={selected.has(e.id)}
|
||||
onToggleSelect={toggle}
|
||||
onOpen={setOpenEmail}
|
||||
onRemove={(id) => setEmails((prev) => prev.filter((x) => x.id !== id))}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user