courrier multi account restore compose
This commit is contained in:
parent
9c62add6d2
commit
a18751fe2a
@ -562,32 +562,62 @@ export default function CourrierPage() {
|
||||
setShowComposeModal(true);
|
||||
};
|
||||
|
||||
// Update handleMailboxChange to properly handle per-account folders
|
||||
// Update handleMailboxChange to ensure consistent folder naming and prevent race conditions
|
||||
const handleMailboxChange = (folder: string, accountId?: string) => {
|
||||
if (accountId && accountId !== 'loading-account') {
|
||||
const account = accounts.find(a => a.id === accountId);
|
||||
if (!account) {
|
||||
toast({
|
||||
title: "Account not found",
|
||||
description: `The account ${accountId} could not be found.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Only allow navigation to folders in selectedAccount.folders
|
||||
if (!account.folders.includes(folder)) {
|
||||
toast({
|
||||
title: "Folder not found",
|
||||
description: `The folder ${folder} does not exist for this account.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedFolders(prev => ({ ...prev, [accountId]: folder }));
|
||||
changeFolder(folder, accountId);
|
||||
} else {
|
||||
changeFolder(folder, accountId);
|
||||
if (!accountId || accountId === 'loading-account') {
|
||||
// Use a default behavior if no valid accountId is provided
|
||||
console.warn('No valid accountId provided for folder change');
|
||||
changeFolder(folder);
|
||||
return;
|
||||
}
|
||||
|
||||
const account = accounts.find(a => a.id === accountId);
|
||||
if (!account) {
|
||||
toast({
|
||||
title: "Account not found",
|
||||
description: `The account ${accountId} could not be found.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure folder has account prefix
|
||||
const prefixedFolder = folder.includes(':') ? folder : `${accountId}:${folder}`;
|
||||
|
||||
// Ensure folder exists in account.folders (either in prefixed or unprefixed form)
|
||||
const folderExists = account.folders?.some(f =>
|
||||
f === prefixedFolder || f === folder ||
|
||||
// Handle case where folder is base name and account folders are prefixed
|
||||
(folder.includes(':') ? false : `${accountId}:${folder}` === f)
|
||||
);
|
||||
|
||||
if (!folderExists && folder !== 'INBOX') {
|
||||
toast({
|
||||
title: "Folder not found",
|
||||
description: `The folder ${folder} does not exist for this account.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI state first to prevent flickering
|
||||
setSelectedAccount(account);
|
||||
|
||||
// Use a callback to ensure we have the latest state when updating selectedFolders
|
||||
setSelectedFolders(prev => {
|
||||
const updated = { ...prev, [accountId]: prefixedFolder };
|
||||
console.log('Updated selectedFolders:', updated);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Set loading state to provide feedback
|
||||
setLoading(true);
|
||||
|
||||
// Reset page when changing folders
|
||||
setPage(1);
|
||||
|
||||
// Make sure we pass the prefixed folder to change folder
|
||||
changeFolder(prefixedFolder, accountId);
|
||||
};
|
||||
|
||||
// Update the folder button rendering to show selected state based on account
|
||||
@ -899,168 +929,4 @@ export default function CourrierPage() {
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{searchQuery
|
||||
? `No results found for "${searchQuery}"`
|
||||
: `Your ${currentFolder.toLowerCase()} is empty`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<EmailList
|
||||
emails={emails}
|
||||
selectedEmailIds={selectedEmailIds}
|
||||
selectedEmail={selectedEmail}
|
||||
onSelectEmail={(emailId) => handleEmailSelect(emailId, selectedAccount?.id || '', currentFolder)}
|
||||
onToggleSelect={toggleEmailSelection}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
onToggleStarred={toggleStarred}
|
||||
onLoadMore={handleLoadMore}
|
||||
hasMoreEmails={page < totalPages}
|
||||
currentFolder={currentFolder}
|
||||
isLoading={isLoading}
|
||||
totalEmails={emails.length}
|
||||
onBulkAction={handleBulkAction}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Panel 3: Email Detail - Always visible */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Content for Panel 3 based on state but always visible */}
|
||||
<div className="flex-1 overflow-hidden bg-white">
|
||||
{selectedEmail ? (
|
||||
<EmailDetailView
|
||||
email={selectedEmail as any}
|
||||
onBack={() => {
|
||||
handleEmailSelect('', '', '');
|
||||
// Ensure sidebar stays visible
|
||||
setSidebarOpen(true);
|
||||
}}
|
||||
onReply={handleReply}
|
||||
onReplyAll={handleReplyAll}
|
||||
onForward={handleForward}
|
||||
onToggleStar={() => toggleStarred(selectedEmail.id)}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>Select an email to view or</p>
|
||||
<button
|
||||
className="text-primary mt-2 hover:underline"
|
||||
onClick={() => {
|
||||
setComposeType('new');
|
||||
setShowComposeModal(true);
|
||||
}}
|
||||
>
|
||||
Compose a new message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Modals and Dialogs */}
|
||||
<DeleteConfirmDialog
|
||||
show={showDeleteConfirm}
|
||||
selectedCount={selectedEmailIds.length}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
/>
|
||||
|
||||
{/* Compose Email Dialog */}
|
||||
<Dialog open={showComposeModal} onOpenChange={(open) => !open && setShowComposeModal(false)}>
|
||||
<DialogContent className="sm:max-w-[800px] p-0 h-[80vh]">
|
||||
<DialogTitle asChild>
|
||||
<span className="sr-only">New Message</span>
|
||||
</DialogTitle>
|
||||
<ComposeEmail
|
||||
type={composeType}
|
||||
initialEmail={composeType !== 'new' ? selectedEmail : undefined}
|
||||
onSend={(emailData) => {
|
||||
const result = sendEmail(emailData);
|
||||
return result;
|
||||
}}
|
||||
onClose={() => setShowComposeModal(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Password Modal */}
|
||||
<Dialog open={showEditModal} onOpenChange={open => { if (!open) setShowEditModal(false); }}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogTitle>Edit Account Password</DialogTitle>
|
||||
<form onSubmit={async e => {
|
||||
e.preventDefault();
|
||||
if (!accountToEdit) return;
|
||||
setEditLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/courrier/account', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accountId: accountToEdit.id, newPassword }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to update password');
|
||||
toast({ title: 'Password updated', description: 'Password changed successfully.' });
|
||||
setShowEditModal(false);
|
||||
setNewPassword('');
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to update password', variant: 'destructive' });
|
||||
} finally {
|
||||
setEditLoading(false);
|
||||
}
|
||||
}}>
|
||||
<div className="mb-2">
|
||||
<Label htmlFor="new-password">New Password</Label>
|
||||
<Input id="new-password" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required className="mt-1" />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)}>Cancel</Button>
|
||||
<Button type="submit" disabled={editLoading}>{editLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Save'}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Account Dialog */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={open => { if (!open) setShowDeleteDialog(false); }}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Account</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this account? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setShowDeleteDialog(false)}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="destructive" disabled={deleteLoading} onClick={async () => {
|
||||
if (!accountToDelete) return;
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/courrier/account?accountId=${accountToDelete.id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to delete account');
|
||||
toast({ title: 'Account deleted', description: 'The account was deleted successfully.' });
|
||||
setShowDeleteDialog(false);
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to delete account', variant: 'destructive' });
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
}}>Delete</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
: `Your ${currentFolder.toLowerCase()} is empty`
|
||||
105
app/hooks/use-courrier.ts
Normal file
105
app/hooks/use-courrier.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Change the current folder and load emails from that folder
|
||||
*/
|
||||
const changeFolder = async (folder: string, accountId?: string) => {
|
||||
console.log(`Changing folder to ${folder} for account ${accountId || 'default'}`);
|
||||
try {
|
||||
// Reset selected email
|
||||
setSelectedEmail(null);
|
||||
setSelectedEmailIds([]);
|
||||
|
||||
// Record the new folder
|
||||
setCurrentFolder(folder);
|
||||
|
||||
// Reset search query when changing folders
|
||||
setSearchQuery('');
|
||||
|
||||
// Reset to page 1
|
||||
setPage(1);
|
||||
|
||||
// Clear existing emails before loading new ones to prevent UI flicker
|
||||
setEmails([]);
|
||||
|
||||
// Show loading state
|
||||
setIsLoading(true);
|
||||
|
||||
// Load emails for the new folder with a deliberate delay to allow state to update
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await loadEmails(folder, 1, 20, accountId);
|
||||
} catch (error) {
|
||||
console.error(`Error changing to folder ${folder}:`, error);
|
||||
setError(`Failed to load emails from ${folder}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load emails for the current folder
|
||||
*/
|
||||
const loadEmails = async (
|
||||
folderOverride?: string,
|
||||
pageOverride?: number,
|
||||
perPageOverride?: number,
|
||||
accountIdOverride?: string
|
||||
) => {
|
||||
const folderToUse = folderOverride || currentFolder;
|
||||
const pageToUse = pageOverride || page;
|
||||
const perPageToUse = perPageOverride || perPage;
|
||||
const accountIdToUse = accountIdOverride !== undefined ? accountIdOverride :
|
||||
folderToUse.includes(':') ? folderToUse.split(':')[0] : undefined;
|
||||
|
||||
console.log(`Loading emails: folder=${folderToUse}, page=${pageToUse}, accountId=${accountIdToUse || 'default'}`);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
// Construct the API URL with a unique timestamp to prevent caching
|
||||
let url = `/api/courrier/emails?folder=${encodeURIComponent(folderToUse)}&page=${pageToUse}&perPage=${perPageToUse}`;
|
||||
|
||||
// Add accountId parameter if specified
|
||||
if (accountIdToUse) {
|
||||
url += `&accountId=${encodeURIComponent(accountIdToUse)}`;
|
||||
}
|
||||
|
||||
// Add cache-busting timestamp
|
||||
url += `&_t=${Date.now()}`;
|
||||
|
||||
console.log(`Fetching emails from API: ${url}`);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorText;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorText = errorData.error || `Server error: ${response.status}`;
|
||||
} catch {
|
||||
errorText = `HTTP error: ${response.status}`;
|
||||
}
|
||||
throw new Error(errorText);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (pageOverride === 1 || !pageOverride) {
|
||||
// Replace emails when loading first page
|
||||
setEmails(data.emails);
|
||||
} else {
|
||||
// Append emails when loading subsequent pages
|
||||
setEmails(prev => [...prev, ...data.emails]);
|
||||
}
|
||||
|
||||
// Update pagination info
|
||||
setTotalPages(data.totalPages);
|
||||
setMailboxes(data.mailboxes || []);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error loading emails:', error);
|
||||
setError(`Failed to load emails: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
@ -140,18 +140,20 @@ export default function EmailSidebar({
|
||||
return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
// Render folder button with exact same styling as in courrier page
|
||||
// Improve the renderFolderButton function to ensure consistent handling
|
||||
const renderFolderButton = (folder: string, accountId: string) => {
|
||||
// Get the account prefix from the folder name
|
||||
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
|
||||
// Ensure folder always has accountId prefix for consistency
|
||||
const prefixedFolder = folder.includes(':') ? folder : `${accountId}:${folder}`;
|
||||
|
||||
// Extract the base folder name and account ID for display and checking
|
||||
const [folderAccountId, baseFolder] = prefixedFolder.includes(':')
|
||||
? prefixedFolder.split(':')
|
||||
: [accountId, folder];
|
||||
|
||||
// Only show folders that belong to this account
|
||||
if (folderAccountId !== accountId) return null;
|
||||
|
||||
const isSelected = selectedFolders[accountId] === folder;
|
||||
|
||||
// Get the base folder name for display
|
||||
const baseFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
||||
const isSelected = selectedFolders[accountId] === prefixedFolder;
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@ -252,6 +252,43 @@ export const useCourrier = () => {
|
||||
}
|
||||
}, [currentFolder, page, perPage, searchQuery, session?.user?.id, toast]);
|
||||
|
||||
// Change folder and load emails from that folder
|
||||
const changeFolder = useCallback(async (folder: string, accountId?: string) => {
|
||||
console.log(`Changing folder to ${folder} for account ${accountId || 'default'}`);
|
||||
try {
|
||||
// Reset selected email
|
||||
setSelectedEmail(null);
|
||||
setSelectedEmailIds([]);
|
||||
|
||||
// Record the new folder
|
||||
setCurrentFolder(folder);
|
||||
|
||||
// Reset search query when changing folders
|
||||
setSearchQuery('');
|
||||
|
||||
// Reset to page 1
|
||||
setPage(1);
|
||||
|
||||
// Clear existing emails before loading new ones to prevent UI flicker
|
||||
setEmails([]);
|
||||
|
||||
// Show loading state
|
||||
setIsLoading(true);
|
||||
|
||||
// Use a small delay to ensure state updates have propagated
|
||||
// This helps prevent race conditions when multiple folders are clicked quickly
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Call loadEmails with correct boolean parameter type
|
||||
await loadEmails(false, accountId);
|
||||
} catch (error) {
|
||||
console.error(`Error changing to folder ${folder}:`, error);
|
||||
setError(error instanceof Error ? error.message : 'Unknown error');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [loadEmails]);
|
||||
|
||||
// Load emails when folder or page changes
|
||||
useEffect(() => {
|
||||
if (session?.user?.id) {
|
||||
@ -528,19 +565,6 @@ export const useCourrier = () => {
|
||||
}
|
||||
}, [emails, selectedEmailIds]);
|
||||
|
||||
// Change folder and load emails
|
||||
const changeFolder = useCallback((folder: string, accountId?: string) => {
|
||||
setCurrentFolder(folder);
|
||||
setPage(1); // Reset to first page when changing folders
|
||||
setSelectedEmail(null);
|
||||
setSelectedEmailIds([]);
|
||||
|
||||
// Load the emails for this folder with the correct accountId
|
||||
if (session?.user?.id) {
|
||||
loadEmails(false, accountId);
|
||||
}
|
||||
}, [session?.user?.id, loadEmails]);
|
||||
|
||||
// Search emails
|
||||
const searchEmails = useCallback((query: string) => {
|
||||
setSearchQuery(query);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user