@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.194",
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) => getMailboxDisplayName(mailbox.fullPath))[0]
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: getMailboxDisplayName(confirmed.fullPath),
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
- { fullPath: "INBOX/Sent", displayNameOverride: " My Sent " },
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({ fullPath: "INBOX/Verzonden" }, "sent", t),
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({ fullPath: "INBOX/Nieuwsbrieven" }, undefined, t),
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(labelForMailbox({ fullPath: "INBOX/Sent" }, "sent"), "Sent");
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
- { fullPath: "INBOX/Sent", displayNameOverride: " " },
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);
@@ -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: { fullPath: string; displayNameOverride?: string },
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 = getMailboxDisplayName(mailbox.fullPath);
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
+ });
@@ -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
- buildMailboxRoleMap,
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 = getMailboxDisplayName(mailbox.fullPath).trim().toLowerCase();
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 getMailboxDisplayName(a.fullPath).localeCompare(
69
- getMailboxDisplayName(b.fullPath),
70
- undefined,
71
- { sensitivity: "base", numeric: true },
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 { getMailboxDisplayName } from "./folder-roles.js";
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 lastSegment = fullPath.split("/").pop();
55
+ const leaf = mailboxLeafName(mailbox).toLowerCase();
56
56
  if (!index.has(fullPath)) index.set(fullPath, mailbox.mailboxId);
57
- if (lastSegment && !index.has(lastSegment)) {
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 = getMailboxDisplayName(mailbox.fullPath).toLowerCase();
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 = getMailboxDisplayName(mailbox.fullPath);
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
- { fullPath: renaming.fullPath },
245
+ {
246
+ fullPath: renaming.fullPath,
247
+ hierarchyDelimiter: renaming.hierarchyDelimiter,
248
+ },
245
249
  roleMap.get(renaming.mailboxId),
246
250
  translator,
247
251
  )}