@remit/ui 0.0.154 → 0.0.155

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/ui",
3
- "version": "0.0.154",
3
+ "version": "0.0.155",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -10,18 +10,55 @@ describe("provenanceFolderLabel", () => {
10
10
 
11
11
  it("reads a junk appointment as Spam whatever the server calls it", () => {
12
12
  assert.equal(
13
- provenanceFolderLabel({ role: "junk", providerPath: "Bulk Mail" }),
13
+ provenanceFolderLabel({
14
+ role: "junk",
15
+ providerPath: "Bulk Mail",
16
+ hierarchyDelimiter: "/",
17
+ }),
14
18
  "Spam",
15
19
  );
16
20
  });
17
21
 
18
22
  it("falls back to the leaf of a folder nobody appointed", () => {
19
23
  assert.equal(
20
- provenanceFolderLabel({ providerPath: "Projects/Bookkeeping" }),
24
+ provenanceFolderLabel({
25
+ providerPath: "Projects/Bookkeeping",
26
+ hierarchyDelimiter: "/",
27
+ }),
21
28
  "Bookkeeping",
22
29
  );
23
30
  });
24
31
 
32
+ it("cuts the path on the delimiter the server reports, not on a slash", () => {
33
+ assert.equal(
34
+ provenanceFolderLabel({
35
+ providerPath: "INBOX.Projects.Q3",
36
+ hierarchyDelimiter: ".",
37
+ }),
38
+ "Q3",
39
+ );
40
+ });
41
+
42
+ it("leaves a slash alone in a folder name on a dotted server", () => {
43
+ assert.equal(
44
+ provenanceFolderLabel({
45
+ providerPath: "INBOX.Reading/Writing",
46
+ hierarchyDelimiter: ".",
47
+ }),
48
+ "Reading/Writing",
49
+ );
50
+ });
51
+
52
+ it("treats a flat namespace as one folder name", () => {
53
+ assert.equal(
54
+ provenanceFolderLabel({
55
+ providerPath: "Projects/Q3",
56
+ hierarchyDelimiter: "",
57
+ }),
58
+ "Projects/Q3",
59
+ );
60
+ });
61
+
25
62
  it("refuses to label a view rather than a place", () => {
26
63
  assert.equal(provenanceFolderLabel({ role: "all" }), undefined);
27
64
  assert.equal(provenanceFolderLabel({ role: "flagged" }), undefined);
@@ -29,28 +66,43 @@ describe("provenanceFolderLabel", () => {
29
66
 
30
67
  it("refuses to label Gmail's own reserved namespace", () => {
31
68
  assert.equal(
32
- provenanceFolderLabel({ providerPath: "[Gmail]/All Mail" }),
69
+ provenanceFolderLabel({
70
+ providerPath: "[Gmail]/All Mail",
71
+ hierarchyDelimiter: "/",
72
+ }),
33
73
  undefined,
34
74
  );
35
75
  assert.equal(
36
- provenanceFolderLabel({ providerPath: "[Gmail]/Starred" }),
76
+ provenanceFolderLabel({
77
+ providerPath: "[Gmail]/Starred",
78
+ hierarchyDelimiter: "/",
79
+ }),
37
80
  undefined,
38
81
  );
39
82
  });
40
83
 
41
84
  it("refuses the googlemail.com spelling of the same namespace", () => {
42
85
  assert.equal(
43
- provenanceFolderLabel({ providerPath: "[Google Mail]/All Mail" }),
86
+ provenanceFolderLabel({
87
+ providerPath: "[Google Mail]/All Mail",
88
+ hierarchyDelimiter: "/",
89
+ }),
44
90
  undefined,
45
91
  );
46
92
  assert.equal(
47
- provenanceFolderLabel({ providerPath: "[Google Mail]/Starred" }),
93
+ provenanceFolderLabel({
94
+ providerPath: "[Google Mail]/Starred",
95
+ hierarchyDelimiter: "/",
96
+ }),
48
97
  undefined,
49
98
  );
50
99
  });
51
100
 
52
101
  it("labels a user folder that merely mentions Gmail", () => {
53
- assert.equal(provenanceFolderLabel({ providerPath: "Gmail" }), "Gmail");
102
+ assert.equal(
103
+ provenanceFolderLabel({ providerPath: "Gmail", hierarchyDelimiter: "/" }),
104
+ "Gmail",
105
+ );
54
106
  });
55
107
 
56
108
  it("has nothing to say about a folder it knows nothing about", () => {
@@ -9,6 +9,7 @@ import {
9
9
  Trash2,
10
10
  } from "lucide-react";
11
11
  import type { ReactNode } from "react";
12
+ import { folderLeaf, folderPathSegments } from "../lib/folder-tree.js";
12
13
 
13
14
  /* ------------------------------------------------------------------ */
14
15
  /* Folder role vocabulary (RFC 032 exclusive-folder-appointment, */
@@ -48,30 +49,28 @@ export function canonicalRoleLabel(role: FolderRole): string {
48
49
  return ROLE_LABEL[role];
49
50
  }
50
51
 
51
- /**
52
- * Leaf segment of a provider path (`INBOX/Spam` → `Spam`). Only for the two
53
- * surfaces whose wire payload carries no `hierarchyDelimiter` — quarantine
54
- * entries and search-result provenance. Everything else: `folderLeaf`.
55
- */
56
- export function providerLeaf(providerPath: string): string {
57
- const parts = providerPath.split("/");
58
- return parts[parts.length - 1] || providerPath;
59
- }
60
-
61
52
  /* ------------------------------------------------------------------ */
62
53
  /* Provenance: where a search result actually lives */
63
54
  /* ------------------------------------------------------------------ */
64
55
 
65
56
  /**
66
57
  * The folder a search result was read from. `role` is the account's IMAP
67
- * special-use appointment (`junk` is `\Junk`); accounts that expose a folder
68
- * nobody appointed carry only a `providerPath`.
58
+ * special-use appointment (`junk` is `\Junk`); a folder nobody appointed a role
59
+ * to carries only its path.
60
+ *
61
+ * The path and the separator its own server cuts it on travel together or not
62
+ * at all: `INBOX.Projects.Q3` is three segments on a `.`-delimited server and a
63
+ * single folder name on a `/`-delimited one, so a path alone cannot be cut.
69
64
  */
70
- export interface ResultFolder {
71
- role?: FolderRole;
72
- /** Provider path as the server spells it, e.g. `Projects/Bookkeeping`. */
73
- providerPath?: string;
74
- }
65
+ export type ResultFolder =
66
+ | {
67
+ role?: FolderRole;
68
+ /** Provider path as the server spells it, e.g. `Projects/Bookkeeping`. */
69
+ providerPath: string;
70
+ /** The account's own hierarchy separator; `""` means a flat namespace. */
71
+ hierarchyDelimiter: string;
72
+ }
73
+ | { role?: FolderRole; providerPath?: never; hierarchyDelimiter?: never };
75
74
 
76
75
  /**
77
76
  * Roles that name a view rather than a place a message is filed. A message in
@@ -113,8 +112,12 @@ export function provenanceFolderLabel(
113
112
  return canonicalRoleLabel(folder.role);
114
113
  }
115
114
  if (!folder.providerPath) return undefined;
116
- if (GMAIL_NAMESPACES.has(folder.providerPath.split("/")[0])) return undefined;
117
- return providerLeaf(folder.providerPath);
115
+ const segments = folderPathSegments(
116
+ folder.providerPath,
117
+ folder.hierarchyDelimiter,
118
+ );
119
+ if (GMAIL_NAMESPACES.has(segments[0])) return undefined;
120
+ return folderLeaf(folder.providerPath, folder.hierarchyDelimiter);
118
121
  }
119
122
 
120
123
  export function roleIcon(role: FolderRole): ReactNode {
@@ -123,7 +123,7 @@ const crossFolderMatches: SearchResult[] = [
123
123
  subject: "Invoices for the quarter",
124
124
  snippet: "The quarterly set, filed with the rest of the bookkeeping.",
125
125
  date: "Jan 30",
126
- folder: { providerPath: "Projects/Bookkeeping" },
126
+ folder: { providerPath: "Projects/Bookkeeping", hierarchyDelimiter: "/" },
127
127
  },
128
128
  ];
129
129
 
@@ -1,7 +1,8 @@
1
1
  import { Bug } from "lucide-react";
2
+ import { folderLeaf } from "../lib/folder-tree.js";
2
3
  import { Badge } from "./badge.js";
3
4
  import { Button } from "./button.js";
4
- import { canonicalRoleLabel, providerLeaf } from "./folder-role.js";
5
+ import { canonicalRoleLabel } from "./folder-role.js";
5
6
  import {
6
7
  type QuarantineEntry,
7
8
  quarantineSummary,
@@ -47,7 +48,7 @@ export function QuarantineEntryRow({
47
48
  </Badge>
48
49
  )}
49
50
  <span className="truncate" title={entry.mailboxPath}>
50
- {providerLeaf(entry.mailboxPath)}
51
+ {folderLeaf(entry.mailboxPath, entry.mailboxDelimiter)}
51
52
  </span>
52
53
  <span aria-hidden>·</span>
53
54
  <span>{`uid ${entry.uid}`}</span>
@@ -14,6 +14,7 @@ export const quarantineDemoEntries: readonly QuarantineEntry[] = [
14
14
  uid: 40217,
15
15
  mailboxRole: "inbox",
16
16
  mailboxPath: "INBOX",
17
+ mailboxDelimiter: "/",
17
18
  failureStage: "BodyParse",
18
19
  failureCode: "UnreadableBody",
19
20
  failureMessage: "multipart boundary was never closed",
@@ -42,6 +43,7 @@ export const quarantineDemoEntries: readonly QuarantineEntry[] = [
42
43
  uid: 40219,
43
44
  mailboxRole: "archive",
44
45
  mailboxPath: "Archive/2026",
46
+ mailboxDelimiter: "/",
45
47
  failureStage: "BodyParse",
46
48
  failureCode: "UnknownCharset",
47
49
  failureMessage: "declared charset is not a known encoding",
@@ -65,7 +67,9 @@ export const quarantineDemoEntries: readonly QuarantineEntry[] = [
65
67
  // A folder the user never appointed a role to — the ordinary state of a
66
68
  // plain folder, and the reason mailboxRole is optional.
67
69
  mailboxRole: undefined,
68
- mailboxPath: "Clients/Acme Holdings",
70
+ mailboxPath: "INBOX.Clients.Acme Holdings",
71
+ // A `.`-delimited server: the row still reads the leaf, not the path.
72
+ mailboxDelimiter: ".",
69
73
  failureStage: "BodyParse",
70
74
  failureCode: "UnreadableBody",
71
75
  failureMessage: "stream ended before the declared body length",
@@ -91,6 +95,7 @@ export const quarantineDemoEntries: readonly QuarantineEntry[] = [
91
95
  uid: 40263,
92
96
  mailboxRole: "inbox",
93
97
  mailboxPath: "INBOX",
98
+ mailboxDelimiter: "/",
94
99
  failureStage: "BodyParse",
95
100
  failureCode: "UnreadableBody",
96
101
  failureMessage: "connection closed mid-body",
@@ -78,6 +78,12 @@ export interface QuarantineEntry {
78
78
  mailboxRole?: FolderRole;
79
79
  /** The user's own folder name. Shown on screen, withheld from the report. */
80
80
  mailboxPath: string;
81
+ /**
82
+ * The separator that folder's own server cuts its paths on, so the row can
83
+ * show the leaf. Resolved from the account's mailbox list — the quarantine
84
+ * record itself carries no delimiter. `""` means a flat namespace.
85
+ */
86
+ mailboxDelimiter: string;
81
87
  failureStage: QuarantineFailureStage;
82
88
  failureCode: QuarantineFailureCode;
83
89
  /** Parser error text. Shown on screen, never in the report. */
@@ -44,6 +44,19 @@ describe("QuarantineSection", () => {
44
44
  assert.match(html, /Acme Holdings/);
45
45
  });
46
46
 
47
+ it("shows the folder name of a dotted path, not the whole path", () => {
48
+ const html = render([
49
+ {
50
+ ...base,
51
+ mailboxPath: "INBOX.Projects.Q3",
52
+ mailboxDelimiter: ".",
53
+ mailboxRole: undefined,
54
+ },
55
+ ]);
56
+ assert.match(html, />Q3</);
57
+ assert.doesNotMatch(html, />INBOX\.Projects\.Q3</);
58
+ });
59
+
47
60
  it("offers reporting as the only per-row action", () => {
48
61
  const html = render([base]);
49
62
  assert.match(html, /Cut a bug/);
@@ -235,7 +235,10 @@ describe("SearchResults provenance labels", () => {
235
235
  {
236
236
  ...result,
237
237
  id: "c1",
238
- folder: { providerPath: "Projects/Books" },
238
+ folder: {
239
+ providerPath: "Projects/Books",
240
+ hierarchyDelimiter: "/",
241
+ },
239
242
  },
240
243
  ],
241
244
  },
@@ -274,7 +277,10 @@ describe("SearchResults provenance labels", () => {
274
277
  {
275
278
  ...result,
276
279
  id: "v3",
277
- folder: { providerPath: "[Gmail]/Important" },
280
+ folder: {
281
+ providerPath: "[Gmail]/Important",
282
+ hierarchyDelimiter: "/",
283
+ },
278
284
  },
279
285
  ],
280
286
  },
@@ -112,7 +112,7 @@ const crossFolderMatches: SearchResult[] = [
112
112
  subject: "Invoices for the quarter",
113
113
  snippet: "The quarterly set, filed with the rest of the bookkeeping.",
114
114
  date: "Jan 30",
115
- folder: { providerPath: "Projects/Bookkeeping" },
115
+ folder: { providerPath: "Projects/Bookkeeping", hierarchyDelimiter: "/" },
116
116
  },
117
117
  ];
118
118
 
@@ -355,7 +355,10 @@ export const VirtualFoldersGoUnlabelled: Story = {
355
355
  {
356
356
  ...topMatches[2],
357
357
  id: "v3",
358
- folder: { providerPath: "[Gmail]/Important" },
358
+ folder: {
359
+ providerPath: "[Gmail]/Important",
360
+ hierarchyDelimiter: "/",
361
+ },
359
362
  },
360
363
  ...crossFolderMatches.slice(0, 1),
361
364
  ],
package/src/index.ts CHANGED
@@ -354,7 +354,6 @@ export {
354
354
  type FolderRole,
355
355
  isVirtualFolderRole,
356
356
  provenanceFolderLabel,
357
- providerLeaf,
358
357
  type ResultFolder,
359
358
  roleIcon,
360
359
  } from "./components/folder-role.js";