feat(folders): drag-and-drop reorder for all folders
This commit is contained in:
@@ -17,7 +17,16 @@ import {
|
|||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn, buildMailboxTree, type MailboxNode } from '@/lib/utils';
|
import { cn, buildMailboxTree, type MailboxNode } from '@/lib/utils';
|
||||||
import { ChevronRight, ChevronDown } from 'lucide-react';
|
import { ChevronRight, ChevronDown, GripVertical } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
DndContext, closestCenter, PointerSensor, KeyboardSensor,
|
||||||
|
useSensor, useSensors, type DragEndEvent,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
SortableContext, verticalListSortingStrategy, useSortable,
|
||||||
|
arrayMove, sortableKeyboardCoordinates,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
|
||||||
const STANDARD_ROLES = ['inbox', 'drafts', 'sent', 'trash', 'junk', 'archive'] as const;
|
const STANDARD_ROLES = ['inbox', 'drafts', 'sent', 'trash', 'junk', 'archive'] as const;
|
||||||
|
|
||||||
@@ -102,10 +111,47 @@ function IconPicker({ currentIcon, onSelect, onClose }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a folder row with a drag handle so it can be reordered within its
|
||||||
|
* sibling group. The handle carries the dnd-kit listeners; the rest of the row
|
||||||
|
* (buttons, inline editors) stays fully interactive.
|
||||||
|
*/
|
||||||
|
function SortableFolderRow({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
zIndex: isDragging ? 10 : undefined,
|
||||||
|
position: isDragging ? 'relative' : undefined,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style} className="flex items-stretch">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="flex items-center px-1 text-muted-foreground/40 hover:text-foreground cursor-grab active:cursor-grabbing touch-none rounded-md focus:outline-none focus:ring-2 focus:ring-ring flex-shrink-0"
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
>
|
||||||
|
<GripVertical className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<div className="flex-1 min-w-0">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function FolderSettings() {
|
export function FolderSettings() {
|
||||||
const t = useTranslations('settings.folders');
|
const t = useTranslations('settings.folders');
|
||||||
const { client } = useAuthStore();
|
const { client } = useAuthStore();
|
||||||
const { mailboxes, fetchMailboxes, createMailbox, renameMailbox, deleteMailbox, setMailboxRole } = useEmailStore();
|
const { mailboxes, fetchMailboxes, createMailbox, renameMailbox, deleteMailbox, setMailboxRole, reorderMailboxes } = useEmailStore();
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
// Small activation distance so clicking the row's buttons still works.
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||||
|
);
|
||||||
const { folderIcons, setFolderIcon } = useSettingsStore();
|
const { folderIcons, setFolderIcon } = useSettingsStore();
|
||||||
const { isFeatureEnabled } = usePolicyStore();
|
const { isFeatureEnabled } = usePolicyStore();
|
||||||
const folderIconsAllowed = isFeatureEnabled('folderIconsEnabled');
|
const folderIconsAllowed = isFeatureEnabled('folderIconsEnabled');
|
||||||
@@ -129,6 +175,31 @@ export function FolderSettings() {
|
|||||||
const ownMailboxes = mailboxes.filter(mb => !mb.isShared);
|
const ownMailboxes = mailboxes.filter(mb => !mb.isShared);
|
||||||
const folderTree = buildMailboxTree(ownMailboxes);
|
const folderTree = buildMailboxTree(ownMailboxes);
|
||||||
|
|
||||||
|
// Reorder folders within a sibling group (same parent). Drops onto a folder
|
||||||
|
// in a different group are ignored — this reorders, it doesn't reparent.
|
||||||
|
const handleFolderDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over || active.id === over.id || !client) return;
|
||||||
|
|
||||||
|
const groups: MailboxNode[][] = [];
|
||||||
|
const collectGroups = (nodes: MailboxNode[]) => {
|
||||||
|
groups.push(nodes);
|
||||||
|
nodes.forEach(n => { if (n.children.length > 0) collectGroups(n.children); });
|
||||||
|
};
|
||||||
|
collectGroups(folderTree);
|
||||||
|
|
||||||
|
const group = groups.find(g => g.some(n => n.id === active.id));
|
||||||
|
if (!group) return;
|
||||||
|
const oldIndex = group.findIndex(n => n.id === active.id);
|
||||||
|
const newIndex = group.findIndex(n => n.id === over.id);
|
||||||
|
if (newIndex < 0) return; // dropped outside the active folder's sibling group
|
||||||
|
|
||||||
|
const orderedIds = arrayMove(group, oldIndex, newIndex).map(n => n.id);
|
||||||
|
reorderMailboxes(client, orderedIds).catch(() => {
|
||||||
|
toast.error(t('reorder_error'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getRoleMailboxId = (role: string): string => {
|
const getRoleMailboxId = (role: string): string => {
|
||||||
const mb = ownMailboxes.find(m => m.role === role);
|
const mb = ownMailboxes.find(m => m.role === role);
|
||||||
return mb?.id ?? '';
|
return mb?.id ?? '';
|
||||||
@@ -385,6 +456,7 @@ export function FolderSettings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={mb.id}>
|
<div key={mb.id}>
|
||||||
|
<SortableFolderRow id={mb.id} title={t('reorder')}>
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between py-2 px-3 rounded-md hover:bg-muted/50"
|
className="flex items-center justify-between py-2 px-3 rounded-md hover:bg-muted/50"
|
||||||
style={{ paddingLeft: 12 + depth * 16 }}
|
style={{ paddingLeft: 12 + depth * 16 }}
|
||||||
@@ -482,12 +554,15 @@ export function FolderSettings() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</SortableFolderRow>
|
||||||
{/* Inline subfolder creation */}
|
{/* Inline subfolder creation */}
|
||||||
{renderCreateInline(mb.id, depth + 1)}
|
{renderCreateInline(mb.id, depth + 1)}
|
||||||
{/* Render children if expanded */}
|
{/* Render children if expanded */}
|
||||||
{hasChildren && isExpanded && (
|
{hasChildren && isExpanded && (
|
||||||
<div>
|
<div>
|
||||||
{node.children.map(child => renderFolderNode(child))}
|
<SortableContext items={node.children.map(c => c.id)} strategy={verticalListSortingStrategy}>
|
||||||
|
{node.children.map(child => renderFolderNode(child))}
|
||||||
|
</SortableContext>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -505,7 +580,11 @@ export function FolderSettings() {
|
|||||||
<p className="text-sm text-muted-foreground">{t('no_folders')}</p>
|
<p className="text-sm text-muted-foreground">{t('no_folders')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
folderTree.map(node => renderFolderNode(node))
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleFolderDragEnd}>
|
||||||
|
<SortableContext items={folderTree.map(n => n.id)} strategy={verticalListSortingStrategy}>
|
||||||
|
{folderTree.map(node => renderFolderNode(node))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+10
-7
@@ -486,25 +486,28 @@ export function buildMailboxTree(mailboxes: Mailbox[]): MailboxNode[] {
|
|||||||
return a.isShared ? 1 : -1;
|
return a.isShared ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Priority: Role-based ordering (inbox first, trash last, etc.)
|
// 2. Priority: user-defined order. When a folder has been explicitly
|
||||||
|
// reordered its sortOrder is non-zero and takes precedence over the
|
||||||
|
// default role/name ordering below. Untouched folders keep sortOrder 0,
|
||||||
|
// so the default arrangement is unchanged until the user drags something.
|
||||||
|
if (a.sortOrder !== b.sortOrder) {
|
||||||
|
return a.sortOrder - b.sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Priority: Role-based ordering (inbox first, trash last, etc.)
|
||||||
const aPriority = a.role ? (ROLE_PRIORITY[a.role] ?? 999) : 999;
|
const aPriority = a.role ? (ROLE_PRIORITY[a.role] ?? 999) : 999;
|
||||||
const bPriority = b.role ? (ROLE_PRIORITY[b.role] ?? 999) : 999;
|
const bPriority = b.role ? (ROLE_PRIORITY[b.role] ?? 999) : 999;
|
||||||
if (aPriority !== bPriority) {
|
if (aPriority !== bPriority) {
|
||||||
return aPriority - bPriority;
|
return aPriority - bPriority;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Priority: Year folders (e.g., "2025", "2024") sorted numerically descending
|
// 4. Priority: Year folders (e.g., "2025", "2024") sorted numerically descending
|
||||||
const aIsYear = /^\d{4}$/.test(a.name);
|
const aIsYear = /^\d{4}$/.test(a.name);
|
||||||
const bIsYear = /^\d{4}$/.test(b.name);
|
const bIsYear = /^\d{4}$/.test(b.name);
|
||||||
if (aIsYear && bIsYear) {
|
if (aIsYear && bIsYear) {
|
||||||
return parseInt(b.name) - parseInt(a.name); // Descending: 2025, 2024, 2023...
|
return parseInt(b.name) - parseInt(a.name); // Descending: 2025, 2024, 2023...
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Fallback: Server sortOrder
|
|
||||||
if (a.sortOrder !== b.sortOrder) {
|
|
||||||
return a.sortOrder - b.sortOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Fallback: Alphabetical by name
|
// 5. Fallback: Alphabetical by name
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1571,6 +1571,8 @@
|
|||||||
"role_none": "None",
|
"role_none": "None",
|
||||||
"create_folder": "Create Folder",
|
"create_folder": "Create Folder",
|
||||||
"create_subfolder": "Create Subfolder",
|
"create_subfolder": "Create Subfolder",
|
||||||
|
"reorder": "Drag to reorder",
|
||||||
|
"reorder_error": "Failed to reorder folders",
|
||||||
"subfolder_of": "Inside {name}",
|
"subfolder_of": "Inside {name}",
|
||||||
"subfolder_name": "Subfolder name",
|
"subfolder_name": "Subfolder name",
|
||||||
"new_folder_name": "Folder name",
|
"new_folder_name": "Folder name",
|
||||||
|
|||||||
@@ -1528,6 +1528,8 @@
|
|||||||
"role_none": "אין",
|
"role_none": "אין",
|
||||||
"create_folder": "צור תיקיה",
|
"create_folder": "צור תיקיה",
|
||||||
"create_subfolder": "צור תיקיית משנה",
|
"create_subfolder": "צור תיקיית משנה",
|
||||||
|
"reorder": "גרור לשינוי הסדר",
|
||||||
|
"reorder_error": "שינוי סדר התיקיות נכשל",
|
||||||
"subfolder_of": "בתוך{name}",
|
"subfolder_of": "בתוך{name}",
|
||||||
"subfolder_name": "שם תיקיית משנה",
|
"subfolder_name": "שם תיקיית משנה",
|
||||||
"new_folder_name": "שם התיקיה",
|
"new_folder_name": "שם התיקיה",
|
||||||
|
|||||||
Generated
+56
-11
@@ -9,6 +9,9 @@
|
|||||||
"version": "1.7.7",
|
"version": "1.7.7",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@tanstack/react-virtual": "^3.13.24",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@tiptap/extension-color": "^3.22.4",
|
"@tiptap/extension-color": "^3.22.4",
|
||||||
"@tiptap/extension-image": "^3.22.4",
|
"@tiptap/extension-image": "^3.22.4",
|
||||||
@@ -571,6 +574,59 @@
|
|||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.2",
|
"version": "1.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||||
@@ -6223,7 +6279,6 @@
|
|||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -7904,16 +7959,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next-intl/node_modules/@swc/helpers": {
|
|
||||||
"version": "0.5.23",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
|
|
||||||
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
|
|||||||
@@ -33,6 +33,9 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@tanstack/react-virtual": "^3.13.24",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@tiptap/extension-color": "^3.22.4",
|
"@tiptap/extension-color": "^3.22.4",
|
||||||
"@tiptap/extension-image": "^3.22.4",
|
"@tiptap/extension-image": "^3.22.4",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ interface EmailStore {
|
|||||||
renameMailbox: (client: IJMAPClient, mailboxId: string, name: string) => Promise<void>;
|
renameMailbox: (client: IJMAPClient, mailboxId: string, name: string) => Promise<void>;
|
||||||
deleteMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
deleteMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||||
setMailboxRole: (client: IJMAPClient, mailboxId: string, role: string | null) => Promise<void>;
|
setMailboxRole: (client: IJMAPClient, mailboxId: string, role: string | null) => Promise<void>;
|
||||||
|
reorderMailboxes: (client: IJMAPClient, orderedIds: string[]) => Promise<void>;
|
||||||
emptyMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
emptyMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
|
||||||
markMailboxAsRead: (client: IJMAPClient, mailboxId: string) => Promise<number>;
|
markMailboxAsRead: (client: IJMAPClient, mailboxId: string) => Promise<number>;
|
||||||
|
|
||||||
@@ -3227,6 +3228,45 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reorderMailboxes: async (client, orderedIds) => {
|
||||||
|
// Assign a 1-based sortOrder to the given sibling group in its new order.
|
||||||
|
// sortOrder is the primary sort key (see buildMailboxTree), so this pins
|
||||||
|
// the folders' arrangement in the sidebar and settings.
|
||||||
|
const updates = orderedIds.map((id, idx) => ({ id, sortOrder: idx + 1 }));
|
||||||
|
const applyLocal = (list: Mailbox[]) =>
|
||||||
|
list.map(mb => {
|
||||||
|
const u = updates.find(x => x.id === mb.id);
|
||||||
|
return u ? { ...mb, sortOrder: u.sortOrder } : mb;
|
||||||
|
});
|
||||||
|
// Optimistic local update so the reorder is reflected immediately.
|
||||||
|
const viewingId = get().viewingAccountId;
|
||||||
|
if (viewingId) {
|
||||||
|
set((state) => ({
|
||||||
|
accountMailboxes: {
|
||||||
|
...state.accountMailboxes,
|
||||||
|
[viewingId]: applyLocal(state.accountMailboxes[viewingId] ?? []),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set({ mailboxes: applyLocal(get().mailboxes) });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const effectiveClient = resolveActionClient(client);
|
||||||
|
for (const u of updates) {
|
||||||
|
await effectiveClient.updateMailbox(u.id, { sortOrder: u.sortOrder });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error instanceof Error ? error.message : 'Failed to reorder folders' });
|
||||||
|
// Re-sync from server so local state doesn't drift from persisted order.
|
||||||
|
if (viewingId) {
|
||||||
|
await refreshMailboxesForViewingAccount(client);
|
||||||
|
} else {
|
||||||
|
await get().fetchMailboxes(client);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
emptyMailbox: async (client, mailboxId) => {
|
emptyMailbox: async (client, mailboxId) => {
|
||||||
try {
|
try {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
|
|||||||
Reference in New Issue
Block a user