c11d747919
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>
152 lines
7.0 KiB
JavaScript
152 lines
7.0 KiB
JavaScript
import axios from 'axios';
|
|
|
|
// Cookie-based auth (Google OAuth2 session), so send credentials with each call.
|
|
const api = axios.create({ baseURL: '/api/v1', withCredentials: true });
|
|
|
|
api.interceptors.response.use(
|
|
(r) => r,
|
|
(err) => {
|
|
if (err.response?.status === 401) {
|
|
// Not signed in. Send the user to the public landing page (unless already
|
|
// there) so they can read the features and choose to log in.
|
|
if (window.location.pathname !== '/') window.location.href = '/';
|
|
}
|
|
return Promise.reject(err);
|
|
}
|
|
);
|
|
|
|
// Top-level navigation that starts the Google flow and returns to the app.
|
|
export const LOGIN_URL = '/api/v1/auth/login?returnUrl=/app';
|
|
|
|
export const AppApi = {
|
|
info: () => api.get('/app/info').then((r) => r.data)
|
|
};
|
|
|
|
export const AuthApi = {
|
|
me: () => api.get('/auth/me').then((r) => r.data),
|
|
logout: () => api.post('/auth/logout')
|
|
};
|
|
|
|
export const SyncApi = {
|
|
status: () => api.get('/sync/status').then((r) => r.data),
|
|
full: () => api.post('/sync/full'),
|
|
incremental: () => api.post('/sync/incremental')
|
|
};
|
|
|
|
export const AnalyticsApi = {
|
|
dashboard: () => api.get('/analytics/dashboard').then((r) => r.data),
|
|
health: () => api.get('/analytics/health').then((r) => r.data),
|
|
topSenders: (take = 20) => api.get(`/analytics/top-senders?take=${take}`).then((r) => r.data),
|
|
volume: (days = 90) => api.get(`/analytics/volume?days=${days}`).then((r) => r.data),
|
|
heatmap: () => api.get('/analytics/heatmap').then((r) => r.data),
|
|
categoryHeatmap: () => api.get('/analytics/category-heatmap').then((r) => r.data),
|
|
attachments: () => api.get('/analytics/attachments').then((r) => r.data),
|
|
sidebarCounts: () => api.get('/analytics/sidebar-counts').then((r) => r.data),
|
|
// Large take to retrieve all senders for the volume-tiers page.
|
|
allSenders: () => api.get('/analytics/top-senders?take=5000').then((r) => r.data)
|
|
};
|
|
|
|
export const CleanupApi = {
|
|
preview: (req) => api.post('/cleanup/preview', req).then((r) => r.data),
|
|
execute: (req) => api.post('/cleanup/execute', req).then((r) => r.data)
|
|
};
|
|
|
|
// Bulk actions over an explicit set of email IDs (selection-driven, always confirmed —
|
|
// the user already opted in by selecting rows and clicking the action).
|
|
// Numeric values must match CleanupActionType in Domain/Enums/Enums.cs (no string
|
|
// enum converter is configured on the API, so plain numbers are required here).
|
|
const CLEANUP_ACTION = { Archive: 0, Trash: 1, MarkRead: 5, MarkUnread: 6, Star: 7, Unstar: 8 };
|
|
|
|
export const BulkApi = {
|
|
markRead: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.MarkRead, emailIds: ids, confirmed: true }),
|
|
markUnread: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.MarkUnread, emailIds: ids, confirmed: true }),
|
|
star: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Star, emailIds: ids, confirmed: true }),
|
|
unstar: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Unstar, emailIds: ids, confirmed: true }),
|
|
archive: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Archive, emailIds: ids, confirmed: true }),
|
|
trash: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Trash, emailIds: ids, confirmed: true }),
|
|
};
|
|
|
|
export const UnsubscribeApi = {
|
|
detect: () => api.post('/unsubscribe/detect'),
|
|
safeList: () => api.get('/unsubscribe/safe-list').then((r) => r.data),
|
|
process: (req) => api.post('/unsubscribe/process', req).then((r) => r.data)
|
|
};
|
|
|
|
export const LayoutApi = {
|
|
get: () => api.get('/widgetlayout').then((r) => r.data),
|
|
save: (layout) => api.put('/widgetlayout', layout)
|
|
};
|
|
|
|
export const SearchApi = {
|
|
folder: (slug, page = 1, pageSize = 50) =>
|
|
api.post('/search', folderToRequest(slug, page, pageSize)).then((r) => r.data),
|
|
query: (q, page = 1, pageSize = 50) =>
|
|
api.get('/search', { params: { q, page, pageSize } }).then((r) => r.data),
|
|
};
|
|
|
|
function folderToRequest(slug, page, pageSize) {
|
|
const base = { page, pageSize };
|
|
switch (slug) {
|
|
// ── Mailbox ──
|
|
case 'inbox': return { ...base, isInInbox: true, isTrashed: false };
|
|
case 'allmail': return { ...base };
|
|
case 'unread': return { ...base, isUnread: true };
|
|
case 'starred': return { ...base, isStarred: true };
|
|
case 'sent': return { ...base, gmailLabel: 'SENT' };
|
|
case 'drafts': return { ...base, gmailLabel: 'DRAFT' };
|
|
case 'archive': return { ...base, isInInbox: false, isTrashed: false };
|
|
case 'spam': return { ...base, gmailLabel: 'SPAM' };
|
|
case 'trash': return { ...base, isTrashed: true };
|
|
// ── Special filters ──
|
|
case 'large': return { ...base, minSizeBytes: 5_000_000 };
|
|
case 'old': {
|
|
const d = new Date(); d.setFullYear(d.getFullYear() - 1);
|
|
return { ...base, to: d.toISOString().slice(0, 10) };
|
|
}
|
|
// ── Smart folders ──
|
|
case 'automated': return { ...base, category: 'Notification' };
|
|
case 'finance': return { ...base, category: 'Finance' };
|
|
case 'social': return { ...base, category: 'Social' };
|
|
case 'shopping': return { ...base, category: 'Shopping' };
|
|
case 'noreply': return { ...base, query: 'from:noreply' };
|
|
case 'gaming': return { ...base, category: 'Gaming' };
|
|
case 'sales': return { ...base, category: 'SeasonalSales' };
|
|
case 'ridesharing': return { ...base, category: 'RideSharing' };
|
|
case 'food': return { ...base, category: 'FoodDelivery' };
|
|
case 'wellness': return { ...base, category: 'Wellness' };
|
|
// New categories
|
|
case 'travel': return { ...base, category: 'Travel' };
|
|
case 'subscriptions': return { ...base, category: 'Subscriptions' };
|
|
case 'parcels': return { ...base, category: 'Parcels' };
|
|
case 'recruitment': return { ...base, category: 'Recruitment' };
|
|
case 'events': return { ...base, category: 'Events' };
|
|
case 'security': return { ...base, category: 'SecurityAlerts' };
|
|
case 'healthcare': return { ...base, category: 'Healthcare' };
|
|
case 'education': return { ...base, category: 'Education' };
|
|
case 'news': return { ...base, category: 'NewsMedia' };
|
|
case 'property': return { ...base, category: 'PropertyUtilities' };
|
|
case 'charity': return { ...base, category: 'Charity' };
|
|
case 'government': return { ...base, category: 'Government' };
|
|
case 'crypto': return { ...base, category: 'CryptoInvesting' };
|
|
case 'family': return { ...base, category: 'FamilySchool' };
|
|
default: return { ...base };
|
|
}
|
|
}
|
|
|
|
export const EmailApi = {
|
|
get: (id) => api.get(`/email/${id}`).then((r) => r.data),
|
|
markRead: (id) => api.post(`/email/${id}/read`),
|
|
markUnread: (id) => api.post(`/email/${id}/unread`),
|
|
star: (id) => api.post(`/email/${id}/star`),
|
|
unstar: (id) => api.post(`/email/${id}/unstar`),
|
|
trash: (id) => api.post(`/email/${id}/trash`),
|
|
untrash: (id) => api.post(`/email/${id}/untrash`),
|
|
unsubscribe:(id) => api.post(`/email/${id}/unsubscribe`).then((r) => r.data),
|
|
};
|
|
|
|
export const ExportApi = {
|
|
reportUrl: (format) => `/api/v1/export/report?format=${format}`
|
|
};
|
|
|
|
export default api;
|