Merge pull request #881 from warmbly/devin/1791458091-mailbox-tag-sets

feat: group mailbox accounts by their complete unordered tag sets
This commit is contained in:
Matthew Meszaros
2026-10-08 12:27:24 +00:00
committed by GitHub
3 changed files with 39 additions and 4 deletions
+1 -1
View File
@@ -15,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
+26 -1
View File
@@ -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"]);
});
});
+12 -2
View File
@@ -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);
}