@remit/web-client 0.0.276 → 0.0.277
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.277",
|
|
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": {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
|
|
2
|
+
/**
|
|
3
|
+
* Issue #615: the remove control in Settings > Senders sent `{ flags: {
|
|
4
|
+
* [group]: null } }`. The server's openapi-backend validator doesn't honour
|
|
5
|
+
* `nullable: true` on the `allOf`-wrapped flag schema TypeSpec emits for
|
|
6
|
+
* `TrustedFlag | null`, so it rejects the null form as invalid — the same gap
|
|
7
|
+
* `useToggleTrusted` already works around by sending `{ value: false }`
|
|
8
|
+
* instead of `null`. This mock server enforces that same rejection, so the
|
|
9
|
+
* pre-fix `null` body surfaces as a failed mutation (the row never clears)
|
|
10
|
+
* rather than a silently accepted no-op.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { afterEach, describe, it } from "node:test";
|
|
15
|
+
import type { RemitImapAddressResponse } from "@remit/api-http-client/types.gen.ts";
|
|
16
|
+
import { createElement } from "react";
|
|
17
|
+
import { createDomHarness, type DomHarness } from "@/test-support/dom";
|
|
18
|
+
import {
|
|
19
|
+
type HttpCall,
|
|
20
|
+
type HttpMock,
|
|
21
|
+
httpError,
|
|
22
|
+
mockFetch,
|
|
23
|
+
} from "@/test-support/http";
|
|
24
|
+
import { SearchGroupPane } from "./senders";
|
|
25
|
+
|
|
26
|
+
const ADDRESS_ID = "addr-1";
|
|
27
|
+
|
|
28
|
+
let harness: DomHarness | undefined;
|
|
29
|
+
let http: HttpMock | undefined;
|
|
30
|
+
let mutedFlag: { value: boolean; setAt: number } | undefined;
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
harness?.close();
|
|
34
|
+
harness = undefined;
|
|
35
|
+
http?.restore();
|
|
36
|
+
http = undefined;
|
|
37
|
+
mutedFlag = undefined;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const address = (): RemitImapAddressResponse =>
|
|
41
|
+
({
|
|
42
|
+
addressId: ADDRESS_ID,
|
|
43
|
+
displayName: "Alex Rivera",
|
|
44
|
+
normalizedEmail: "alex@example.com",
|
|
45
|
+
flags: mutedFlag ? { muted: mutedFlag } : {},
|
|
46
|
+
}) as RemitImapAddressResponse;
|
|
47
|
+
|
|
48
|
+
const readFlagsPatch = (call: HttpCall): unknown => {
|
|
49
|
+
const body = call.body as { flags?: Record<string, unknown> } | undefined;
|
|
50
|
+
return body?.flags?.muted;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const respond = (call: HttpCall): unknown => {
|
|
54
|
+
if (call.method === "GET" && call.path === "/addresses/search") {
|
|
55
|
+
return { items: [address()] };
|
|
56
|
+
}
|
|
57
|
+
if (call.method === "PATCH" && call.path === `/addresses/${ADDRESS_ID}`) {
|
|
58
|
+
const patch = readFlagsPatch(call);
|
|
59
|
+
if (patch === null) {
|
|
60
|
+
return httpError(400, "flags.muted: nullable removal is not accepted");
|
|
61
|
+
}
|
|
62
|
+
mutedFlag = patch as { value: boolean; setAt: number };
|
|
63
|
+
return address();
|
|
64
|
+
}
|
|
65
|
+
return {};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const mount = async (): Promise<void> => {
|
|
69
|
+
mutedFlag = { value: true, setAt: 1_700_000_000_000 };
|
|
70
|
+
http = mockFetch(respond);
|
|
71
|
+
harness = createDomHarness();
|
|
72
|
+
harness.renderApp(
|
|
73
|
+
createElement(SearchGroupPane, { group: "muted", query: "alex" }),
|
|
74
|
+
);
|
|
75
|
+
await harness.waitFor(
|
|
76
|
+
() => harness?.text().includes("alex@example.com") ?? false,
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe("removing a sender flag (#615)", () => {
|
|
81
|
+
it("sends a request body the server accepts, and the flag clears from the list", async () => {
|
|
82
|
+
await mount();
|
|
83
|
+
if (!harness) throw new Error("nothing mounted");
|
|
84
|
+
|
|
85
|
+
harness.click(harness.byLabel("Remove muted flag"));
|
|
86
|
+
|
|
87
|
+
await harness.waitFor(
|
|
88
|
+
() => !(harness?.text().includes("alex@example.com") ?? true),
|
|
89
|
+
"the unmuted sender to drop out of the muted list",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const patches = (http?.calls ?? []).filter(
|
|
93
|
+
(call) =>
|
|
94
|
+
call.method === "PATCH" && call.path === `/addresses/${ADDRESS_ID}`,
|
|
95
|
+
);
|
|
96
|
+
assert.equal(patches.length, 1);
|
|
97
|
+
const sentFlag = readFlagsPatch(patches[0]) as
|
|
98
|
+
| { value: boolean; setAt: number }
|
|
99
|
+
| null
|
|
100
|
+
| undefined;
|
|
101
|
+
assert.notEqual(sentFlag, null, "the server rejects a null flag removal");
|
|
102
|
+
assert.equal(sentFlag?.value, false);
|
|
103
|
+
assert.equal(typeof sentFlag?.setAt, "number");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -174,7 +174,7 @@ function VipGroupPane({
|
|
|
174
174
|
/* Muted / Blocked groups: search-based (backend gap noted) */
|
|
175
175
|
/* ------------------------------------------------------------------ */
|
|
176
176
|
|
|
177
|
-
function SearchGroupPane({
|
|
177
|
+
export function SearchGroupPane({
|
|
178
178
|
group,
|
|
179
179
|
query,
|
|
180
180
|
onCount,
|
|
@@ -303,7 +303,9 @@ function SearchGroupPane({
|
|
|
303
303
|
setRetryId(a.addressId);
|
|
304
304
|
removeMutation.mutate({
|
|
305
305
|
path: { addressId: a.addressId },
|
|
306
|
-
body: {
|
|
306
|
+
body: {
|
|
307
|
+
flags: { [group]: { value: false, setAt: Date.now() } },
|
|
308
|
+
},
|
|
307
309
|
});
|
|
308
310
|
}}
|
|
309
311
|
>
|
|
@@ -321,7 +323,9 @@ function SearchGroupPane({
|
|
|
321
323
|
setRetryId(a.addressId);
|
|
322
324
|
removeMutation.mutate({
|
|
323
325
|
path: { addressId: a.addressId },
|
|
324
|
-
body: {
|
|
326
|
+
body: {
|
|
327
|
+
flags: { [group]: { value: false, setAt: Date.now() } },
|
|
328
|
+
},
|
|
325
329
|
});
|
|
326
330
|
}}
|
|
327
331
|
/>
|