@remit/web-client 0.0.194 → 0.0.195
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/hooks/useAutoMovedBadge.ts +2 -2
- package/src/hooks/useCreateMailbox.render.test.ts +2 -0
- package/src/hooks/useCreateMailbox.ts +2 -2
- package/src/lib/folder-roles.test.ts +59 -16
- package/src/lib/folder-roles.ts +6 -8
- package/src/lib/move-targets.test.ts +19 -0
- package/src/lib/move-targets.ts +7 -11
- package/src/lib/search-token-index.test.ts +29 -0
- package/src/lib/search-token-index.ts +5 -7
- package/src/routes/settings/folders.tsx +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.195",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@hookform/resolvers": "*",
|
|
77
77
|
"@remit/api-http-client": "*",
|
|
78
|
+
"@remit/data-ports": "*",
|
|
78
79
|
"@remit/domain-enums": "*",
|
|
79
80
|
"@remit/ui": "*",
|
|
80
81
|
"@tanstack/react-query": "^5",
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
RemitImapAutoMovedInfo,
|
|
4
4
|
RemitImapMessageSpamReport,
|
|
5
5
|
} from "@remit/api-http-client/types.gen.ts";
|
|
6
|
+
import { mailboxLeafName } from "@remit/data-ports/mailbox-name";
|
|
6
7
|
import { useQuery } from "@tanstack/react-query";
|
|
7
8
|
import { useCallback } from "react";
|
|
8
9
|
import {
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
11
12
|
resolveUndoTargetMailboxId,
|
|
12
13
|
spamReportLabel,
|
|
13
14
|
} from "@/lib/auto-moved";
|
|
14
|
-
import { getMailboxDisplayName } from "@/lib/folder-roles";
|
|
15
15
|
import { useInboxMailbox, useJunkMailbox } from "./useArchiveMailbox";
|
|
16
16
|
import { useMoveMessages } from "./useMoveMessages";
|
|
17
17
|
import { useReportSpam } from "./useReportSpam";
|
|
@@ -132,7 +132,7 @@ export const useAutoMovedBadge = ({
|
|
|
132
132
|
const sourceFolderName = autoMoved.fromMailboxId
|
|
133
133
|
? mailboxes?.items
|
|
134
134
|
.filter((mailbox) => mailbox.mailboxId === autoMoved.fromMailboxId)
|
|
135
|
-
.map((mailbox) =>
|
|
135
|
+
.map((mailbox) => mailboxLeafName(mailbox))[0]
|
|
136
136
|
: undefined;
|
|
137
137
|
|
|
138
138
|
return {
|
|
@@ -67,6 +67,7 @@ const mount = (
|
|
|
67
67
|
mailboxId: `mbx-${body.fullPath}`,
|
|
68
68
|
accountId: ACCOUNT,
|
|
69
69
|
fullPath: body.fullPath,
|
|
70
|
+
hierarchyDelimiter: "/",
|
|
70
71
|
syncStatus: createdSyncStatus,
|
|
71
72
|
} as RemitImapMailboxResponse);
|
|
72
73
|
return { mailboxId: `mbx-${body.fullPath}`, fullPath: body.fullPath };
|
|
@@ -173,6 +174,7 @@ describe("useCreateMailbox.createFolder validation", () => {
|
|
|
173
174
|
mailboxId: `mbx-${body.fullPath}`,
|
|
174
175
|
accountId: ACCOUNT,
|
|
175
176
|
fullPath: body.fullPath,
|
|
177
|
+
hierarchyDelimiter: "/",
|
|
176
178
|
} as RemitImapMailboxResponse);
|
|
177
179
|
return { mailboxId: `mbx-${body.fullPath}`, fullPath: body.fullPath };
|
|
178
180
|
}
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
mailboxOperationsListMailboxesOptions,
|
|
4
4
|
mailboxOperationsListMailboxesQueryKey,
|
|
5
5
|
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
6
|
+
import { mailboxLeafName } from "@remit/data-ports/mailbox-name";
|
|
6
7
|
import type { FolderTreeNode } from "@remit/ui";
|
|
7
8
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
9
|
import { useCallback, useRef } from "react";
|
|
9
|
-
import { getMailboxDisplayName } from "@/lib/folder-roles";
|
|
10
10
|
import { waitForMailboxSynced } from "@/lib/mailbox-sync-wait";
|
|
11
11
|
import {
|
|
12
12
|
composeFolderPath,
|
|
@@ -136,7 +136,7 @@ export function useCreateMailbox(accountId: string | undefined) {
|
|
|
136
136
|
pendingByPath.current.delete(fullPath);
|
|
137
137
|
return {
|
|
138
138
|
id: confirmed.mailboxId,
|
|
139
|
-
label:
|
|
139
|
+
label: mailboxLeafName(confirmed),
|
|
140
140
|
path: confirmed.fullPath,
|
|
141
141
|
};
|
|
142
142
|
},
|
|
@@ -3,7 +3,6 @@ import { describe, test } from "node:test";
|
|
|
3
3
|
import type { RemitImapFolderAppointment } from "@remit/api-http-client/types.gen.ts";
|
|
4
4
|
import {
|
|
5
5
|
buildMailboxRoleMap,
|
|
6
|
-
getMailboxDisplayName,
|
|
7
6
|
labelForMailbox,
|
|
8
7
|
shouldShowUnreadBadgeForRole,
|
|
9
8
|
} from "./folder-roles.js";
|
|
@@ -43,16 +42,6 @@ describe("buildMailboxRoleMap", () => {
|
|
|
43
42
|
});
|
|
44
43
|
});
|
|
45
44
|
|
|
46
|
-
describe("getMailboxDisplayName", () => {
|
|
47
|
-
test("returns the leaf segment of a nested path", () => {
|
|
48
|
-
assert.equal(getMailboxDisplayName("INBOX/Sent Messages"), "Sent Messages");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("returns the whole path when there is no delimiter", () => {
|
|
52
|
-
assert.equal(getMailboxDisplayName("INBOX"), "INBOX");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
45
|
describe("labelForMailbox", () => {
|
|
57
46
|
const t = (key: string, fallback: string) =>
|
|
58
47
|
key === "sidebar.sent" ? "Verzonden" : fallback;
|
|
@@ -60,7 +49,11 @@ describe("labelForMailbox", () => {
|
|
|
60
49
|
test("a trimmed displayNameOverride wins over everything", () => {
|
|
61
50
|
assert.equal(
|
|
62
51
|
labelForMailbox(
|
|
63
|
-
{
|
|
52
|
+
{
|
|
53
|
+
fullPath: "INBOX/Sent",
|
|
54
|
+
hierarchyDelimiter: "/",
|
|
55
|
+
displayNameOverride: " My Sent ",
|
|
56
|
+
},
|
|
64
57
|
"sent",
|
|
65
58
|
t,
|
|
66
59
|
),
|
|
@@ -70,26 +63,44 @@ describe("labelForMailbox", () => {
|
|
|
70
63
|
|
|
71
64
|
test("falls back to the translated canonical role label", () => {
|
|
72
65
|
assert.equal(
|
|
73
|
-
labelForMailbox(
|
|
66
|
+
labelForMailbox(
|
|
67
|
+
{ fullPath: "INBOX/Verzonden", hierarchyDelimiter: "/" },
|
|
68
|
+
"sent",
|
|
69
|
+
t,
|
|
70
|
+
),
|
|
74
71
|
"Verzonden",
|
|
75
72
|
);
|
|
76
73
|
});
|
|
77
74
|
|
|
78
75
|
test("falls back to the provider leaf when there is no role", () => {
|
|
79
76
|
assert.equal(
|
|
80
|
-
labelForMailbox(
|
|
77
|
+
labelForMailbox(
|
|
78
|
+
{ fullPath: "INBOX/Nieuwsbrieven", hierarchyDelimiter: "/" },
|
|
79
|
+
undefined,
|
|
80
|
+
t,
|
|
81
|
+
),
|
|
81
82
|
"Nieuwsbrieven",
|
|
82
83
|
);
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
test("falls back to the leaf when no translator is supplied", () => {
|
|
86
|
-
assert.equal(
|
|
87
|
+
assert.equal(
|
|
88
|
+
labelForMailbox(
|
|
89
|
+
{ fullPath: "INBOX/Sent", hierarchyDelimiter: "/" },
|
|
90
|
+
"sent",
|
|
91
|
+
),
|
|
92
|
+
"Sent",
|
|
93
|
+
);
|
|
87
94
|
});
|
|
88
95
|
|
|
89
96
|
test("a blank/whitespace override is ignored", () => {
|
|
90
97
|
assert.equal(
|
|
91
98
|
labelForMailbox(
|
|
92
|
-
{
|
|
99
|
+
{
|
|
100
|
+
fullPath: "INBOX/Sent",
|
|
101
|
+
hierarchyDelimiter: "/",
|
|
102
|
+
displayNameOverride: " ",
|
|
103
|
+
},
|
|
93
104
|
"sent",
|
|
94
105
|
t,
|
|
95
106
|
),
|
|
@@ -98,6 +109,38 @@ describe("labelForMailbox", () => {
|
|
|
98
109
|
});
|
|
99
110
|
});
|
|
100
111
|
|
|
112
|
+
describe("labelForMailbox — the server’s own delimiter (#877)", () => {
|
|
113
|
+
test("a dot-delimited nested folder renders its leaf", () => {
|
|
114
|
+
assert.equal(
|
|
115
|
+
labelForMailbox(
|
|
116
|
+
{ fullPath: "INBOX.Projects.Q3", hierarchyDelimiter: "." },
|
|
117
|
+
undefined,
|
|
118
|
+
),
|
|
119
|
+
"Q3",
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a flat namespace renders the whole name", () => {
|
|
124
|
+
assert.equal(
|
|
125
|
+
labelForMailbox(
|
|
126
|
+
{ fullPath: "Projects/Q3", hierarchyDelimiter: "" },
|
|
127
|
+
undefined,
|
|
128
|
+
),
|
|
129
|
+
"Projects/Q3",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a slash in a dot-delimited name is part of the name", () => {
|
|
134
|
+
assert.equal(
|
|
135
|
+
labelForMailbox(
|
|
136
|
+
{ fullPath: "INBOX.Reading/Writing", hierarchyDelimiter: "." },
|
|
137
|
+
undefined,
|
|
138
|
+
),
|
|
139
|
+
"Reading/Writing",
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
101
144
|
describe("shouldShowUnreadBadgeForRole", () => {
|
|
102
145
|
test("hides the badge for Sent, Drafts, and Trash", () => {
|
|
103
146
|
assert.equal(shouldShowUnreadBadgeForRole("sent"), false);
|
package/src/lib/folder-roles.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type {
|
|
|
2
2
|
RemitImapCanonicalMailboxRole,
|
|
3
3
|
RemitImapFolderAppointment,
|
|
4
4
|
} from "@remit/api-http-client/types.gen.ts";
|
|
5
|
+
import {
|
|
6
|
+
type MailboxPath,
|
|
7
|
+
mailboxLeafName,
|
|
8
|
+
} from "@remit/data-ports/mailbox-name";
|
|
5
9
|
import type { NavMailboxRole } from "@remit/ui";
|
|
6
10
|
|
|
7
11
|
/**
|
|
@@ -79,12 +83,6 @@ export function buildMailboxRoleMap(
|
|
|
79
83
|
return byMailbox;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
/** Leaf segment of a provider path (`INBOX/Spam` → `Spam`). */
|
|
83
|
-
export const getMailboxDisplayName = (fullPath: string): string => {
|
|
84
|
-
const parts = fullPath.split("/");
|
|
85
|
-
return parts[parts.length - 1] || fullPath;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
86
|
type Translator = (key: string, fallback: string) => string;
|
|
89
87
|
|
|
90
88
|
/**
|
|
@@ -92,13 +90,13 @@ type Translator = (key: string, fallback: string) => string;
|
|
|
92
90
|
* canonical localized label for the appointed role, else the provider leaf.
|
|
93
91
|
*/
|
|
94
92
|
export function labelForMailbox(
|
|
95
|
-
mailbox:
|
|
93
|
+
mailbox: MailboxPath & { displayNameOverride?: string },
|
|
96
94
|
role: NavMailboxRole | undefined,
|
|
97
95
|
t?: Translator,
|
|
98
96
|
): string {
|
|
99
97
|
const override = mailbox.displayNameOverride?.trim();
|
|
100
98
|
if (override) return override;
|
|
101
|
-
const leaf =
|
|
99
|
+
const leaf = mailboxLeafName(mailbox);
|
|
102
100
|
if (!role || !t) return leaf;
|
|
103
101
|
return t(`sidebar.${role}`, leaf);
|
|
104
102
|
}
|
|
@@ -214,3 +214,22 @@ describe("buildMoveTargets — Outbox locale exclusion (#290)", () => {
|
|
|
214
214
|
assert.deepStrictEqual(ids, ["custom", "inbox", "outboxy", "trash"]);
|
|
215
215
|
});
|
|
216
216
|
});
|
|
217
|
+
|
|
218
|
+
describe("buildMoveTargets — the server’s own delimiter (#877)", () => {
|
|
219
|
+
test("orders and filters by the leaf under a dot-delimited namespace", () => {
|
|
220
|
+
const dotted = (mailboxId: string, fullPath: string) =>
|
|
221
|
+
make({ mailboxId, fullPath, hierarchyDelimiter: "." });
|
|
222
|
+
const result = buildMoveTargets(
|
|
223
|
+
[
|
|
224
|
+
dotted("beta", "INBOX.Projects.Beta"),
|
|
225
|
+
dotted("outbox", "INBOX.Outbox"),
|
|
226
|
+
dotted("alpha", "INBOX.Zoo.Alpha"),
|
|
227
|
+
],
|
|
228
|
+
[],
|
|
229
|
+
);
|
|
230
|
+
assert.deepStrictEqual(
|
|
231
|
+
result.map((mailbox) => mailbox.mailboxId),
|
|
232
|
+
["alpha", "beta"],
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
});
|
package/src/lib/move-targets.ts
CHANGED
|
@@ -2,11 +2,8 @@ import type {
|
|
|
2
2
|
RemitImapFolderAppointment,
|
|
3
3
|
RemitImapMailboxResponse,
|
|
4
4
|
} from "@remit/api-http-client/types.gen.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
getMailboxDisplayName,
|
|
8
|
-
ROLE_PRIORITY,
|
|
9
|
-
} from "./folder-roles.js";
|
|
5
|
+
import { mailboxLeafName } from "@remit/data-ports/mailbox-name";
|
|
6
|
+
import { buildMailboxRoleMap, ROLE_PRIORITY } from "./folder-roles.js";
|
|
10
7
|
|
|
11
8
|
// Outbox has no IMAP special-use flag (RFC 6154 doesn't define one) and isn't
|
|
12
9
|
// a canonical role (RFC 032 exclusive-folder-appointment) either — it's
|
|
@@ -27,7 +24,7 @@ const OUTBOX_LOCALE_NAMES: ReadonlySet<string> = new Set([
|
|
|
27
24
|
]);
|
|
28
25
|
|
|
29
26
|
const isOutboxDestination = (mailbox: RemitImapMailboxResponse): boolean => {
|
|
30
|
-
const leaf =
|
|
27
|
+
const leaf = mailboxLeafName(mailbox).trim().toLowerCase();
|
|
31
28
|
return OUTBOX_LOCALE_NAMES.has(leaf);
|
|
32
29
|
};
|
|
33
30
|
|
|
@@ -65,10 +62,9 @@ export const buildMoveTargets = (
|
|
|
65
62
|
? ROLE_PRIORITY.indexOf(roleMap.get(b.mailboxId) ?? "inbox")
|
|
66
63
|
: NON_SYSTEM_PRIORITY;
|
|
67
64
|
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
65
|
+
return mailboxLeafName(a).localeCompare(mailboxLeafName(b), undefined, {
|
|
66
|
+
sensitivity: "base",
|
|
67
|
+
numeric: true,
|
|
68
|
+
});
|
|
73
69
|
});
|
|
74
70
|
};
|
|
@@ -209,3 +209,32 @@ describe("buildAccountSuggestionValues", () => {
|
|
|
209
209
|
);
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
|
+
|
|
213
|
+
describe("the server’s own delimiter (#877)", () => {
|
|
214
|
+
it("offers the leaf of a dot-delimited folder as its shortest spelling", () => {
|
|
215
|
+
const values = buildMailboxSuggestionValues([
|
|
216
|
+
[
|
|
217
|
+
mailbox({
|
|
218
|
+
mailboxId: "m1",
|
|
219
|
+
fullPath: "INBOX.Projects.Q3",
|
|
220
|
+
hierarchyDelimiter: ".",
|
|
221
|
+
}),
|
|
222
|
+
],
|
|
223
|
+
]);
|
|
224
|
+
assert.deepEqual(values, [{ value: "Q3", label: "Q3" }]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("resolves in: by the leaf of a dot-delimited folder", () => {
|
|
228
|
+
const index = buildMailboxNameIndex([
|
|
229
|
+
[
|
|
230
|
+
mailbox({
|
|
231
|
+
mailboxId: "m1",
|
|
232
|
+
fullPath: "INBOX.Projects.Q3",
|
|
233
|
+
hierarchyDelimiter: ".",
|
|
234
|
+
}),
|
|
235
|
+
],
|
|
236
|
+
]);
|
|
237
|
+
assert.equal(index.get("q3"), "m1");
|
|
238
|
+
assert.equal(index.get("inbox.projects.q3"), "m1");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
RemitImapAccountResponse,
|
|
14
14
|
RemitImapMailboxResponse,
|
|
15
15
|
} from "@remit/api-http-client/types.gen.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { mailboxLeafName } from "@remit/data-ports/mailbox-name";
|
|
17
17
|
import type { SearchSuggestionValue } from "./search-suggestions.js";
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -52,11 +52,9 @@ export function buildMailboxNameIndex(
|
|
|
52
52
|
for (const mailbox of mailboxes) {
|
|
53
53
|
const fullPath = mailbox.fullPath?.toLowerCase();
|
|
54
54
|
if (!fullPath) continue;
|
|
55
|
-
const
|
|
55
|
+
const leaf = mailboxLeafName(mailbox).toLowerCase();
|
|
56
56
|
if (!index.has(fullPath)) index.set(fullPath, mailbox.mailboxId);
|
|
57
|
-
if (
|
|
58
|
-
index.set(lastSegment, mailbox.mailboxId);
|
|
59
|
-
}
|
|
57
|
+
if (!index.has(leaf)) index.set(leaf, mailbox.mailboxId);
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
return index;
|
|
@@ -78,14 +76,14 @@ export function buildMailboxSuggestionValues(
|
|
|
78
76
|
const all = mailboxesByAccount.flat().filter((mailbox) => mailbox.fullPath);
|
|
79
77
|
const leafCounts = new Map<string, number>();
|
|
80
78
|
for (const mailbox of all) {
|
|
81
|
-
const leaf =
|
|
79
|
+
const leaf = mailboxLeafName(mailbox).toLowerCase();
|
|
82
80
|
leafCounts.set(leaf, (leafCounts.get(leaf) ?? 0) + 1);
|
|
83
81
|
}
|
|
84
82
|
const emailByAccountId = new Map(
|
|
85
83
|
accounts.map((account) => [account.accountId, account.email]),
|
|
86
84
|
);
|
|
87
85
|
return all.map((mailbox) => {
|
|
88
|
-
const leaf =
|
|
86
|
+
const leaf = mailboxLeafName(mailbox);
|
|
89
87
|
const unique = leafCounts.get(leaf.toLowerCase()) === 1;
|
|
90
88
|
const hint =
|
|
91
89
|
accounts.length > 1 ? emailByAccountId.get(mailbox.accountId) : undefined;
|
|
@@ -166,6 +166,7 @@ function AccountFolders({ account }: { account: RemitImapAccountResponse }) {
|
|
|
166
166
|
const candidates: CandidateFolder[] = mailboxes.map((mailbox) => ({
|
|
167
167
|
mailboxId: mailbox.mailboxId,
|
|
168
168
|
providerPath: mailbox.fullPath,
|
|
169
|
+
hierarchyDelimiter: mailbox.hierarchyDelimiter,
|
|
169
170
|
messageCount: mailbox.messageCount,
|
|
170
171
|
}));
|
|
171
172
|
|
|
@@ -241,7 +242,10 @@ function AccountFolders({ account }: { account: RemitImapAccountResponse }) {
|
|
|
241
242
|
translator,
|
|
242
243
|
)}
|
|
243
244
|
defaultLabel={labelForMailbox(
|
|
244
|
-
{
|
|
245
|
+
{
|
|
246
|
+
fullPath: renaming.fullPath,
|
|
247
|
+
hierarchyDelimiter: renaming.hierarchyDelimiter,
|
|
248
|
+
},
|
|
245
249
|
roleMap.get(renaming.mailboxId),
|
|
246
250
|
translator,
|
|
247
251
|
)}
|