From c28097cafd5c132504df6f4ccd5d6ef8e69645a1 Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Thu, 8 Oct 2026 11:18:15 +0000 Subject: [PATCH 1/2] feat: keep mailbox accounts with identical unordered tag sets together in the default list order Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- docs/content/docs/guides/mailboxes.mdx | 2 ++ web/src/lib/groupMailboxes.test.ts | 27 +++++++++++++++++++++++++- web/src/lib/groupMailboxes.ts | 14 +++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/content/docs/guides/mailboxes.mdx b/docs/content/docs/guides/mailboxes.mdx index 59632789d..598526ad3 100644 --- a/docs/content/docs/guides/mailboxes.mdx +++ b/docs/content/docs/guides/mailboxes.mdx @@ -5,6 +5,8 @@ description: "Connect sender accounts, providers, and how they are assigned to w Browsing filters, searches and view choices are [remembered in your browser tab](/guides/browsing/) across refreshes and navigation, separately for each user and workspace. +In **All accounts**, the default order groups mailboxes by their complete set of tags. Mailboxes with **Tag A + Tag B** stay together even when those tags were assigned in a different order, separately from mailboxes with only **Tag A**. Groups follow the workspace's tag order, with untagged mailboxes last; mailboxes within a group are ordered by email address. Choosing a column sort overrides this default grouping. + A mailbox is a sender account you connect to Warmbly. Every warmup message and campaign email goes out through one you own. ### Diagnostic participation and stopping diff --git a/web/src/lib/groupMailboxes.test.ts b/web/src/lib/groupMailboxes.test.ts index df4cb44a5..16bc3b835 100644 --- a/web/src/lib/groupMailboxes.test.ts +++ b/web/src/lib/groupMailboxes.test.ts @@ -11,9 +11,34 @@ describe("default mailbox tag groups", () => { expect(groupMailboxes(rows, tags).map((row) => row.id)).toEqual(["a", "b", "c", "z"]); expect(rows.map((row) => row.id)).toEqual(["z", "c", "b", "a"]); }); - it("assigns overlapping tags to the first tag in the workspace order and ignores assignment order", () => { + it("orders tag sets by workspace order rather than assignment order", () => { const rows = [mailbox("b", ["receiving"]), mailbox("a", ["receiving", "sending"]), mailbox("c", ["deleted"])]; expect(groupMailboxes(rows, tags).map((row) => row.id)).toEqual(["a", "b", "c"]); expect(groupMailboxes(rows, [...tags].reverse()).map((row) => row.id)).toEqual(["a", "b", "c"]); }); + it("keeps all four identical multi-tag sets together instead of using only their first tag", () => { + const allTags = [...tags, { id: "other", title: "Other", position: 2 } as Tag]; + const rows = [ + mailbox("h", ["receiving", "sending"]), mailbox("a", ["sending"]), + mailbox("e", ["sending", "receiving"]), mailbox("d", ["sending", "other"]), + mailbox("b", ["receiving", "sending"]), mailbox("z", []), + mailbox("c", ["sending"]), mailbox("f", ["sending", "receiving"]), + ]; + const before = rows.map((row) => [...row.tags]); + expect(groupMailboxes(rows, allTags).map((row) => row.id)).toEqual(["a", "c", "b", "e", "f", "h", "d", "z"]); + expect(rows.map((row) => row.tags)).toEqual(before); + }); + it("treats duplicate assignments as a set and preserves unknown tag identities", () => { + const rows = [ + mailbox("d", ["missing-y"]), mailbox("b", ["missing-x"]), + mailbox("c", ["missing-y", "missing-y"]), mailbox("a", ["missing-x"]), + mailbox("f", ["sending", "sending"]), mailbox("e", ["sending"]), mailbox("z", []), + ]; + expect(groupMailboxes(rows, tags).map((row) => row.id)).toEqual(["e", "f", "a", "b", "c", "d", "z"]); + }); + it("uses stable email and id ordering inside each tag set, including an empty workspace", () => { + const rows = [mailbox("b", []), mailbox("a", []), mailbox("c", [])]; + rows[0].email = rows[1].email; + expect(groupMailboxes(rows, []).map((row) => row.id)).toEqual(["a", "b", "c"]); + }); }); diff --git a/web/src/lib/groupMailboxes.ts b/web/src/lib/groupMailboxes.ts index 97f13215d..c72aba61e 100644 --- a/web/src/lib/groupMailboxes.ts +++ b/web/src/lib/groupMailboxes.ts @@ -4,6 +4,16 @@ import type Tag from "./api/models/app/Tag"; export default function groupMailboxes(mailboxes: readonly Inbox[], tags: readonly Tag[]): Inbox[] { const ordered = [...tags].sort((a, b) => a.position - b.position || a.title.localeCompare(b.title) || a.id.localeCompare(b.id)); const rank = new Map(ordered.map((tag, index) => [tag.id, index])); - const group = (mailbox: Inbox) => Math.min(...mailbox.tags.map((id) => rank.get(id) ?? ordered.length), ordered.length); - return [...mailboxes].sort((a, b) => group(a) - group(b) || a.email.localeCompare(b.email) || a.id.localeCompare(b.id)); + const compareTag = (a: string, b: string) => + (rank.get(a) ?? ordered.length) - (rank.get(b) ?? ordered.length) || a.localeCompare(b); + const groups = mailboxes.map((mailbox) => ({ mailbox, tags: [...new Set(mailbox.tags)].sort(compareTag) })); + return groups.sort((a, b) => { + if (!a.tags.length && b.tags.length) return 1; + if (a.tags.length && !b.tags.length) return -1; + for (let i = 0; i < Math.min(a.tags.length, b.tags.length); i++) { + const order = compareTag(a.tags[i], b.tags[i]); + if (order) return order; + } + return a.tags.length - b.tags.length || a.mailbox.email.localeCompare(b.mailbox.email) || a.mailbox.id.localeCompare(b.mailbox.id); + }).map(({ mailbox }) => mailbox); } From 4b11147ff9bc9af6495635d8fee03d59e055dbb6 Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Thu, 8 Oct 2026 11:32:49 +0000 Subject: [PATCH 2/2] feat: align mailbox filtering documentation with complete tag-set grouping Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- docs/content/docs/guides/mailboxes.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/content/docs/guides/mailboxes.mdx b/docs/content/docs/guides/mailboxes.mdx index 598526ad3..b209940e7 100644 --- a/docs/content/docs/guides/mailboxes.mdx +++ b/docs/content/docs/guides/mailboxes.mdx @@ -5,8 +5,6 @@ description: "Connect sender accounts, providers, and how they are assigned to w Browsing filters, searches and view choices are [remembered in your browser tab](/guides/browsing/) across refreshes and navigation, separately for each user and workspace. -In **All accounts**, the default order groups mailboxes by their complete set of tags. Mailboxes with **Tag A + Tag B** stay together even when those tags were assigned in a different order, separately from mailboxes with only **Tag A**. Groups follow the workspace's tag order, with untagged mailboxes last; mailboxes within a group are ordered by email address. Choosing a column sort overrides this default grouping. - A mailbox is a sender account you connect to Warmbly. Every warmup message and campaign email goes out through one you own. ### Diagnostic participation and stopping @@ -17,7 +15,7 @@ New mailboxes default to diagnostic participation **off**. The organization-auth Use the **Tags** dropdown above the mailbox list to show accounts with a particular tag. The selection is kept in the page URL and remembered for your user and workspace in the browser tab, so refreshing, opening a bookmark, or visiting another page and returning keeps the filter. Choose **All accounts** to clear it. A tag that no longer exists in the current workspace falls back to **All accounts**. -Without an explicit column sort, **All accounts** groups the loaded mailboxes by workspace tag order, then by email address, with untagged accounts last. A mailbox with several tags belongs to its first tag in that order and appears only once. Selecting a column sort takes priority over grouping. +Without an explicit column sort, **All accounts** groups the loaded mailboxes by their complete set of tags. Mailboxes with **Tag A + Tag B** stay together even when those tags were assigned in a different order, separately from mailboxes with only **Tag A**. Groups follow the workspace's tag order, with untagged mailboxes last; mailboxes within a group are ordered by email address. Each mailbox appears only once. Selecting a column sort takes priority over grouping. ## Connecting an account