@remit/web-client 0.0.54 → 0.0.55
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/hooks/useFilters.test.ts +113 -0
- package/src/hooks/useFilters.ts +12 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFilters — the settings-page filter list read and the create/delete
|
|
3
|
+
* invalidation contract.
|
|
4
|
+
*
|
|
5
|
+
* `useFilterList` is exercised the same way `useRescueCandidates` is: a
|
|
6
|
+
* QueryClient is pre-seeded under the key the hook generates and the hook is
|
|
7
|
+
* rendered synchronously via renderToString, so the queryFn never fires and
|
|
8
|
+
* mapping drift breaks the assertions.
|
|
9
|
+
*
|
|
10
|
+
* Create and delete both invalidate `buildFilterListKey` on success; a drift
|
|
11
|
+
* between that key and the one the list subscribes to would leave the settings
|
|
12
|
+
* list stale after a filter is created or deleted, so the contract is pinned
|
|
13
|
+
* directly against the generated SDK key (the pattern `buildMailboxListKey`
|
|
14
|
+
* uses for trigger-sync).
|
|
15
|
+
*/
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { describe, test } from "node:test";
|
|
18
|
+
import { filterOperationsListFiltersQueryKey } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
19
|
+
import type { RemitImapFilterResponse } from "@remit/api-http-client/types.gen.ts";
|
|
20
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
21
|
+
import { createElement } from "react";
|
|
22
|
+
import { renderToString } from "react-dom/server";
|
|
23
|
+
import { buildFilterListKey, useFilterList } from "./useFilters.js";
|
|
24
|
+
|
|
25
|
+
const ACCOUNT_ID = "acc-1";
|
|
26
|
+
|
|
27
|
+
const filter = (
|
|
28
|
+
overrides: Partial<RemitImapFilterResponse> & { filterId: string },
|
|
29
|
+
): RemitImapFilterResponse =>
|
|
30
|
+
({
|
|
31
|
+
accountConfigId: "cfg-1",
|
|
32
|
+
name: "Travel",
|
|
33
|
+
scope: "Standing",
|
|
34
|
+
state: "Active",
|
|
35
|
+
hasAnchor: false,
|
|
36
|
+
ruleChangedAt: 0,
|
|
37
|
+
matchOperator: "And",
|
|
38
|
+
literalClauses: [],
|
|
39
|
+
actionLabelId: "None",
|
|
40
|
+
actionMailboxId: "None",
|
|
41
|
+
createdAt: 0,
|
|
42
|
+
updatedAt: 0,
|
|
43
|
+
...overrides,
|
|
44
|
+
}) as RemitImapFilterResponse;
|
|
45
|
+
|
|
46
|
+
function renderFilterList(
|
|
47
|
+
accountId: string | undefined,
|
|
48
|
+
seed?: RemitImapFilterResponse[],
|
|
49
|
+
): ReturnType<typeof useFilterList> {
|
|
50
|
+
const client = new QueryClient({
|
|
51
|
+
defaultOptions: { queries: { retry: false } },
|
|
52
|
+
});
|
|
53
|
+
if (accountId && seed) {
|
|
54
|
+
client.setQueryData(buildFilterListKey(accountId), { items: seed });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let captured = {} as ReturnType<typeof useFilterList>;
|
|
58
|
+
function Capture() {
|
|
59
|
+
captured = useFilterList(accountId);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
renderToString(
|
|
64
|
+
createElement(
|
|
65
|
+
QueryClientProvider,
|
|
66
|
+
{ client },
|
|
67
|
+
createElement(Capture),
|
|
68
|
+
) as never,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return captured;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("buildFilterListKey", () => {
|
|
75
|
+
test("matches the generated SDK key create/delete invalidate", () => {
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
buildFilterListKey(ACCOUNT_ID),
|
|
78
|
+
filterOperationsListFiltersQueryKey({ path: { accountId: ACCOUNT_ID } }),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("is distinct per account, so one account's list is not invalidated by another's write", () => {
|
|
83
|
+
assert.notDeepEqual(
|
|
84
|
+
buildFilterListKey("acc-1"),
|
|
85
|
+
buildFilterListKey("acc-2"),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("useFilterList", () => {
|
|
91
|
+
test("returns the account's filters from the cache", () => {
|
|
92
|
+
const { filters } = renderFilterList(ACCOUNT_ID, [
|
|
93
|
+
filter({ filterId: "f1", name: "Travel" }),
|
|
94
|
+
filter({ filterId: "f2", name: "Receipts", scope: "Temporary" }),
|
|
95
|
+
]);
|
|
96
|
+
assert.deepEqual(
|
|
97
|
+
filters.map((f) => f.name),
|
|
98
|
+
["Travel", "Receipts"],
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("returns an empty list, not undefined, before any data lands", () => {
|
|
103
|
+
const { filters } = renderFilterList(ACCOUNT_ID);
|
|
104
|
+
assert.deepEqual(filters, []);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("stays empty and disabled when no account is selected", () => {
|
|
108
|
+
const { filters, isPending } = renderFilterList(undefined);
|
|
109
|
+
assert.deepEqual(filters, []);
|
|
110
|
+
// `enabled: false` keeps the query from fetching for an absent account.
|
|
111
|
+
assert.equal(isPending, true);
|
|
112
|
+
});
|
|
113
|
+
});
|
package/src/hooks/useFilters.ts
CHANGED
|
@@ -12,6 +12,16 @@ import {
|
|
|
12
12
|
type OrganizeScope,
|
|
13
13
|
} from "@/lib/organize/organize-model";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The query key the filter list reads and both mutations invalidate on success.
|
|
17
|
+
* Extracted so create/delete invalidate exactly the key `useFilterList`
|
|
18
|
+
* subscribes to — a drift here would leave the settings list stale after a
|
|
19
|
+
* filter is created or deleted (the same contract `buildMailboxListKey` pins
|
|
20
|
+
* for trigger-sync).
|
|
21
|
+
*/
|
|
22
|
+
export const buildFilterListKey = (accountId: string) =>
|
|
23
|
+
filterOperationsListFiltersQueryKey({ path: { accountId } });
|
|
24
|
+
|
|
15
25
|
/**
|
|
16
26
|
* List the account's standing filters (Standing + Temporary). Expired
|
|
17
27
|
* Temporary filters stay in the list — they are shown distinctly, never hidden
|
|
@@ -46,9 +56,7 @@ export const useCreateFilter = (accountId: string | undefined) => {
|
|
|
46
56
|
onSuccess: () => {
|
|
47
57
|
if (!accountId) return;
|
|
48
58
|
queryClient.invalidateQueries({
|
|
49
|
-
queryKey:
|
|
50
|
-
path: { accountId },
|
|
51
|
-
}),
|
|
59
|
+
queryKey: buildFilterListKey(accountId),
|
|
52
60
|
});
|
|
53
61
|
},
|
|
54
62
|
});
|
|
@@ -86,9 +94,7 @@ export const useDeleteFilter = (accountId: string | undefined) => {
|
|
|
86
94
|
onSuccess: () => {
|
|
87
95
|
if (!accountId) return;
|
|
88
96
|
queryClient.invalidateQueries({
|
|
89
|
-
queryKey:
|
|
90
|
-
path: { accountId },
|
|
91
|
-
}),
|
|
97
|
+
queryKey: buildFilterListKey(accountId),
|
|
92
98
|
});
|
|
93
99
|
},
|
|
94
100
|
});
|