@remit/web-client 0.0.276 → 0.0.278
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 +1 -1
- package/src/components/mail/IntelligencePane.tsx +27 -1
- package/src/components/mail/reclassify-remove-override.render.test.ts +142 -0
- package/src/hooks/useUpdateAddressFlags.ts +35 -10
- package/src/routes/settings/-senders-remove-flag.render.test.ts +105 -0
- package/src/routes/settings/senders.tsx +7 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.278",
|
|
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": {
|
|
@@ -159,16 +159,25 @@ function IntelligenceSkeleton() {
|
|
|
159
159
|
/**
|
|
160
160
|
* Reclassify picker: a small modal listing the category-override options. On
|
|
161
161
|
* selection it PATCHes `AddressFlags.category` for the sender.
|
|
162
|
+
*
|
|
163
|
+
* A sender that already carries an override also gets the way back out. The
|
|
164
|
+
* override is an enum with no false-equivalent member, so removal is not a
|
|
165
|
+
* value to send — it is `clearFlags: ["category"]`, the contract's removal
|
|
166
|
+
* form for every flag (#615).
|
|
162
167
|
*/
|
|
163
168
|
function ReclassifyDialog({
|
|
164
169
|
isOpen,
|
|
165
170
|
current,
|
|
171
|
+
hasOverride,
|
|
166
172
|
onSelect,
|
|
173
|
+
onClear,
|
|
167
174
|
onCancel,
|
|
168
175
|
}: {
|
|
169
176
|
isOpen: boolean;
|
|
170
177
|
current: string;
|
|
178
|
+
hasOverride: boolean;
|
|
171
179
|
onSelect: (category: CategoryOverride) => void;
|
|
180
|
+
onClear: () => void;
|
|
172
181
|
onCancel: () => void;
|
|
173
182
|
}) {
|
|
174
183
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
@@ -210,6 +219,15 @@ function ReclassifyDialog({
|
|
|
210
219
|
)}
|
|
211
220
|
</button>
|
|
212
221
|
))}
|
|
222
|
+
{hasOverride && (
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={onClear}
|
|
226
|
+
className="mt-1 flex min-h-11 items-center rounded border-t border-line px-3 text-left text-sm text-fg transition-colors hover:bg-surface-raised"
|
|
227
|
+
>
|
|
228
|
+
Remove override — classify automatically again
|
|
229
|
+
</button>
|
|
230
|
+
)}
|
|
213
231
|
</div>
|
|
214
232
|
<div className="mt-6 flex justify-end">
|
|
215
233
|
<button
|
|
@@ -247,6 +265,7 @@ function WiredPanel({
|
|
|
247
265
|
}: WiredPanelProps) {
|
|
248
266
|
const {
|
|
249
267
|
data,
|
|
268
|
+
address,
|
|
250
269
|
addressId,
|
|
251
270
|
isSimilarLoading,
|
|
252
271
|
similarError,
|
|
@@ -256,7 +275,7 @@ function WiredPanel({
|
|
|
256
275
|
const [reclassifyOpen, setReclassifyOpen] = useState(false);
|
|
257
276
|
const senderEmail = thread.fromEmail ?? undefined;
|
|
258
277
|
|
|
259
|
-
const { updateFlags } = useUpdateAddressFlags({
|
|
278
|
+
const { updateFlags, clearFlags } = useUpdateAddressFlags({
|
|
260
279
|
addressId,
|
|
261
280
|
senderEmail,
|
|
262
281
|
});
|
|
@@ -315,6 +334,11 @@ function WiredPanel({
|
|
|
315
334
|
[updateFlags],
|
|
316
335
|
);
|
|
317
336
|
|
|
337
|
+
const handleReclassifyClear = useCallback(() => {
|
|
338
|
+
setReclassifyOpen(false);
|
|
339
|
+
clearFlags(["category"]);
|
|
340
|
+
}, [clearFlags]);
|
|
341
|
+
|
|
318
342
|
// Every per-sender flag toggle PATCHes the sender's address row, so none of
|
|
319
343
|
// them can be serviced until that row resolves. Leave them unwired until it
|
|
320
344
|
// does: the panel renders an unwired quick action as disabled, so the flow is
|
|
@@ -383,7 +407,9 @@ function WiredPanel({
|
|
|
383
407
|
<ReclassifyDialog
|
|
384
408
|
isOpen={reclassifyOpen}
|
|
385
409
|
current={data.category.value}
|
|
410
|
+
hasOverride={address?.flags?.category?.value != null}
|
|
386
411
|
onSelect={handleReclassifySelect}
|
|
412
|
+
onClear={handleReclassifyClear}
|
|
387
413
|
onCancel={() => setReclassifyOpen(false)}
|
|
388
414
|
/>
|
|
389
415
|
</>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing a sender's category override (#615).
|
|
3
|
+
*
|
|
4
|
+
* A sender flag is removed by naming its key in `clearFlags`. `category` is the
|
|
5
|
+
* flag that has no other way out: its value is a `MessageCategory` enum with no
|
|
6
|
+
* false-equivalent member, so the `{ value: false }` form the boolean flags use
|
|
7
|
+
* cannot express "no override", and the `null` form the schema documents is
|
|
8
|
+
* rejected by the request validator. Before this the reclassify picker was
|
|
9
|
+
* one-way — a reader could set an override and never take it back.
|
|
10
|
+
*
|
|
11
|
+
* Drives the real chain: the real `IntelligencePane`, the real
|
|
12
|
+
* `useUpdateAddressFlags` mutation, and a fetch mock that answers the way the
|
|
13
|
+
* server does. Both halves are asserted — the body that goes on the wire, and
|
|
14
|
+
* that the override is gone from what the panel renders.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { afterEach, describe, it } from "node:test";
|
|
19
|
+
import type { RemitImapAddressResponse } from "@remit/api-http-client/types.gen.ts";
|
|
20
|
+
import { createElement } from "react";
|
|
21
|
+
import { createDomHarness, type DomHarness } from "@/test-support/dom";
|
|
22
|
+
import {
|
|
23
|
+
makeAccount,
|
|
24
|
+
makeConfig,
|
|
25
|
+
makeThreadMessage,
|
|
26
|
+
} from "@/test-support/fixtures";
|
|
27
|
+
import { type HttpMock, mockFetch } from "@/test-support/http";
|
|
28
|
+
import { IntelligencePane } from "./IntelligencePane";
|
|
29
|
+
|
|
30
|
+
const thread = makeThreadMessage({ messageId: "msg-1", category: "personal" });
|
|
31
|
+
|
|
32
|
+
const addressWith = (
|
|
33
|
+
flags: RemitImapAddressResponse["flags"],
|
|
34
|
+
): RemitImapAddressResponse => ({
|
|
35
|
+
addressId: "addr-1",
|
|
36
|
+
accountConfigId: "cfg-1",
|
|
37
|
+
displayName: "Alice",
|
|
38
|
+
localPart: "alice",
|
|
39
|
+
domain: "example.com",
|
|
40
|
+
normalizedEmail: "alice@example.com",
|
|
41
|
+
flags,
|
|
42
|
+
inboundCount: 6,
|
|
43
|
+
outboundCount: 1,
|
|
44
|
+
replyCount: 2,
|
|
45
|
+
lastInboundAt: 1_000,
|
|
46
|
+
lastReplyAt: 900,
|
|
47
|
+
createdAt: 0,
|
|
48
|
+
updatedAt: 0,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let harness: DomHarness | undefined;
|
|
52
|
+
let http: HttpMock;
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
harness?.close();
|
|
56
|
+
harness = undefined;
|
|
57
|
+
http.restore();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("the reclassify picker on a sender that already carries an override", () => {
|
|
61
|
+
it("removes the override with clearFlags, and the panel stops claiming one", async () => {
|
|
62
|
+
let stored: RemitImapAddressResponse = addressWith({
|
|
63
|
+
category: { value: "newsletter", setAt: 10 },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
http = mockFetch((call) => {
|
|
67
|
+
if (call.path.endsWith("/config")) {
|
|
68
|
+
return makeConfig([makeAccount({ accountId: "acc-1" })], {
|
|
69
|
+
semanticSearchEnabled: false,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (call.path.endsWith("/addresses/search")) {
|
|
73
|
+
return { items: [stored] };
|
|
74
|
+
}
|
|
75
|
+
if (call.method === "PATCH" && call.path.endsWith("/addresses/addr-1")) {
|
|
76
|
+
stored = addressWith({});
|
|
77
|
+
return stored;
|
|
78
|
+
}
|
|
79
|
+
return { items: [] };
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
harness = createDomHarness();
|
|
83
|
+
harness.renderApp(
|
|
84
|
+
createElement(IntelligencePane, { thread, onClose: () => {} }),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
await harness.waitFor(
|
|
88
|
+
() => harness?.text().includes("your override") === true,
|
|
89
|
+
"the panel never showed the sender's category override",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
harness.click(harness.byText("button", "reclassify"));
|
|
93
|
+
harness.click(
|
|
94
|
+
harness.byText(
|
|
95
|
+
"button",
|
|
96
|
+
"Remove override — classify automatically again",
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
await harness.waitFor(
|
|
101
|
+
() => http.to("/addresses/addr-1").length === 1,
|
|
102
|
+
"the remove control sent no request",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
assert.deepEqual(http.to("/addresses/addr-1")[0].body, {
|
|
106
|
+
clearFlags: ["category"],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await harness.waitFor(
|
|
110
|
+
() => harness?.text().includes("your override") === false,
|
|
111
|
+
"the override survived its own removal",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("offers no removal on a sender that carries no override", async () => {
|
|
116
|
+
http = mockFetch((call) => {
|
|
117
|
+
if (call.path.endsWith("/config")) {
|
|
118
|
+
return makeConfig([makeAccount({ accountId: "acc-1" })], {
|
|
119
|
+
semanticSearchEnabled: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (call.path.endsWith("/addresses/search")) {
|
|
123
|
+
return { items: [addressWith({})] };
|
|
124
|
+
}
|
|
125
|
+
return { items: [] };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
harness = createDomHarness();
|
|
129
|
+
harness.renderApp(
|
|
130
|
+
createElement(IntelligencePane, { thread, onClose: () => {} }),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await harness.waitFor(
|
|
134
|
+
() => harness?.text().includes("reclassify") === true,
|
|
135
|
+
"the panel never rendered the category section",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
harness.click(harness.byText("button", "reclassify"));
|
|
139
|
+
|
|
140
|
+
assert.equal(harness.text().includes("Remove override"), false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
5
5
|
import type {
|
|
6
6
|
AddressOperationsSearchAddressesResponse,
|
|
7
|
+
RemitImapAddressFlagKey,
|
|
7
8
|
RemitImapAddressFlags,
|
|
8
9
|
RemitImapUpdateAddressFlagsInput,
|
|
9
10
|
} from "@remit/api-http-client/types.gen.ts";
|
|
@@ -25,15 +26,17 @@ interface MutationContext {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Apply a flag
|
|
29
|
-
* `patch` is a flag update object
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* Apply a flag update onto an existing AddressFlags object. Each key in
|
|
30
|
+
* `patch` is a flag update object; each key in `clear` is removed outright,
|
|
31
|
+
* after the patch, matching the server's ordering. Mirrors the server-side
|
|
32
|
+
* merge semantics so the optimistic cache matches what the API returns. Typed
|
|
33
|
+
* loosely (the per-key flag value types differ — boolean for most, a category
|
|
34
|
+
* enum for `category`) and re-narrowed at the return.
|
|
33
35
|
*/
|
|
34
36
|
function applyFlagPatch(
|
|
35
37
|
current: RemitImapAddressFlags | undefined,
|
|
36
38
|
patch: RemitImapUpdateAddressFlagsInput,
|
|
39
|
+
clear: readonly RemitImapAddressFlagKey[] = [],
|
|
37
40
|
): RemitImapAddressFlags {
|
|
38
41
|
const next: Record<string, unknown> = { ...(current ?? {}) };
|
|
39
42
|
for (const [key, update] of Object.entries(patch)) {
|
|
@@ -45,6 +48,9 @@ function applyFlagPatch(
|
|
|
45
48
|
next[key] = { ...update, setAt: update.setAt ?? Date.now() };
|
|
46
49
|
}
|
|
47
50
|
}
|
|
51
|
+
for (const key of clear) {
|
|
52
|
+
delete next[key];
|
|
53
|
+
}
|
|
48
54
|
return next as RemitImapAddressFlags;
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -69,6 +75,7 @@ export function useUpdateAddressFlags({
|
|
|
69
75
|
...addressDetailOperationsUpdateAddressMutation(),
|
|
70
76
|
onMutate: async (vars): Promise<MutationContext> => {
|
|
71
77
|
const patch = vars.body.flags ?? {};
|
|
78
|
+
const clear = vars.body.clearFlags ?? [];
|
|
72
79
|
await queryClient.cancelQueries({ queryKey: addressCacheKey });
|
|
73
80
|
|
|
74
81
|
const previous =
|
|
@@ -84,7 +91,7 @@ export function useUpdateAddressFlags({
|
|
|
84
91
|
...old,
|
|
85
92
|
items: old.items.map((addr) =>
|
|
86
93
|
addr.addressId === vars.path.addressId
|
|
87
|
-
? { ...addr, flags: applyFlagPatch(addr.flags, patch) }
|
|
94
|
+
? { ...addr, flags: applyFlagPatch(addr.flags, patch, clear) }
|
|
88
95
|
: addr,
|
|
89
96
|
),
|
|
90
97
|
};
|
|
@@ -108,8 +115,11 @@ export function useUpdateAddressFlags({
|
|
|
108
115
|
},
|
|
109
116
|
});
|
|
110
117
|
|
|
111
|
-
const
|
|
112
|
-
(
|
|
118
|
+
const submit = useCallback(
|
|
119
|
+
(body: {
|
|
120
|
+
flags?: RemitImapUpdateAddressFlagsInput;
|
|
121
|
+
clearFlags?: RemitImapAddressFlagKey[];
|
|
122
|
+
}) => {
|
|
113
123
|
if (!addressId) {
|
|
114
124
|
reportFatalError(
|
|
115
125
|
new Error(
|
|
@@ -118,10 +128,25 @@ export function useUpdateAddressFlags({
|
|
|
118
128
|
);
|
|
119
129
|
return;
|
|
120
130
|
}
|
|
121
|
-
mutate({ path: { addressId }, body
|
|
131
|
+
mutate({ path: { addressId }, body });
|
|
122
132
|
},
|
|
123
133
|
[addressId, mutate, senderEmail],
|
|
124
134
|
);
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
const updateFlags = useCallback(
|
|
137
|
+
(flags: RemitImapUpdateAddressFlagsInput) => submit({ flags }),
|
|
138
|
+
[submit],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Remove flags outright. This is the removal form for every flag whatever
|
|
143
|
+
* its value type: `category` holds an enum with no false-equivalent member,
|
|
144
|
+
* so `{ value: false }` cannot express "no override" for it.
|
|
145
|
+
*/
|
|
146
|
+
const clearFlags = useCallback(
|
|
147
|
+
(keys: RemitImapAddressFlagKey[]) => submit({ clearFlags: keys }),
|
|
148
|
+
[submit],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { updateFlags, clearFlags, isPending };
|
|
127
152
|
}
|
|
@@ -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
|
/>
|