@terreno/ui 0.12.2 → 0.13.3
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/dist/ConsentFormScreen.js +8 -7
- package/dist/ConsentFormScreen.js.map +1 -1
- package/dist/Field.js.map +1 -1
- package/dist/PickerSelect.d.ts +22 -10
- package/dist/PickerSelect.js +11 -9
- package/dist/PickerSelect.js.map +1 -1
- package/dist/SelectBadge.js +11 -1
- package/dist/SelectBadge.js.map +1 -1
- package/dist/SelectField.js +2 -2
- package/dist/SelectField.js.map +1 -1
- package/dist/SidebarNavigation.native.js.map +1 -1
- package/dist/Signature.js +4 -0
- package/dist/Signature.js.map +1 -1
- package/dist/Signature.native.js +4 -0
- package/dist/Signature.native.js.map +1 -1
- package/dist/Theme.d.ts +1 -1
- package/dist/Theme.js.map +1 -1
- package/dist/useConsentForms.d.ts +4 -3
- package/dist/useConsentForms.js +26 -4
- package/dist/useConsentForms.js.map +1 -1
- package/dist/useConsentHistory.d.ts +4 -3
- package/dist/useConsentHistory.js +26 -4
- package/dist/useConsentHistory.js.map +1 -1
- package/dist/useSubmitConsent.d.ts +7 -6
- package/dist/useSubmitConsent.js +25 -3
- package/dist/useSubmitConsent.js.map +1 -1
- package/package.json +1 -1
- package/src/ConsentFormScreen.test.tsx +91 -23
- package/src/ConsentFormScreen.tsx +21 -0
- package/src/Field.tsx +1 -1
- package/src/HeightField.test.tsx +68 -0
- package/src/Modal.tsx +2 -2
- package/src/NumberField.test.tsx +13 -19
- package/src/PickerSelect.tsx +48 -34
- package/src/SelectBadge.tsx +7 -3
- package/src/SelectField.tsx +2 -2
- package/src/SidebarNavigation.native.tsx +6 -2
- package/src/Signature.native.tsx +4 -0
- package/src/Signature.test.tsx +10 -0
- package/src/Signature.tsx +4 -0
- package/src/Theme.tsx +17 -14
- package/src/Tooltip.test.tsx +41 -22
- package/src/__snapshots__/AddressField.test.tsx.snap +0 -1
- package/src/__snapshots__/CustomSelectField.test.tsx.snap +0 -7
- package/src/__snapshots__/Field.test.tsx.snap +0 -6
- package/src/__snapshots__/PickerSelect.test.tsx.snap +0 -7
- package/src/__snapshots__/SelectField.test.tsx.snap +0 -6
- package/src/__snapshots__/TerrenoProvider.test.tsx.snap +4 -18
- package/src/__snapshots__/TimezonePicker.test.tsx.snap +0 -6
- package/src/bunSetup.ts +22 -0
- package/src/polyfill.d.ts +1 -1
- package/src/table/__snapshots__/TableBadge.test.tsx.snap +0 -1
- package/src/useConsentForms.test.ts +25 -0
- package/src/useConsentForms.ts +32 -7
- package/src/useConsentHistory.test.ts +99 -0
- package/src/useConsentHistory.ts +31 -6
- package/src/useSubmitConsent.test.ts +24 -0
- package/src/useSubmitConsent.ts +35 -10
|
@@ -51,7 +51,6 @@ exports[`SelectField renders correctly with default props 1`] = `
|
|
|
51
51
|
},
|
|
52
52
|
],
|
|
53
53
|
"props": {
|
|
54
|
-
"activeOpacity": 1,
|
|
55
54
|
"onPress": [Function],
|
|
56
55
|
"style": {
|
|
57
56
|
"alignItems": "center",
|
|
@@ -341,7 +340,6 @@ exports[`SelectField renders with title 1`] = `
|
|
|
341
340
|
},
|
|
342
341
|
],
|
|
343
342
|
"props": {
|
|
344
|
-
"activeOpacity": 1,
|
|
345
343
|
"onPress": [Function],
|
|
346
344
|
"style": {
|
|
347
345
|
"alignItems": "center",
|
|
@@ -616,7 +614,6 @@ exports[`SelectField renders with selected value 1`] = `
|
|
|
616
614
|
},
|
|
617
615
|
],
|
|
618
616
|
"props": {
|
|
619
|
-
"activeOpacity": 1,
|
|
620
617
|
"onPress": [Function],
|
|
621
618
|
"style": {
|
|
622
619
|
"alignItems": "center",
|
|
@@ -891,7 +888,6 @@ exports[`SelectField renders with custom placeholder 1`] = `
|
|
|
891
888
|
},
|
|
892
889
|
],
|
|
893
890
|
"props": {
|
|
894
|
-
"activeOpacity": 1,
|
|
895
891
|
"onPress": [Function],
|
|
896
892
|
"style": {
|
|
897
893
|
"alignItems": "center",
|
|
@@ -1166,7 +1162,6 @@ exports[`SelectField renders disabled state 1`] = `
|
|
|
1166
1162
|
},
|
|
1167
1163
|
],
|
|
1168
1164
|
"props": {
|
|
1169
|
-
"activeOpacity": 1,
|
|
1170
1165
|
"onPress": [Function],
|
|
1171
1166
|
"style": {
|
|
1172
1167
|
"alignItems": "center",
|
|
@@ -1443,7 +1438,6 @@ exports[`SelectField renders with requireValue (no clear option) 1`] = `
|
|
|
1443
1438
|
},
|
|
1444
1439
|
],
|
|
1445
1440
|
"props": {
|
|
1446
|
-
"activeOpacity": 1,
|
|
1447
1441
|
"onPress": [Function],
|
|
1448
1442
|
"style": {
|
|
1449
1443
|
"alignItems": "center",
|
|
@@ -27,15 +27,8 @@ exports[`TerrenoProvider renders correctly with default props 1`] = `
|
|
|
27
27
|
},
|
|
28
28
|
],
|
|
29
29
|
"props": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"style": [
|
|
33
|
-
{
|
|
34
|
-
"flex": 1,
|
|
35
|
-
},
|
|
36
|
-
undefined,
|
|
37
|
-
],
|
|
38
|
-
"testID": undefined,
|
|
30
|
+
"style": undefined,
|
|
31
|
+
"testID": "portal-host",
|
|
39
32
|
},
|
|
40
33
|
"type": "View",
|
|
41
34
|
},
|
|
@@ -123,15 +116,8 @@ exports[`TerrenoProvider renders with openAPISpecUrl 1`] = `
|
|
|
123
116
|
},
|
|
124
117
|
],
|
|
125
118
|
"props": {
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"style": [
|
|
129
|
-
{
|
|
130
|
-
"flex": 1,
|
|
131
|
-
},
|
|
132
|
-
undefined,
|
|
133
|
-
],
|
|
134
|
-
"testID": undefined,
|
|
119
|
+
"style": undefined,
|
|
120
|
+
"testID": "portal-host",
|
|
135
121
|
},
|
|
136
122
|
"type": "View",
|
|
137
123
|
},
|
|
@@ -66,7 +66,6 @@ exports[`TimezonePicker renders correctly with default props 1`] = `
|
|
|
66
66
|
},
|
|
67
67
|
],
|
|
68
68
|
"props": {
|
|
69
|
-
"activeOpacity": 1,
|
|
70
69
|
"onPress": [Function],
|
|
71
70
|
"style": {
|
|
72
71
|
"alignItems": "center",
|
|
@@ -381,7 +380,6 @@ exports[`TimezonePicker hides title when hideTitle is true 1`] = `
|
|
|
381
380
|
},
|
|
382
381
|
],
|
|
383
382
|
"props": {
|
|
384
|
-
"activeOpacity": 1,
|
|
385
383
|
"onPress": [Function],
|
|
386
384
|
"style": {
|
|
387
385
|
"alignItems": "center",
|
|
@@ -711,7 +709,6 @@ exports[`TimezonePicker renders with selected timezone 1`] = `
|
|
|
711
709
|
},
|
|
712
710
|
],
|
|
713
711
|
"props": {
|
|
714
|
-
"activeOpacity": 1,
|
|
715
712
|
"onPress": [Function],
|
|
716
713
|
"style": {
|
|
717
714
|
"alignItems": "center",
|
|
@@ -1041,7 +1038,6 @@ exports[`TimezonePicker renders USA timezones by default 1`] = `
|
|
|
1041
1038
|
},
|
|
1042
1039
|
],
|
|
1043
1040
|
"props": {
|
|
1044
|
-
"activeOpacity": 1,
|
|
1045
1041
|
"onPress": [Function],
|
|
1046
1042
|
"style": {
|
|
1047
1043
|
"alignItems": "center",
|
|
@@ -1371,7 +1367,6 @@ exports[`TimezonePicker renders with short timezone labels 1`] = `
|
|
|
1371
1367
|
},
|
|
1372
1368
|
],
|
|
1373
1369
|
"props": {
|
|
1374
|
-
"activeOpacity": 1,
|
|
1375
1370
|
"onPress": [Function],
|
|
1376
1371
|
"style": {
|
|
1377
1372
|
"alignItems": "center",
|
|
@@ -1701,7 +1696,6 @@ exports[`TimezonePicker calls onChange when timezone is selected 1`] = `
|
|
|
1701
1696
|
},
|
|
1702
1697
|
],
|
|
1703
1698
|
"props": {
|
|
1704
|
-
"activeOpacity": 1,
|
|
1705
1699
|
"onPress": [Function],
|
|
1706
1700
|
"style": {
|
|
1707
1701
|
"alignItems": "center",
|
package/src/bunSetup.ts
CHANGED
|
@@ -530,6 +530,28 @@ mock.module("react-native-signature-canvas", () => ({
|
|
|
530
530
|
Signature: mock(() => null),
|
|
531
531
|
}));
|
|
532
532
|
|
|
533
|
+
// Mock react-signature-canvas (web). The real module references `window` at
|
|
534
|
+
// import time, which doesn't exist under bun test.
|
|
535
|
+
mock.module("react-signature-canvas", () => {
|
|
536
|
+
const SignatureCanvasMock = React.forwardRef(
|
|
537
|
+
({backgroundColor}: {backgroundColor?: string}, _ref) =>
|
|
538
|
+
React.createElement("View", {style: {backgroundColor}, testID: "signature-canvas"})
|
|
539
|
+
);
|
|
540
|
+
return {__esModule: true, default: SignatureCanvasMock};
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Mock react-native-portalize. The real `Host` wraps children in an extra View
|
|
544
|
+
// whose presence makes snapshots brittle, and individual tests already mock
|
|
545
|
+
// this to render inline; hoisting the mock to setup keeps test ordering from
|
|
546
|
+
// leaking different shapes into other test files. Shape matches the per-file
|
|
547
|
+
// mock used by Tooltip.test.tsx so the two don't disagree.
|
|
548
|
+
mock.module("react-native-portalize", () => ({
|
|
549
|
+
Host: ({children}: MockComponentProps) =>
|
|
550
|
+
React.createElement("View", {style: undefined, testID: "portal-host"}, children),
|
|
551
|
+
Portal: ({children}: MockComponentProps) =>
|
|
552
|
+
React.createElement("View", {style: undefined, testID: "portal"}, children),
|
|
553
|
+
}));
|
|
554
|
+
|
|
533
555
|
// Mock IconButton component
|
|
534
556
|
mock.module("./IconButton", () => ({
|
|
535
557
|
IconButton: mock(() => null),
|
package/src/polyfill.d.ts
CHANGED
|
@@ -149,4 +149,29 @@ describe("useConsentForms", () => {
|
|
|
149
149
|
const {result} = renderHook(() => useConsentForms(api as unknown as ConsentFormsApi));
|
|
150
150
|
expect(result.current.error).toBe("error");
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
it("injects the pending-consents endpoint only once per (api, baseUrl)", () => {
|
|
154
|
+
let injectCallCount = 0;
|
|
155
|
+
const refetch = mock(() => {});
|
|
156
|
+
const useGetPendingConsentsQuery = mock(() => ({
|
|
157
|
+
data: undefined,
|
|
158
|
+
error: undefined,
|
|
159
|
+
isLoading: false,
|
|
160
|
+
refetch,
|
|
161
|
+
}));
|
|
162
|
+
const api = {
|
|
163
|
+
enhanceEndpoints: () => ({
|
|
164
|
+
injectEndpoints: () => {
|
|
165
|
+
injectCallCount += 1;
|
|
166
|
+
return {useGetPendingConsentsQuery};
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
const {rerender} = renderHook(() => useConsentForms(api as unknown as ConsentFormsApi));
|
|
171
|
+
rerender(undefined);
|
|
172
|
+
rerender(undefined);
|
|
173
|
+
// The hook reuses the cached enhanced api after the first render so the
|
|
174
|
+
// dev-mode RTK warning about re-injecting endpoints never fires.
|
|
175
|
+
expect(injectCallCount).toBe(1);
|
|
176
|
+
});
|
|
152
177
|
});
|
package/src/useConsentForms.ts
CHANGED
|
@@ -41,13 +41,15 @@ interface ConsentFormsHookState {
|
|
|
41
41
|
refetch: () => void | Promise<void>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface ConsentFormsEnhancedApi {
|
|
45
|
+
useGetPendingConsentsQuery: () => ConsentFormsHookState;
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
interface ConsentFormsApiWithTags {
|
|
45
49
|
injectEndpoints: (options: {
|
|
46
50
|
endpoints: (build: ConsentFormsQueryBuilder) => {getPendingConsents: unknown};
|
|
47
51
|
overrideExisting: boolean;
|
|
48
|
-
}) =>
|
|
49
|
-
useGetPendingConsentsQuery: () => ConsentFormsHookState;
|
|
50
|
-
};
|
|
52
|
+
}) => ConsentFormsEnhancedApi;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
interface ConsentFormsApi {
|
|
@@ -73,11 +75,27 @@ export const detectLocale = (): string => {
|
|
|
73
75
|
return "en";
|
|
74
76
|
};
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Cache the enhanced api per (api, baseUrl). `injectEndpoints` logs a console
|
|
80
|
+
* error in development whenever an endpoint with the same name is re-injected
|
|
81
|
+
* (with `overrideExisting: false`), so calling it on every render of every
|
|
82
|
+
* consumer would flood the console. WeakMap-by-api lets the GC reclaim entries
|
|
83
|
+
* when the api object is unreachable.
|
|
84
|
+
*/
|
|
85
|
+
const enhancedApiCache = new WeakMap<ConsentFormsApi, Map<string, ConsentFormsEnhancedApi>>();
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
const getEnhancedApi = (api: ConsentFormsApi, base: string): ConsentFormsEnhancedApi => {
|
|
88
|
+
let byBase = enhancedApiCache.get(api);
|
|
89
|
+
if (!byBase) {
|
|
90
|
+
byBase = new Map();
|
|
91
|
+
enhancedApiCache.set(api, byBase);
|
|
92
|
+
}
|
|
93
|
+
const cached = byBase.get(base);
|
|
94
|
+
if (cached) {
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
const apiWithConsentTags = api.enhanceEndpoints({addTagTypes: ["PendingConsents"]});
|
|
98
|
+
const enhanced = apiWithConsentTags.injectEndpoints({
|
|
81
99
|
endpoints: (build) => ({
|
|
82
100
|
getPendingConsents: build.query({
|
|
83
101
|
async onQueryStarted(_arg: unknown, {queryFulfilled}) {
|
|
@@ -97,6 +115,13 @@ export const useConsentForms = (api: ConsentFormsApi, baseUrl?: string) => {
|
|
|
97
115
|
}),
|
|
98
116
|
overrideExisting: false,
|
|
99
117
|
});
|
|
118
|
+
byBase.set(base, enhanced);
|
|
119
|
+
return enhanced;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const useConsentForms = (api: ConsentFormsApi, baseUrl?: string) => {
|
|
123
|
+
const base = baseUrl || "";
|
|
124
|
+
const enhancedApi = getEnhancedApi(api, base);
|
|
100
125
|
|
|
101
126
|
const {data, isLoading, error, refetch} = enhancedApi.useGetPendingConsentsQuery();
|
|
102
127
|
const forms: ConsentFormPublic[] = Array.isArray(data) ? data : (data?.data ?? []);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {renderHook} from "@testing-library/react-native";
|
|
3
|
+
|
|
4
|
+
import type {ConsentHistoryEntry} from "./useConsentHistory";
|
|
5
|
+
import {useConsentHistory} from "./useConsentHistory";
|
|
6
|
+
|
|
7
|
+
type ConsentHistoryApi = Parameters<typeof useConsentHistory>[0];
|
|
8
|
+
|
|
9
|
+
interface MockQueryDef {
|
|
10
|
+
query: () => string;
|
|
11
|
+
providesTags: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MockInjectOpts {
|
|
15
|
+
endpoints: (build: {query: (def: MockQueryDef) => string}) => Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("useConsentHistory", () => {
|
|
19
|
+
const buildApi = (queryResult: {data?: unknown; error?: unknown; isLoading?: boolean}) => {
|
|
20
|
+
const refetch = mock(() => {});
|
|
21
|
+
const useGetMyConsentsQuery = mock(() => ({
|
|
22
|
+
data: queryResult.data,
|
|
23
|
+
error: queryResult.error,
|
|
24
|
+
isLoading: queryResult.isLoading ?? false,
|
|
25
|
+
refetch,
|
|
26
|
+
}));
|
|
27
|
+
const api = {
|
|
28
|
+
injectEndpoints: mock((opts: MockInjectOpts) => {
|
|
29
|
+
const build = {
|
|
30
|
+
query: mock((def: MockQueryDef) => {
|
|
31
|
+
// Exercise the URL builder so the closure captures `base`
|
|
32
|
+
const url = def.query();
|
|
33
|
+
expect(url).toContain("/consents/my");
|
|
34
|
+
return "my-consents-query";
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
opts.endpoints(build);
|
|
38
|
+
return {useGetMyConsentsQuery};
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
return {api, refetch};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
it("returns an array of entries when response is an array", () => {
|
|
45
|
+
const {api} = buildApi({
|
|
46
|
+
data: [{_id: "1", agreed: true} as unknown as ConsentHistoryEntry],
|
|
47
|
+
});
|
|
48
|
+
const {result} = renderHook(() => useConsentHistory(api as unknown as ConsentHistoryApi));
|
|
49
|
+
expect(Array.isArray(result.current.entries)).toBe(true);
|
|
50
|
+
expect(result.current.entries).toHaveLength(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("unwraps .data property when response is object shape", () => {
|
|
54
|
+
const {api} = buildApi({
|
|
55
|
+
data: {data: [{_id: "2", agreed: false} as unknown as ConsentHistoryEntry]},
|
|
56
|
+
});
|
|
57
|
+
const {result} = renderHook(() =>
|
|
58
|
+
useConsentHistory(api as unknown as ConsentHistoryApi, "/api")
|
|
59
|
+
);
|
|
60
|
+
expect(Array.isArray(result.current.entries)).toBe(true);
|
|
61
|
+
expect(result.current.entries).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns empty array when no data is present", () => {
|
|
65
|
+
const {api} = buildApi({data: undefined, isLoading: true});
|
|
66
|
+
const {result} = renderHook(() => useConsentHistory(api as unknown as ConsentHistoryApi));
|
|
67
|
+
expect(result.current.entries).toEqual([]);
|
|
68
|
+
expect(result.current.isLoading).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("surfaces error from the query hook", () => {
|
|
72
|
+
const {api} = buildApi({data: undefined, error: "boom"});
|
|
73
|
+
const {result} = renderHook(() => useConsentHistory(api as unknown as ConsentHistoryApi));
|
|
74
|
+
expect(result.current.error).toBe("boom");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("injects the my-consents endpoint only once per (api, baseUrl)", () => {
|
|
78
|
+
let injectCallCount = 0;
|
|
79
|
+
const refetch = mock(() => {});
|
|
80
|
+
const useGetMyConsentsQuery = mock(() => ({
|
|
81
|
+
data: undefined,
|
|
82
|
+
error: undefined,
|
|
83
|
+
isLoading: false,
|
|
84
|
+
refetch,
|
|
85
|
+
}));
|
|
86
|
+
const api = {
|
|
87
|
+
injectEndpoints: () => {
|
|
88
|
+
injectCallCount += 1;
|
|
89
|
+
return {useGetMyConsentsQuery};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const {rerender} = renderHook(() => useConsentHistory(api as unknown as ConsentHistoryApi));
|
|
93
|
+
rerender(undefined);
|
|
94
|
+
rerender(undefined);
|
|
95
|
+
// The hook reuses the cached enhanced api after the first render so the
|
|
96
|
+
// dev-mode RTK warning about re-injecting endpoints never fires.
|
|
97
|
+
expect(injectCallCount).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/useConsentHistory.ts
CHANGED
|
@@ -35,19 +35,37 @@ interface ConsentHistoryHookState {
|
|
|
35
35
|
refetch: () => void | Promise<void>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
interface ConsentHistoryEnhancedApi {
|
|
39
|
+
useGetMyConsentsQuery: () => ConsentHistoryHookState;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
interface ConsentHistoryApi {
|
|
39
43
|
injectEndpoints: (options: {
|
|
40
44
|
endpoints: (build: ConsentHistoryQueryBuilder) => {getMyConsents: unknown};
|
|
41
45
|
overrideExisting: boolean;
|
|
42
|
-
}) =>
|
|
43
|
-
useGetMyConsentsQuery: () => ConsentHistoryHookState;
|
|
44
|
-
};
|
|
46
|
+
}) => ConsentHistoryEnhancedApi;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Cache the enhanced api per (api, baseUrl). `injectEndpoints` logs a console
|
|
51
|
+
* error in development whenever an endpoint with the same name is re-injected
|
|
52
|
+
* (with `overrideExisting: false`), so calling it on every render of every
|
|
53
|
+
* consumer would flood the console. WeakMap-by-api lets the GC reclaim entries
|
|
54
|
+
* when the api object is unreachable.
|
|
55
|
+
*/
|
|
56
|
+
const enhancedApiCache = new WeakMap<ConsentHistoryApi, Map<string, ConsentHistoryEnhancedApi>>();
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
const getEnhancedApi = (api: ConsentHistoryApi, base: string): ConsentHistoryEnhancedApi => {
|
|
59
|
+
let byBase = enhancedApiCache.get(api);
|
|
60
|
+
if (!byBase) {
|
|
61
|
+
byBase = new Map();
|
|
62
|
+
enhancedApiCache.set(api, byBase);
|
|
63
|
+
}
|
|
64
|
+
const cached = byBase.get(base);
|
|
65
|
+
if (cached) {
|
|
66
|
+
return cached;
|
|
67
|
+
}
|
|
68
|
+
const enhanced = api.injectEndpoints({
|
|
51
69
|
endpoints: (build) => ({
|
|
52
70
|
getMyConsents: build.query({
|
|
53
71
|
providesTags: ["MyConsents"],
|
|
@@ -56,6 +74,13 @@ export const useConsentHistory = (api: ConsentHistoryApi, baseUrl?: string) => {
|
|
|
56
74
|
}),
|
|
57
75
|
overrideExisting: false,
|
|
58
76
|
});
|
|
77
|
+
byBase.set(base, enhanced);
|
|
78
|
+
return enhanced;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const useConsentHistory = (api: ConsentHistoryApi, baseUrl?: string) => {
|
|
82
|
+
const base = baseUrl || "";
|
|
83
|
+
const enhancedApi = getEnhancedApi(api, base);
|
|
59
84
|
|
|
60
85
|
const {data, isLoading, error, refetch} = enhancedApi.useGetMyConsentsQuery();
|
|
61
86
|
const entries: ConsentHistoryEntry[] = Array.isArray(data) ? data : (data?.data ?? []);
|
|
@@ -72,4 +72,28 @@ describe("useSubmitConsent", () => {
|
|
|
72
72
|
const {result} = renderHook(() => useSubmitConsent(api as unknown as SubmitConsentApi));
|
|
73
73
|
expect(result.current.submit).toBeDefined();
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
it("injects the submit endpoint only once per (api, baseUrl)", () => {
|
|
77
|
+
let injectCallCount = 0;
|
|
78
|
+
const unwrap = mock(async () => ({}));
|
|
79
|
+
const submitMutation = mock(() => ({unwrap}));
|
|
80
|
+
const useSubmitConsentResponseMutation = mock(() => [
|
|
81
|
+
submitMutation,
|
|
82
|
+
{error: undefined, isLoading: false},
|
|
83
|
+
]);
|
|
84
|
+
const api = {
|
|
85
|
+
enhanceEndpoints: () => ({
|
|
86
|
+
injectEndpoints: () => {
|
|
87
|
+
injectCallCount += 1;
|
|
88
|
+
return {useSubmitConsentResponseMutation};
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
const {rerender} = renderHook(() => useSubmitConsent(api as unknown as SubmitConsentApi));
|
|
93
|
+
rerender(undefined);
|
|
94
|
+
rerender(undefined);
|
|
95
|
+
// The hook reuses the cached enhanced api after the first render so the
|
|
96
|
+
// dev-mode RTK warning about re-injecting endpoints never fires.
|
|
97
|
+
expect(injectCallCount).toBe(1);
|
|
98
|
+
});
|
|
75
99
|
});
|
package/src/useSubmitConsent.ts
CHANGED
|
@@ -26,27 +26,45 @@ interface SubmitConsentMutationBuilder {
|
|
|
26
26
|
}) => unknown;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
interface SubmitConsentEnhancedApi {
|
|
30
|
+
useSubmitConsentResponseMutation: () => [
|
|
31
|
+
(body: SubmitConsentBody) => SubmitConsentMutationResult,
|
|
32
|
+
SubmitConsentMutationHookState,
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
interface SubmitConsentApiWithTags {
|
|
30
37
|
injectEndpoints: (options: {
|
|
31
38
|
endpoints: (build: SubmitConsentMutationBuilder) => {submitConsentResponse: unknown};
|
|
32
39
|
overrideExisting: boolean;
|
|
33
|
-
}) =>
|
|
34
|
-
useSubmitConsentResponseMutation: () => [
|
|
35
|
-
(body: SubmitConsentBody) => SubmitConsentMutationResult,
|
|
36
|
-
SubmitConsentMutationHookState,
|
|
37
|
-
];
|
|
38
|
-
};
|
|
40
|
+
}) => SubmitConsentEnhancedApi;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
interface SubmitConsentApi {
|
|
42
44
|
enhanceEndpoints: (options: {addTagTypes: string[]}) => SubmitConsentApiWithTags;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Cache the enhanced api per (api, baseUrl). `injectEndpoints` logs a console
|
|
49
|
+
* error in development whenever an endpoint with the same name is re-injected
|
|
50
|
+
* (with `overrideExisting: false`), so calling it on every render of every
|
|
51
|
+
* consumer would flood the console. WeakMap-by-api lets the GC reclaim entries
|
|
52
|
+
* when the api object is unreachable.
|
|
53
|
+
*/
|
|
54
|
+
const enhancedApiCache = new WeakMap<SubmitConsentApi, Map<string, SubmitConsentEnhancedApi>>();
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
const getEnhancedApi = (api: SubmitConsentApi, base: string): SubmitConsentEnhancedApi => {
|
|
57
|
+
let byBase = enhancedApiCache.get(api);
|
|
58
|
+
if (!byBase) {
|
|
59
|
+
byBase = new Map();
|
|
60
|
+
enhancedApiCache.set(api, byBase);
|
|
61
|
+
}
|
|
62
|
+
const cached = byBase.get(base);
|
|
63
|
+
if (cached) {
|
|
64
|
+
return cached;
|
|
65
|
+
}
|
|
66
|
+
const apiWithConsentTags = api.enhanceEndpoints({addTagTypes: ["PendingConsents"]});
|
|
67
|
+
const enhanced = apiWithConsentTags.injectEndpoints({
|
|
50
68
|
endpoints: (build) => ({
|
|
51
69
|
submitConsentResponse: build.mutation({
|
|
52
70
|
invalidatesTags: ["PendingConsents"],
|
|
@@ -59,6 +77,13 @@ export const useSubmitConsent = (api: SubmitConsentApi, baseUrl?: string) => {
|
|
|
59
77
|
}),
|
|
60
78
|
overrideExisting: false,
|
|
61
79
|
});
|
|
80
|
+
byBase.set(base, enhanced);
|
|
81
|
+
return enhanced;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const useSubmitConsent = (api: SubmitConsentApi, baseUrl?: string) => {
|
|
85
|
+
const base = baseUrl || "";
|
|
86
|
+
const enhancedApi = getEnhancedApi(api, base);
|
|
62
87
|
|
|
63
88
|
const [submitMutation, {isLoading: isSubmitting, error}] =
|
|
64
89
|
enhancedApi.useSubmitConsentResponseMutation();
|