diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 842aa31c5..89212d63d 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -30,37 +30,50 @@ /> @@ -213,6 +226,73 @@ const MAILBOX_ICONS = { important: Bookmark, } +// Drag and drop +const draggedItem = ref(null) +const dropTargetId = ref(null) + +const handleDragStart = (mailboxId: string) => (e: DragEvent) => { + draggedItem.value = mailboxId + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData('text/plain', mailboxId) + } +} + +const handleDragEnd = () => { + draggedItem.value = null + dropTargetId.value = null +} + +const handleDragOver = (mailboxId: string) => (e: DragEvent) => { + if (draggedItem.value && draggedItem.value !== mailboxId) { + e.preventDefault() + if (e.dataTransfer) e.dataTransfer.dropEffect = 'move' + + dropTargetId.value = mailboxId + } +} + +const handleDragLeave = () => (dropTargetId.value = null) + +const handleDrop = (targetMailboxId: string) => (e: DragEvent) => { + e.preventDefault() + if (draggedItem.value && draggedItem.value !== targetMailboxId) { + const draggedIndex = mailboxes.data.findIndex((m) => m.id === draggedItem.value) + const targetIndex = + targetMailboxId === 'starred' + ? mailboxes.data.length - 1 + : mailboxes.data.findIndex((m) => m.id === targetMailboxId) + if (draggedIndex + 1 === targetIndex) { + dropTargetId.value = null + draggedItem.value = null + return + } + updateMailboxPosition.submit({ + target_mailbox_id: draggedItem.value, + prior_mailbox_id: + targetIndex === 0 + ? null + : targetMailboxId === 'starred' + ? mailboxes.data.at(-1).id + : mailboxes.data[targetIndex - 1].id, + }) + } + dropTargetId.value = null + draggedItem.value = null +} + +const updateMailboxPosition = createResource({ + url: 'mail.client.doctype.mailbox.mailbox.update_mailbox_position', + makeParams: ({ + target_mailbox_id, + prior_mailbox_id, + }: { + target_mailbox_id: string + prior_mailbox_id: string | null + }) => ({ user: user.data.name, target_mailbox_id, prior_mailbox_id }), + onSuccess: () => mailboxes.reload(), +}) + const sidebarItems = computed(() => { if (route.meta.isDashboard) return dashboardItems @@ -227,6 +307,14 @@ const sidebarItems = computed(() => { to: { name: 'Mailbox', params: { mailbox: mailbox.id } }, suffix: mailbox.unread_threads ? String(mailbox.unread_threads) : '', activeFor: [mailbox.id], + draggable: true, + isDragging: draggedItem.value === mailbox.id, + isDropTarget: dropTargetId.value === mailbox.id, + onDragStart: handleDragStart(mailbox.id), + onDragEnd: handleDragEnd, + onDragOver: handleDragOver(mailbox.id), + onDragLeave: handleDragLeave, + onDrop: handleDrop(mailbox.id), menuOptions: [ { label: __('Edit Folder'), @@ -251,6 +339,10 @@ const sidebarItems = computed(() => { icon: Star, to: { name: 'Mailbox', params: { mailbox: 'starred' } }, activeFor: ['starred'], + isDropTarget: dropTargetId.value === 'starred', + onDragOver: handleDragOver('starred'), + onDragLeave: handleDragLeave, + onDrop: handleDrop('starred'), } const addMailboxItem = { @@ -260,7 +352,7 @@ const sidebarItems = computed(() => { } return mailboxes.data?.length - ? [{ items: [mailboxItems[0], starredItem, ...mailboxItems.slice(1), addMailboxItem] }] + ? [{ items: [...mailboxItems, starredItem, addMailboxItem] }] : [] }) diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 9d004918a..61964472b 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -53,11 +53,11 @@ const registerServiceWorker = async () => { } router.isReady().then(async () => { - if (import.meta.env.DEV) - await frappeRequest({ url: '/api/method/mail.www.mail.get_context_for_dev' }).then( - (values) => Object.keys(values).forEach((key) => (window[key] = values[key])), - ) + // if (import.meta.env.DEV) + // await frappeRequest({ url: '/api/method/mail.www.mail.get_context_for_dev' }).then( + // (values) => Object.keys(values).forEach((key) => (window[key] = values[key])), + // ) - registerServiceWorker() + // registerServiceWorker() app.mount('#app') }) diff --git a/frontend/src/types/doctypes.ts b/frontend/src/types/doctypes.ts index e31fffaaf..9b058519e 100644 --- a/frontend/src/types/doctypes.ts +++ b/frontend/src/types/doctypes.ts @@ -1,12 +1,9 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - interface DocType { name: string creation: string modified: string owner: string modified_by: string - docstatus: 0 | 1 | 2 } interface ChildDocType extends DocType { @@ -388,7 +385,7 @@ export interface MailMessageMailbox extends ChildDocType { mailbox_name: string } -// Last updated: 2025-12-09 13:11:05.006924 +// Last updated: 2025-12-17 10:54:02.407375 export interface Identity extends DocType { /** May Delete: Check */ may_delete: 0 | 1 @@ -420,7 +417,7 @@ export interface MailSignature extends DocType { html_body?: string } -// Last updated: 2025-12-09 12:55:32.269456 +// Last updated: 2025-12-15 11:47:17.806197 export interface VacationResponse extends DocType { /** User: Link (User) */ user: string diff --git a/mail/api/mail.py b/mail/api/mail.py index 07119d1ca..eac61284a 100644 --- a/mail/api/mail.py +++ b/mail/api/mail.py @@ -20,6 +20,7 @@ set_spam_status, ) from mail.client.doctype.mail_queue.mail_queue import MailQueue +from mail.client.doctype.mailbox.mailbox import update_mailbox from mail.jmap import get_mailbox_id_by_role from mail.utils import convert_html_to_text from mail.utils.user import has_role @@ -33,11 +34,9 @@ def get_mailboxes() -> list[dict]: if not has_role(user, "Mail User") or user == "Administrator": return [] - fields = ["id", "_name", "role", "total_threads", "unread_threads"] + fields = ["id", "_name", "role", "total_threads", "unread_threads", "sort_order"] mailboxes = get_user_mailboxes(user) - return [ - {field: mailbox[field] for field in fields} for mailbox in mailboxes if mailbox["subscribed"] == 1 - ] + return [{field: mailbox[field] for field in fields} for mailbox in mailboxes] def get_user_mailboxes(user) -> list[dict]: @@ -539,3 +538,10 @@ def normalize_search_filter(filter: dict) -> dict: def parse_date_to_utc_iso(date_str: str) -> str: """Parse date string and convert to ISO format with UTC timezone.""" return datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC).isoformat() + + +@frappe.whitelist() +def update_mailbox_sort_order(mailboxes: dict) -> None: + """Updates mailbox sort order of the given mailboxes.""" + + print(mailboxes)