@remit/web-client 0.0.103 → 0.0.105
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.
|
|
3
|
+
"version": "0.0.105",
|
|
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": {
|
|
@@ -1,32 +1,3 @@
|
|
|
1
|
-
export const threadKeys = {
|
|
2
|
-
all: ["threads"] as const,
|
|
3
|
-
|
|
4
|
-
lists: () => [...threadKeys.all, "list"] as const,
|
|
5
|
-
|
|
6
|
-
list: (
|
|
7
|
-
mailboxId: string,
|
|
8
|
-
filters?: { filter?: string; page?: number; cursor?: string },
|
|
9
|
-
) => [...threadKeys.lists(), mailboxId, filters] as const,
|
|
10
|
-
|
|
11
|
-
details: () => [...threadKeys.all, "detail"] as const,
|
|
12
|
-
|
|
13
|
-
detail: (threadId: string) => [...threadKeys.details(), threadId] as const,
|
|
14
|
-
|
|
15
|
-
messages: (threadId: string) =>
|
|
16
|
-
[...threadKeys.detail(threadId), "messages"] as const,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const mailboxKeys = {
|
|
20
|
-
all: ["mailboxes"] as const,
|
|
21
|
-
|
|
22
|
-
lists: () => [...mailboxKeys.all, "list"] as const,
|
|
23
|
-
|
|
24
|
-
list: (accountId: string) => [...mailboxKeys.lists(), accountId] as const,
|
|
25
|
-
|
|
26
|
-
detail: (mailboxId: string) =>
|
|
27
|
-
[...mailboxKeys.all, "detail", mailboxId] as const,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
1
|
export const messageKeys = {
|
|
31
2
|
all: ["messages"] as const,
|
|
32
3
|
|
|
@@ -61,12 +61,15 @@ describe("buildMoveTargets — excluded destinations (#236, #976)", () => {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
test("with no appointments at all, nothing is excluded by role (only Outbox by name)", () => {
|
|
64
|
-
const result = buildMoveTargets(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
const result = buildMoveTargets(
|
|
65
|
+
[
|
|
66
|
+
make({ mailboxId: "m1", fullPath: "INBOX" }),
|
|
67
|
+
make({ mailboxId: "m2", fullPath: "Drafts" }),
|
|
68
|
+
make({ mailboxId: "m3", fullPath: "Outbox" }),
|
|
69
|
+
make({ mailboxId: "m4", fullPath: "Sent Mail" }),
|
|
70
|
+
],
|
|
71
|
+
[],
|
|
72
|
+
);
|
|
70
73
|
const ids = result.map((mailbox) => mailbox.mailboxId).sort();
|
|
71
74
|
assert.deepStrictEqual(ids, ["m1", "m2", "m4"]);
|
|
72
75
|
});
|
|
@@ -89,11 +92,14 @@ describe("buildMoveTargets — excluded destinations (#236, #976)", () => {
|
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
test("keeps user-defined folders", () => {
|
|
92
|
-
const result = buildMoveTargets(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const result = buildMoveTargets(
|
|
96
|
+
[
|
|
97
|
+
make({ mailboxId: "m1", fullPath: "INBOX" }),
|
|
98
|
+
make({ mailboxId: "m2", fullPath: "Project Alpha" }),
|
|
99
|
+
make({ mailboxId: "m3", fullPath: "Receipts/2025" }),
|
|
100
|
+
],
|
|
101
|
+
[],
|
|
102
|
+
);
|
|
97
103
|
const ids = result.map((mailbox) => mailbox.mailboxId);
|
|
98
104
|
assert.deepStrictEqual(ids.sort(), ["m1", "m2", "m3"]);
|
|
99
105
|
});
|
|
@@ -121,10 +127,13 @@ describe("buildMoveTargets — sort order", () => {
|
|
|
121
127
|
});
|
|
122
128
|
|
|
123
129
|
test("plain folders sort alphabetically, case-insensitively", () => {
|
|
124
|
-
const result = buildMoveTargets(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
const result = buildMoveTargets(
|
|
131
|
+
[
|
|
132
|
+
make({ mailboxId: "b", fullPath: "banana" }),
|
|
133
|
+
make({ mailboxId: "a", fullPath: "Apple" }),
|
|
134
|
+
],
|
|
135
|
+
[],
|
|
136
|
+
);
|
|
128
137
|
assert.deepStrictEqual(
|
|
129
138
|
result.map((m) => m.mailboxId),
|
|
130
139
|
["a", "b"],
|
|
@@ -150,45 +159,57 @@ describe("buildMoveTargets — Outbox locale exclusion (#290)", () => {
|
|
|
150
159
|
|
|
151
160
|
for (const name of localizedOutboxNames) {
|
|
152
161
|
test(`drops localized Outbox "${name}"`, () => {
|
|
153
|
-
const result = buildMoveTargets(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
const result = buildMoveTargets(
|
|
163
|
+
[
|
|
164
|
+
make({ mailboxId: "inbox", fullPath: "INBOX" }),
|
|
165
|
+
make({ mailboxId: "outbox", fullPath: name }),
|
|
166
|
+
],
|
|
167
|
+
[],
|
|
168
|
+
);
|
|
157
169
|
const ids = result.map((mailbox) => mailbox.mailboxId);
|
|
158
170
|
assert.deepStrictEqual(ids, ["inbox"]);
|
|
159
171
|
});
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
test("matches Outbox locale names case-insensitively", () => {
|
|
163
|
-
const result = buildMoveTargets(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
const result = buildMoveTargets(
|
|
176
|
+
[
|
|
177
|
+
make({ mailboxId: "inbox", fullPath: "INBOX" }),
|
|
178
|
+
make({ mailboxId: "m1", fullPath: "POSTVAK UIT" }),
|
|
179
|
+
make({ mailboxId: "m2", fullPath: "postausgang" }),
|
|
180
|
+
make({ mailboxId: "m3", fullPath: "BOÎTE D'ENVOI" }),
|
|
181
|
+
],
|
|
182
|
+
[],
|
|
183
|
+
);
|
|
169
184
|
const ids = result.map((mailbox) => mailbox.mailboxId);
|
|
170
185
|
assert.deepStrictEqual(ids, ["inbox"]);
|
|
171
186
|
});
|
|
172
187
|
|
|
173
188
|
test("matches Outbox locale names with surrounding whitespace", () => {
|
|
174
|
-
const result = buildMoveTargets(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
189
|
+
const result = buildMoveTargets(
|
|
190
|
+
[
|
|
191
|
+
make({ mailboxId: "inbox", fullPath: "INBOX" }),
|
|
192
|
+
make({ mailboxId: "m1", fullPath: " Outbox " }),
|
|
193
|
+
make({ mailboxId: "m2", fullPath: " Postvak UIT " }),
|
|
194
|
+
],
|
|
195
|
+
[],
|
|
196
|
+
);
|
|
179
197
|
const ids = result.map((mailbox) => mailbox.mailboxId);
|
|
180
198
|
assert.deepStrictEqual(ids, ["inbox"]);
|
|
181
199
|
});
|
|
182
200
|
|
|
183
201
|
test("does not over-match unrelated mailbox names", () => {
|
|
184
|
-
const result = buildMoveTargets(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
const result = buildMoveTargets(
|
|
203
|
+
[
|
|
204
|
+
make({ mailboxId: "inbox", fullPath: "INBOX" }),
|
|
205
|
+
make({ mailboxId: "trash", fullPath: "Trash" }),
|
|
206
|
+
make({ mailboxId: "custom", fullPath: "Custom Folder" }),
|
|
207
|
+
// Substring of a locale name must not trigger the filter — only
|
|
208
|
+
// the exact normalized leaf matches.
|
|
209
|
+
make({ mailboxId: "outboxy", fullPath: "Outbox Archive" }),
|
|
210
|
+
],
|
|
211
|
+
[],
|
|
212
|
+
);
|
|
192
213
|
const ids = result.map((mailbox) => mailbox.mailboxId).sort();
|
|
193
214
|
assert.deepStrictEqual(ids, ["custom", "inbox", "outboxy", "trash"]);
|
|
194
215
|
});
|
package/src/lib/move-targets.ts
CHANGED
|
@@ -49,7 +49,7 @@ const NON_SYSTEM_PRIORITY = ROLE_PRIORITY.length;
|
|
|
49
49
|
*/
|
|
50
50
|
export const buildMoveTargets = (
|
|
51
51
|
mailboxes: readonly RemitImapMailboxResponse[],
|
|
52
|
-
folderAppointments: readonly RemitImapFolderAppointment[]
|
|
52
|
+
folderAppointments: readonly RemitImapFolderAppointment[],
|
|
53
53
|
): RemitImapMailboxResponse[] => {
|
|
54
54
|
const roleMap = buildMailboxRoleMap(folderAppointments);
|
|
55
55
|
const filtered = mailboxes.filter((mailbox) => {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { afterEach, describe, it } from "node:test";
|
|
4
|
+
import type { RemitImapFilterResponse } from "@remit/api-http-client/types.gen.ts";
|
|
5
|
+
import { createElement } from "react";
|
|
6
|
+
import { createDomHarness, type DomHarness } from "../../test-support/dom";
|
|
7
|
+
import { makeAccount, makeMailbox } from "../../test-support/fixtures";
|
|
8
|
+
import { type HttpMock, mockFetch } from "../../test-support/http";
|
|
9
|
+
import { AccountFilters } from "./filters.tsx";
|
|
10
|
+
|
|
11
|
+
let harness: DomHarness | undefined;
|
|
12
|
+
let http: HttpMock | undefined;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
harness?.close();
|
|
16
|
+
harness = undefined;
|
|
17
|
+
http?.restore();
|
|
18
|
+
http = undefined;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const ACCOUNT_ID = "acc-1";
|
|
22
|
+
|
|
23
|
+
const account = makeAccount({
|
|
24
|
+
accountId: ACCOUNT_ID,
|
|
25
|
+
folderAppointments: [
|
|
26
|
+
{ role: "Inbox", mailboxId: "mbx-inbox" },
|
|
27
|
+
{ role: "Drafts", mailboxId: "mbx-concepten" },
|
|
28
|
+
{ role: "Sent", mailboxId: "mbx-verzonden" },
|
|
29
|
+
{ role: "Archive", mailboxId: "mbx-archief" },
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Dutch leaf names for the appointed Drafts and Sent: only the appointment can
|
|
34
|
+
// exclude them, so a name-based filter would not pass this.
|
|
35
|
+
const mailboxes = [
|
|
36
|
+
makeMailbox({ mailboxId: "mbx-receipts", fullPath: "INBOX/Receipts" }),
|
|
37
|
+
makeMailbox({ mailboxId: "mbx-concepten", fullPath: "INBOX/Concepten" }),
|
|
38
|
+
makeMailbox({ mailboxId: "mbx-verzonden", fullPath: "INBOX/Verzonden" }),
|
|
39
|
+
makeMailbox({ mailboxId: "mbx-archief", fullPath: "INBOX/Archief" }),
|
|
40
|
+
makeMailbox({ mailboxId: "mbx-inbox", fullPath: "INBOX" }),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const filter: RemitImapFilterResponse = {
|
|
44
|
+
filterId: "f-1",
|
|
45
|
+
accountConfigId: ACCOUNT_ID,
|
|
46
|
+
name: "Receipts",
|
|
47
|
+
scope: "Standing",
|
|
48
|
+
state: "Active",
|
|
49
|
+
hasAnchor: false,
|
|
50
|
+
ruleChangedAt: 0,
|
|
51
|
+
actionChangedAt: 0,
|
|
52
|
+
matchOperator: "And",
|
|
53
|
+
literalClauses: [{ field: "From", value: "receipts@stripe.com" }],
|
|
54
|
+
actionLabelId: "None",
|
|
55
|
+
actionMailboxId: "mbx-receipts",
|
|
56
|
+
createdAt: 0,
|
|
57
|
+
updatedAt: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wait on the DOM answering rather than on a turn count: how much React work
|
|
62
|
+
* lands per turn varies with the machine, so a fixed flush is a flake and a
|
|
63
|
+
* ceiling is what keeps a surface that never renders a failure, not a hang.
|
|
64
|
+
*/
|
|
65
|
+
const settleUntil = async (
|
|
66
|
+
dom: DomHarness,
|
|
67
|
+
done: () => boolean,
|
|
68
|
+
): Promise<void> => {
|
|
69
|
+
const deadline = Date.now() + 5000;
|
|
70
|
+
for (;;) {
|
|
71
|
+
await dom.flush();
|
|
72
|
+
if (done() || Date.now() >= deadline) return;
|
|
73
|
+
await dom.wait(10);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const offeredDestinations = async (): Promise<string[]> => {
|
|
78
|
+
http = mockFetch((call) => {
|
|
79
|
+
if (call.path.endsWith("/mailboxes")) return { items: mailboxes };
|
|
80
|
+
if (call.path.endsWith("/filters")) return { items: [filter] };
|
|
81
|
+
if (call.path.endsWith("/labels")) return { items: [] };
|
|
82
|
+
if (call.path.endsWith("/organize/preview"))
|
|
83
|
+
return { matchedCount: 0, messageIds: [] };
|
|
84
|
+
return {};
|
|
85
|
+
});
|
|
86
|
+
const dom = createDomHarness();
|
|
87
|
+
harness = dom;
|
|
88
|
+
dom.renderApp(createElement(AccountFilters, { account }));
|
|
89
|
+
await settleUntil(
|
|
90
|
+
dom,
|
|
91
|
+
() => !!dom.query('[aria-label="Edit filter Receipts"]'),
|
|
92
|
+
);
|
|
93
|
+
dom.click(dom.byLabel("Edit filter Receipts"));
|
|
94
|
+
await settleUntil(
|
|
95
|
+
dom,
|
|
96
|
+
() => !!dom.query('select[aria-label="Destination folder"]'),
|
|
97
|
+
);
|
|
98
|
+
return dom
|
|
99
|
+
.queryAll<HTMLOptionElement>(
|
|
100
|
+
'select[aria-label="Destination folder"] option',
|
|
101
|
+
)
|
|
102
|
+
.map((option) => option.value)
|
|
103
|
+
.filter((value) => value.startsWith("mbx-"));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
describe("Settings › Filters — move destinations (#236, #540)", () => {
|
|
107
|
+
it("offers every folder but the appointed Drafts and Sent, in role order", async () => {
|
|
108
|
+
assert.deepEqual(await offeredDestinations(), [
|
|
109
|
+
"mbx-inbox",
|
|
110
|
+
"mbx-archief",
|
|
111
|
+
"mbx-receipts",
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -34,7 +34,11 @@ const filtersHelp = (
|
|
|
34
34
|
</div>
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
-
function AccountFilters({
|
|
37
|
+
export function AccountFilters({
|
|
38
|
+
account,
|
|
39
|
+
}: {
|
|
40
|
+
account: RemitImapAccountResponse;
|
|
41
|
+
}) {
|
|
38
42
|
const accountId = account.accountId;
|
|
39
43
|
const { filters, isPending, isError, error, refetch } =
|
|
40
44
|
useFilterList(accountId);
|
|
@@ -58,11 +62,14 @@ function AccountFilters({ account }: { account: RemitImapAccountResponse }) {
|
|
|
58
62
|
|
|
59
63
|
const folders = useMemo(
|
|
60
64
|
() =>
|
|
61
|
-
buildMoveTargets(
|
|
65
|
+
buildMoveTargets(
|
|
66
|
+
mailboxesData?.items ?? [],
|
|
67
|
+
account.folderAppointments,
|
|
68
|
+
).map((mailbox) => ({
|
|
62
69
|
id: mailbox.mailboxId,
|
|
63
70
|
label: getMailboxDisplayName(mailbox.fullPath),
|
|
64
71
|
})),
|
|
65
|
-
[mailboxesData?.items],
|
|
72
|
+
[mailboxesData?.items, account.folderAppointments],
|
|
66
73
|
);
|
|
67
74
|
|
|
68
75
|
const { labels: labelItems } = useLabelList(accountId);
|