From 4546d0b6bd7924a481ecabf1466bd3a2301fb755 Mon Sep 17 00:00:00 2001 From: krantheman Date: Fri, 16 Jan 2026 13:55:21 +0700 Subject: [PATCH 1/4] feat: add mailbox sorting (ui) --- frontend/src/components/AppSidebar.vue | 148 +++++++++++++++++++------ mail/api/mail.py | 6 +- 2 files changed, 116 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 842aa31c5..6626e6938 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -30,37 +30,50 @@ /> @@ -213,12 +226,67 @@ 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 targetMailboxSortOrder = + targetMailboxId === 'starred' + ? sortedMailboxes.value.at(-1)._sort_order + 1 + : mailboxes.data.find((m: { id: string }) => m.id === targetMailboxId)._sort_order + mailboxes.data + .filter((m) => m._sort_order >= targetMailboxSortOrder) + .forEach((m) => m._sort_order++) + mailboxes.data.find((m: { id: string }) => m.id === draggedItem.value)._sort_order = + targetMailboxSortOrder + } + dropTargetId.value = null + draggedItem.value = null +} + +const sortedMailboxes = computed(() => + mailboxes.data?.slice().sort((a, b) => a._sort_order - b._sort_order), +) + const sidebarItems = computed(() => { if (route.meta.isDashboard) return dashboardItems const mailboxItems = - mailboxes.data?.map( - (mailbox: { id: string; _name: string; role?: string; unread_threads: number }) => ({ + sortedMailboxes.value?.map( + (mailbox: { + id: string + _name: string + role?: string + unread_threads: number + _sort_order: number + }) => ({ label: mailbox._name, icon: mailbox.role && mailbox.role in MAILBOX_ICONS @@ -227,6 +295,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 +327,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 +340,7 @@ const sidebarItems = computed(() => { } return mailboxes.data?.length - ? [{ items: [mailboxItems[0], starredItem, ...mailboxItems.slice(1), addMailboxItem] }] + ? [{ items: [...mailboxItems, starredItem, addMailboxItem] }] : [] }) diff --git a/mail/api/mail.py b/mail/api/mail.py index 07119d1ca..65a69fcef 100644 --- a/mail/api/mail.py +++ b/mail/api/mail.py @@ -33,11 +33,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", "_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]: From 8fa59c8239ea26e2c0c77a5387ee8ee5a4b17ffb Mon Sep 17 00:00:00 2001 From: krantheman Date: Fri, 16 Jan 2026 14:04:39 +0700 Subject: [PATCH 2/4] refactor: don't sort if drop is on folder below --- frontend/src/components/AppSidebar.vue | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 6626e6938..0aaea4ee0 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -257,10 +257,23 @@ const handleDragLeave = () => (dropTargetId.value = null) const handleDrop = (targetMailboxId: string) => (e: DragEvent) => { e.preventDefault() if (draggedItem.value && draggedItem.value !== targetMailboxId) { + const draggedIndex = sortedMailboxes.value.findIndex((m) => m.id === draggedItem.value) + const targetIndex = + targetMailboxId === 'starred' + ? mailboxes.data.length + : sortedMailboxes.value.findIndex((m) => m.id === targetMailboxId) + + if (draggedIndex + 1 === targetIndex) { + dropTargetId.value = null + draggedItem.value = null + return + } + const targetMailboxSortOrder = targetMailboxId === 'starred' ? sortedMailboxes.value.at(-1)._sort_order + 1 : mailboxes.data.find((m: { id: string }) => m.id === targetMailboxId)._sort_order + mailboxes.data .filter((m) => m._sort_order >= targetMailboxSortOrder) .forEach((m) => m._sort_order++) From 9fb3725dbfa9de8569299a28b02b89eb845d879b Mon Sep 17 00:00:00 2001 From: krantheman Date: Mon, 19 Jan 2026 13:29:48 +0700 Subject: [PATCH 3/4] feat: update sort_order as well --- frontend/src/components/AppSidebar.vue | 26 ++++++++++++++++++++++---- mail/api/mail.py | 8 ++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 0aaea4ee0..a4ef4fa1e 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -260,7 +260,7 @@ const handleDrop = (targetMailboxId: string) => (e: DragEvent) => { const draggedIndex = sortedMailboxes.value.findIndex((m) => m.id === draggedItem.value) const targetIndex = targetMailboxId === 'starred' - ? mailboxes.data.length + ? mailboxes.data.length - 1 : sortedMailboxes.value.findIndex((m) => m.id === targetMailboxId) if (draggedIndex + 1 === targetIndex) { @@ -269,6 +269,12 @@ const handleDrop = (targetMailboxId: string) => (e: DragEvent) => { return } + const draggedMailbox = sortedMailboxes.value[draggedIndex] + draggedMailbox.sort_order = sortedMailboxes.value[targetIndex].sort_order + 1 + + const updatedMailboxes: Record = {} + updatedMailboxes[draggedMailbox.id] = draggedMailbox.sort_order + const targetMailboxSortOrder = targetMailboxId === 'starred' ? sortedMailboxes.value.at(-1)._sort_order + 1 @@ -276,14 +282,26 @@ const handleDrop = (targetMailboxId: string) => (e: DragEvent) => { mailboxes.data .filter((m) => m._sort_order >= targetMailboxSortOrder) - .forEach((m) => m._sort_order++) - mailboxes.data.find((m: { id: string }) => m.id === draggedItem.value)._sort_order = - targetMailboxSortOrder + .forEach((m) => { + m._sort_order++ + if (m.id !== draggedMailbox.id) { + m.sort_order += 2 + updatedMailboxes[m.id] = m.sort_order + } + }) + draggedMailbox._sort_order = targetMailboxSortOrder + updateMailboxSortOrder.submit({ mailboxes: updatedMailboxes }) } dropTargetId.value = null draggedItem.value = null } +const updateMailboxSortOrder = createResource({ + url: 'mail.api.mail.update_mailbox_sort_order', + makeParams: ({ mailboxes }: { mailboxes: Record }) => ({ mailboxes }), + onSuccess: () => mailboxes.reload(), +}) + const sortedMailboxes = computed(() => mailboxes.data?.slice().sort((a, b) => a._sort_order - b._sort_order), ) diff --git a/mail/api/mail.py b/mail/api/mail.py index 65a69fcef..8b9e0018f 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 @@ -537,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) From 8d2d7420bd225a12420a56e1f78cdf88a5985f6e Mon Sep 17 00:00:00 2001 From: krantheman Date: Wed, 11 Feb 2026 14:39:32 +0700 Subject: [PATCH 4/4] temp --- frontend/src/components/AppSidebar.vue | 63 +++++++++----------------- frontend/src/main.ts | 10 ++-- frontend/src/types/doctypes.ts | 7 +-- mail/api/mail.py | 2 +- 4 files changed, 30 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index a4ef4fa1e..89212d63d 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -257,67 +257,48 @@ const handleDragLeave = () => (dropTargetId.value = null) const handleDrop = (targetMailboxId: string) => (e: DragEvent) => { e.preventDefault() if (draggedItem.value && draggedItem.value !== targetMailboxId) { - const draggedIndex = sortedMailboxes.value.findIndex((m) => m.id === draggedItem.value) + const draggedIndex = mailboxes.data.findIndex((m) => m.id === draggedItem.value) const targetIndex = targetMailboxId === 'starred' ? mailboxes.data.length - 1 - : sortedMailboxes.value.findIndex((m) => m.id === targetMailboxId) - + : mailboxes.data.findIndex((m) => m.id === targetMailboxId) if (draggedIndex + 1 === targetIndex) { dropTargetId.value = null draggedItem.value = null return } - - const draggedMailbox = sortedMailboxes.value[draggedIndex] - draggedMailbox.sort_order = sortedMailboxes.value[targetIndex].sort_order + 1 - - const updatedMailboxes: Record = {} - updatedMailboxes[draggedMailbox.id] = draggedMailbox.sort_order - - const targetMailboxSortOrder = - targetMailboxId === 'starred' - ? sortedMailboxes.value.at(-1)._sort_order + 1 - : mailboxes.data.find((m: { id: string }) => m.id === targetMailboxId)._sort_order - - mailboxes.data - .filter((m) => m._sort_order >= targetMailboxSortOrder) - .forEach((m) => { - m._sort_order++ - if (m.id !== draggedMailbox.id) { - m.sort_order += 2 - updatedMailboxes[m.id] = m.sort_order - } - }) - draggedMailbox._sort_order = targetMailboxSortOrder - updateMailboxSortOrder.submit({ mailboxes: updatedMailboxes }) + 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 updateMailboxSortOrder = createResource({ - url: 'mail.api.mail.update_mailbox_sort_order', - makeParams: ({ mailboxes }: { mailboxes: Record }) => ({ mailboxes }), +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 sortedMailboxes = computed(() => - mailboxes.data?.slice().sort((a, b) => a._sort_order - b._sort_order), -) - const sidebarItems = computed(() => { if (route.meta.isDashboard) return dashboardItems const mailboxItems = - sortedMailboxes.value?.map( - (mailbox: { - id: string - _name: string - role?: string - unread_threads: number - _sort_order: number - }) => ({ + mailboxes.data?.map( + (mailbox: { id: string; _name: string; role?: string; unread_threads: number }) => ({ label: mailbox._name, icon: mailbox.role && mailbox.role in MAILBOX_ICONS 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 8b9e0018f..eac61284a 100644 --- a/mail/api/mail.py +++ b/mail/api/mail.py @@ -34,7 +34,7 @@ def get_mailboxes() -> list[dict]: if not has_role(user, "Mail User") or user == "Administrator": return [] - fields = ["id", "_name", "role", "total_threads", "unread_threads", "sort_order", "_sort_order"] + 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]