@hubspot/ui-extensions 0.12.3 → 0.13.0
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/__tests__/crm/hooks/useAssociations.spec.js +28 -0
- package/dist/__tests__/crm/utils/fetchAssociations.spec.js +2 -1
- package/dist/__tests__/hooks/useDebounce.spec.js +123 -0
- package/dist/__tests__/hooks/utils/useFetchLifecycle.spec.js +324 -0
- package/dist/__tests__/internal/hook-utils.spec.d.ts +1 -0
- package/dist/__tests__/internal/hook-utils.spec.js +17 -0
- package/dist/__tests__/test-d/extension-points.test-d.js +1 -0
- package/dist/crm/hooks/useAssociations.d.ts +4 -12
- package/dist/crm/hooks/useAssociations.js +46 -138
- package/dist/crm/hooks/useCrmProperties.js +29 -125
- package/dist/crm/utils/fetchAssociations.d.ts +0 -8
- package/dist/crm/utils/fetchAssociations.js +0 -10
- package/dist/hooks/useDebounce.d.ts +19 -0
- package/dist/hooks/useDebounce.js +32 -0
- package/dist/hooks/utils/useFetchLifecycle.d.ts +35 -0
- package/dist/hooks/utils/useFetchLifecycle.js +103 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/hook-utils.d.ts +13 -6
- package/dist/internal/hook-utils.js +24 -10
- package/dist/pages/components/index.d.ts +1 -0
- package/dist/pages/components/index.js +1 -0
- package/dist/{shared/types/pages → pages}/components/page-routes.d.ts +6 -39
- package/dist/pages/components/page-routes.js +62 -0
- package/dist/pages/create-page-router.d.ts +33 -0
- package/dist/pages/create-page-router.js +121 -0
- package/dist/pages/create-page-router.test.d.ts +1 -0
- package/dist/pages/create-page-router.test.js +296 -0
- package/dist/pages/hooks.d.ts +7 -0
- package/dist/pages/hooks.js +14 -0
- package/dist/pages/index.d.ts +6 -0
- package/dist/pages/index.js +4 -0
- package/dist/pages/internal/app-page-route-context.d.ts +16 -0
- package/dist/pages/internal/app-page-route-context.js +12 -0
- package/dist/pages/internal/convert-page-routes-react-elements.d.ts +9 -0
- package/dist/pages/internal/convert-page-routes-react-elements.js +138 -0
- package/dist/pages/internal/page-router-internal-types.d.ts +40 -0
- package/dist/pages/internal/page-router-internal-types.js +5 -0
- package/dist/pages/internal/trie-router.d.ts +31 -0
- package/dist/pages/internal/trie-router.js +141 -0
- package/dist/pages/internal/trie-router.test.d.ts +1 -0
- package/dist/pages/internal/trie-router.test.js +263 -0
- package/dist/pages/internal/useAppPageLocation.d.ts +1 -0
- package/dist/pages/internal/useAppPageLocation.js +13 -0
- package/dist/pages/types.d.ts +28 -0
- package/dist/pages/types.js +1 -0
- package/dist/shared/remoteComponents.d.ts +24 -0
- package/dist/shared/remoteComponents.js +28 -0
- package/dist/shared/types/actions.d.ts +12 -2
- package/dist/shared/types/components/button.d.ts +2 -2
- package/dist/shared/types/components/image.d.ts +2 -2
- package/dist/shared/types/components/inputs.d.ts +7 -1
- package/dist/shared/types/components/link.d.ts +2 -2
- package/dist/shared/types/context.d.ts +5 -0
- package/dist/shared/types/crm.d.ts +4 -0
- package/dist/shared/types/extension-points.d.ts +9 -3
- package/dist/shared/types/extension-points.js +1 -0
- package/dist/shared/types/pages/components/index.d.ts +3 -1
- package/dist/shared/types/pages/components/page-breadcrumbs.d.ts +34 -0
- package/dist/shared/types/pages/components/page-breadcrumbs.js +1 -0
- package/dist/shared/types/pages/components/page-header.d.ts +82 -0
- package/dist/shared/types/pages/components/page-header.js +1 -0
- package/dist/shared/types/pages/components/page-link.d.ts +4 -2
- package/dist/shared/types/pages/components/page-title.d.ts +12 -0
- package/dist/shared/types/pages/components/page-title.js +1 -0
- package/dist/shared/types/shared.d.ts +8 -0
- package/dist/shared/types/worker-globals.d.ts +3 -12
- package/dist/testing/__tests__/createRenderer.spec.js +1 -1
- package/dist/testing/internal/mocks/index.d.ts +14 -6
- package/dist/testing/internal/mocks/index.js +19 -5
- package/dist/testing/internal/mocks/mock-app-page-location.d.ts +7 -0
- package/dist/testing/internal/mocks/mock-app-page-location.js +35 -0
- package/dist/testing/internal/mocks/mock-extension-point-api.js +21 -1
- package/dist/testing/internal/mocks/mock-hooks.js +12 -7
- package/dist/testing/render.js +7 -6
- package/dist/testing/types.d.ts +19 -3
- package/dist/utils/pagination.d.ts +17 -0
- package/dist/utils/pagination.js +10 -0
- package/package.json +2 -2
- package/dist/experimental/pages/index.d.ts +0 -4
- package/dist/experimental/pages/index.js +0 -3
- package/dist/shared/types/pages/app-pages-types.d.ts +0 -75
- /package/dist/{shared/types/pages/app-pages-types.js → __tests__/hooks/useDebounce.spec.d.ts} +0 -0
- /package/dist/{shared/types/pages/components/page-routes.js → __tests__/hooks/utils/useFetchLifecycle.spec.d.ts} +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useCallback, useMemo, useReducer } from 'react';
|
|
2
|
+
import { useFetchLifecycle } from "../../hooks/utils/useFetchLifecycle.js";
|
|
3
|
+
import { createMockAwareHook, useStableValue, } from "../../internal/hook-utils.js";
|
|
4
|
+
import { calculatePaginationFlags, DEFAULT_PAGE_SIZE, } from "../../utils/pagination.js";
|
|
5
|
+
import { fetchAssociations } from "../utils/fetchAssociations.js";
|
|
4
6
|
function createInitialState(pageSize) {
|
|
5
7
|
return {
|
|
6
8
|
results: [],
|
|
@@ -108,39 +110,8 @@ const DEFAULT_OPTIONS = {};
|
|
|
108
110
|
function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
109
111
|
const pageSize = config?.pageLength ?? DEFAULT_PAGE_SIZE;
|
|
110
112
|
const [state, dispatch] = useReducer(associationsReducer, useMemo(() => createInitialState(pageSize), [pageSize]));
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
* Create stable references for config and options to prevent unnecessary re-renders and API calls.
|
|
115
|
-
* Then, external developers can pass inline objects without worrying about memoization
|
|
116
|
-
* We handle the deep equality comparison ourselves, and return the same object reference when content is equivalent.
|
|
117
|
-
*/
|
|
118
|
-
const lastConfigRef = useRef();
|
|
119
|
-
const lastConfigKeyRef = useRef();
|
|
120
|
-
const lastOptionsRef = useRef();
|
|
121
|
-
const lastOptionsKeyRef = useRef();
|
|
122
|
-
// Track in-flight refetch to support cancellation
|
|
123
|
-
const refetchAbortRef = useRef(null);
|
|
124
|
-
// Track refetch cleanup function to prevent memory leaks
|
|
125
|
-
const refetchCleanupRef = useRef(null);
|
|
126
|
-
const stableConfig = useMemo(() => {
|
|
127
|
-
const configKey = JSON.stringify(config);
|
|
128
|
-
if (configKey === lastConfigKeyRef.current) {
|
|
129
|
-
return lastConfigRef.current;
|
|
130
|
-
}
|
|
131
|
-
lastConfigKeyRef.current = configKey;
|
|
132
|
-
lastConfigRef.current = config;
|
|
133
|
-
return config;
|
|
134
|
-
}, [config]);
|
|
135
|
-
const stableOptions = useMemo(() => {
|
|
136
|
-
const optionsKey = JSON.stringify(options);
|
|
137
|
-
if (optionsKey === lastOptionsKeyRef.current) {
|
|
138
|
-
return lastOptionsRef.current;
|
|
139
|
-
}
|
|
140
|
-
lastOptionsKeyRef.current = optionsKey;
|
|
141
|
-
lastOptionsRef.current = options;
|
|
142
|
-
return options;
|
|
143
|
-
}, [options]);
|
|
113
|
+
const stableConfig = useStableValue(config);
|
|
114
|
+
const stableOptions = useStableValue(options);
|
|
144
115
|
// Pagination actions
|
|
145
116
|
const nextPage = useCallback(() => {
|
|
146
117
|
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
@@ -155,122 +126,59 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
|
155
126
|
}
|
|
156
127
|
}, [state.currentPage, state.hasMore]);
|
|
157
128
|
const reset = useCallback(() => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
let cancelled = false;
|
|
163
|
-
let cleanup = null;
|
|
164
|
-
const fetchData = async () => {
|
|
165
|
-
try {
|
|
166
|
-
dispatch({ type: 'FETCH_START' });
|
|
167
|
-
// Build request using current offset token
|
|
168
|
-
const request = {
|
|
169
|
-
toObjectType: stableConfig?.toObjectType,
|
|
170
|
-
properties: stableConfig?.properties,
|
|
171
|
-
pageLength: pageSize,
|
|
172
|
-
offset: state.currentOffset,
|
|
173
|
-
};
|
|
174
|
-
const result = await fetchAssociations(request, {
|
|
175
|
-
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
176
|
-
formattingOptions: stableOptions.formattingOptions,
|
|
177
|
-
});
|
|
178
|
-
if (!cancelled) {
|
|
179
|
-
dispatch({
|
|
180
|
-
type: 'FETCH_SUCCESS',
|
|
181
|
-
payload: {
|
|
182
|
-
results: result.data.results,
|
|
183
|
-
hasMore: result.data.hasMore,
|
|
184
|
-
nextOffset: result.data.nextOffset,
|
|
185
|
-
currentOffset: state.currentOffset,
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
cleanup = result.cleanup;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch (err) {
|
|
192
|
-
if (!cancelled) {
|
|
193
|
-
const errorData = err instanceof Error
|
|
194
|
-
? err
|
|
195
|
-
: new Error('Failed to fetch associations');
|
|
196
|
-
dispatch({ type: 'FETCH_ERROR', payload: errorData });
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
fetchData();
|
|
201
|
-
return () => {
|
|
202
|
-
cancelled = true;
|
|
203
|
-
// Call cleanup function to release resources
|
|
204
|
-
if (cleanup) {
|
|
205
|
-
cleanup();
|
|
206
|
-
}
|
|
207
|
-
// Clean up any active refetch subscription
|
|
208
|
-
if (refetchCleanupRef.current) {
|
|
209
|
-
refetchCleanupRef.current();
|
|
210
|
-
refetchCleanupRef.current = null;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
}, [
|
|
214
|
-
stableConfig,
|
|
215
|
-
stableOptions,
|
|
216
|
-
state.currentPage,
|
|
217
|
-
state.currentOffset,
|
|
218
|
-
pageSize,
|
|
219
|
-
]);
|
|
220
|
-
const refetch = useCallback(async () => {
|
|
221
|
-
if (refetchAbortRef.current) {
|
|
222
|
-
refetchAbortRef.current.cancelled = true;
|
|
223
|
-
}
|
|
224
|
-
if (refetchCleanupRef.current) {
|
|
225
|
-
refetchCleanupRef.current();
|
|
226
|
-
refetchCleanupRef.current = null;
|
|
129
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
130
|
+
// Only reset to first page if we're not already on the first page
|
|
131
|
+
if (paginationFlags.hasPreviousPage) {
|
|
132
|
+
dispatch({ type: 'RESET' });
|
|
227
133
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
dispatch({ type: 'REFETCH_START' });
|
|
232
|
-
// Build request using current offset token to refetch current page
|
|
134
|
+
}, [state.currentPage, state.hasMore]);
|
|
135
|
+
const { refetch } = useFetchLifecycle({
|
|
136
|
+
fetchFn: () => {
|
|
233
137
|
const request = {
|
|
234
138
|
toObjectType: stableConfig?.toObjectType,
|
|
235
139
|
properties: stableConfig?.properties,
|
|
236
140
|
pageLength: pageSize,
|
|
237
141
|
offset: state.currentOffset,
|
|
238
142
|
};
|
|
239
|
-
|
|
143
|
+
return fetchAssociations(request, {
|
|
240
144
|
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
241
145
|
formattingOptions: stableOptions.formattingOptions,
|
|
242
146
|
});
|
|
243
|
-
|
|
244
|
-
|
|
147
|
+
},
|
|
148
|
+
callbacks: {
|
|
149
|
+
onStart: ({ isRefetch }) => dispatch({ type: isRefetch ? 'REFETCH_START' : 'FETCH_START' }),
|
|
150
|
+
onSuccess: (data, { isRefetch }) => dispatch(isRefetch
|
|
151
|
+
? {
|
|
245
152
|
type: 'REFETCH_SUCCESS',
|
|
246
153
|
payload: {
|
|
247
|
-
results:
|
|
248
|
-
hasMore:
|
|
249
|
-
nextOffset:
|
|
154
|
+
results: data.results,
|
|
155
|
+
hasMore: data.hasMore,
|
|
156
|
+
nextOffset: data.nextOffset,
|
|
250
157
|
},
|
|
251
|
-
});
|
|
252
|
-
refetchCleanupRef.current = result.cleanup;
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
if (result.cleanup) {
|
|
256
|
-
result.cleanup();
|
|
257
158
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
159
|
+
: {
|
|
160
|
+
type: 'FETCH_SUCCESS',
|
|
161
|
+
payload: {
|
|
162
|
+
results: data.results,
|
|
163
|
+
hasMore: data.hasMore,
|
|
164
|
+
nextOffset: data.nextOffset,
|
|
165
|
+
currentOffset: state.currentOffset,
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
onError: (error, { isRefetch }) => dispatch({
|
|
169
|
+
type: isRefetch ? 'REFETCH_ERROR' : 'FETCH_ERROR',
|
|
170
|
+
payload: error,
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
deps: [
|
|
174
|
+
stableConfig,
|
|
175
|
+
stableOptions,
|
|
176
|
+
state.currentPage,
|
|
177
|
+
state.currentOffset,
|
|
178
|
+
pageSize,
|
|
179
|
+
],
|
|
180
|
+
defaultErrorMessage: 'Failed to fetch associations',
|
|
181
|
+
});
|
|
274
182
|
// Calculate pagination flags
|
|
275
183
|
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
276
184
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useMemo, useReducer } from 'react';
|
|
2
|
+
import { useFetchLifecycle } from "../../hooks/utils/useFetchLifecycle.js";
|
|
3
|
+
import { createMockAwareHook, useStableValue, } from "../../internal/hook-utils.js";
|
|
3
4
|
import { fetchCrmProperties } from "../utils/fetchCrmProperties.js";
|
|
4
5
|
const initialState = {
|
|
5
6
|
properties: {},
|
|
@@ -58,130 +59,33 @@ const DEFAULT_OPTIONS = {};
|
|
|
58
59
|
*/
|
|
59
60
|
function useCrmPropertiesInternal(propertyNames, options = DEFAULT_OPTIONS) {
|
|
60
61
|
const [state, dispatch] = useReducer(crmPropertiesReducer, initialState);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Track in-flight refetch to support cancellation
|
|
73
|
-
const refetchAbortRef = useRef(null);
|
|
74
|
-
// Track refetch cleanup function to prevent memory leaks
|
|
75
|
-
const refetchCleanupRef = useRef(null);
|
|
76
|
-
const stablePropertyNames = useMemo(() => {
|
|
77
|
-
if (!Array.isArray(propertyNames)) {
|
|
78
|
-
return propertyNames;
|
|
79
|
-
}
|
|
80
|
-
const sortedNames = [...propertyNames].sort();
|
|
81
|
-
const propertyNamesKey = JSON.stringify(sortedNames);
|
|
82
|
-
if (propertyNamesKey === lastPropertyNamesKeyRef.current) {
|
|
83
|
-
return lastPropertyNamesRef.current;
|
|
84
|
-
}
|
|
85
|
-
lastPropertyNamesKeyRef.current = propertyNamesKey;
|
|
86
|
-
lastPropertyNamesRef.current = sortedNames;
|
|
87
|
-
return sortedNames;
|
|
88
|
-
}, [propertyNames]);
|
|
89
|
-
const stableOptions = useMemo(() => {
|
|
90
|
-
const optionsKey = JSON.stringify(options);
|
|
91
|
-
if (optionsKey === lastOptionsKeyRef.current) {
|
|
92
|
-
return lastOptionsRef.current;
|
|
93
|
-
}
|
|
94
|
-
lastOptionsKeyRef.current = optionsKey;
|
|
95
|
-
lastOptionsRef.current = options;
|
|
96
|
-
return options;
|
|
97
|
-
}, [options]);
|
|
98
|
-
// Fetch the properties
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
let cancelled = false;
|
|
101
|
-
let cleanup = null;
|
|
102
|
-
const fetchData = async () => {
|
|
103
|
-
try {
|
|
104
|
-
dispatch({ type: 'FETCH_START' });
|
|
105
|
-
const result = await fetchCrmProperties(stablePropertyNames, (data) => {
|
|
106
|
-
if (!cancelled) {
|
|
107
|
-
dispatch({ type: 'FETCH_SUCCESS', payload: data });
|
|
108
|
-
}
|
|
109
|
-
}, stableOptions);
|
|
110
|
-
if (!cancelled) {
|
|
111
|
-
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
|
|
112
|
-
cleanup = result.cleanup;
|
|
113
|
-
}
|
|
62
|
+
// Sort property names so that order doesn't matter for cache keys
|
|
63
|
+
const sortedPropertyNames = useMemo(() => Array.isArray(propertyNames) ? [...propertyNames].sort() : propertyNames, [propertyNames]);
|
|
64
|
+
const stablePropertyNames = useStableValue(sortedPropertyNames);
|
|
65
|
+
const stableOptions = useStableValue(options);
|
|
66
|
+
const { refetch } = useFetchLifecycle({
|
|
67
|
+
fetchFn: (signal) => fetchCrmProperties(stablePropertyNames, (data) => {
|
|
68
|
+
if (!signal.cancelled) {
|
|
69
|
+
dispatch({
|
|
70
|
+
type: signal.isRefetch ? 'REFETCH_SUCCESS' : 'FETCH_SUCCESS',
|
|
71
|
+
payload: data,
|
|
72
|
+
});
|
|
114
73
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
// Clean up any active refetch subscription
|
|
132
|
-
if (refetchCleanupRef.current) {
|
|
133
|
-
refetchCleanupRef.current();
|
|
134
|
-
refetchCleanupRef.current = null;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
}, [stablePropertyNames, stableOptions]);
|
|
138
|
-
const refetch = useCallback(async () => {
|
|
139
|
-
// Cancel any in-flight refetch
|
|
140
|
-
if (refetchAbortRef.current) {
|
|
141
|
-
refetchAbortRef.current.cancelled = true;
|
|
142
|
-
}
|
|
143
|
-
// Clean up old refetch subscription to prevent memory leaks
|
|
144
|
-
if (refetchCleanupRef.current) {
|
|
145
|
-
refetchCleanupRef.current();
|
|
146
|
-
refetchCleanupRef.current = null;
|
|
147
|
-
}
|
|
148
|
-
// Create new abort signal for this refetch
|
|
149
|
-
const abortSignal = { cancelled: false };
|
|
150
|
-
refetchAbortRef.current = abortSignal;
|
|
151
|
-
try {
|
|
152
|
-
dispatch({ type: 'REFETCH_START' });
|
|
153
|
-
const result = await fetchCrmProperties(stablePropertyNames, (data) => {
|
|
154
|
-
if (!abortSignal.cancelled) {
|
|
155
|
-
dispatch({ type: 'REFETCH_SUCCESS', payload: data });
|
|
156
|
-
}
|
|
157
|
-
}, stableOptions);
|
|
158
|
-
if (!abortSignal.cancelled) {
|
|
159
|
-
dispatch({ type: 'REFETCH_SUCCESS', payload: result.data });
|
|
160
|
-
// Store cleanup for next refetch or unmount
|
|
161
|
-
refetchCleanupRef.current = result.cleanup;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
// If cancelled, clean up immediately
|
|
165
|
-
if (result.cleanup) {
|
|
166
|
-
result.cleanup();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
if (!abortSignal.cancelled) {
|
|
172
|
-
const errorData = err instanceof Error
|
|
173
|
-
? err
|
|
174
|
-
: new Error('Failed to refetch CRM properties');
|
|
175
|
-
dispatch({ type: 'REFETCH_ERROR', payload: errorData });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
finally {
|
|
179
|
-
// Clear the abort ref if this is still the current refetch
|
|
180
|
-
if (refetchAbortRef.current === abortSignal) {
|
|
181
|
-
refetchAbortRef.current = null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}, [stablePropertyNames, stableOptions]);
|
|
74
|
+
}, stableOptions),
|
|
75
|
+
callbacks: {
|
|
76
|
+
onStart: ({ isRefetch }) => dispatch({ type: isRefetch ? 'REFETCH_START' : 'FETCH_START' }),
|
|
77
|
+
onSuccess: (data, { isRefetch }) => dispatch({
|
|
78
|
+
type: isRefetch ? 'REFETCH_SUCCESS' : 'FETCH_SUCCESS',
|
|
79
|
+
payload: data,
|
|
80
|
+
}),
|
|
81
|
+
onError: (error, { isRefetch }) => dispatch({
|
|
82
|
+
type: isRefetch ? 'REFETCH_ERROR' : 'FETCH_ERROR',
|
|
83
|
+
payload: error,
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
deps: [stablePropertyNames, stableOptions],
|
|
87
|
+
defaultErrorMessage: 'Failed to fetch CRM properties',
|
|
88
|
+
});
|
|
185
89
|
return {
|
|
186
90
|
...state,
|
|
187
91
|
refetch,
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import type { FetchCrmPropertiesOptions } from './fetchCrmProperties.ts';
|
|
2
|
-
export declare const DEFAULT_PAGE_SIZE = 10;
|
|
3
|
-
/**
|
|
4
|
-
* Calculate pagination flags based on current page and API hasMore flag
|
|
5
|
-
*/
|
|
6
|
-
export declare function calculatePaginationFlags(currentPage: number, hasMore: boolean): {
|
|
7
|
-
hasNextPage: boolean;
|
|
8
|
-
hasPreviousPage: boolean;
|
|
9
|
-
};
|
|
10
2
|
export type AssociationResult = {
|
|
11
3
|
toObjectId: number;
|
|
12
4
|
associationTypes: {
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
export const DEFAULT_PAGE_SIZE = 10;
|
|
2
|
-
/**
|
|
3
|
-
* Calculate pagination flags based on current page and API hasMore flag
|
|
4
|
-
*/
|
|
5
|
-
export function calculatePaginationFlags(currentPage, hasMore) {
|
|
6
|
-
return {
|
|
7
|
-
hasNextPage: hasMore,
|
|
8
|
-
hasPreviousPage: currentPage > 1,
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
1
|
function isAssociationsResponse(data) {
|
|
12
2
|
if (data === null ||
|
|
13
3
|
typeof data !== 'object' ||
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JsonSerializable } from '../shared/types/shared.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a debounced version of `value` that only updates after the value
|
|
4
|
+
* has stopped changing for `delayMs` milliseconds (default: 300ms).
|
|
5
|
+
*
|
|
6
|
+
* Useful for delaying expensive operations (e.g. search requests) until the
|
|
7
|
+
* user has finished typing.
|
|
8
|
+
*
|
|
9
|
+
* Object and array values are compared by deep equality (via `JSON.stringify`),
|
|
10
|
+
* so values must be JSON-serializable. Functions, Symbols, BigInts, and
|
|
11
|
+
* circular references are not supported.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const [searchText, setSearchText] = useState('');
|
|
16
|
+
* const debouncedQuery = useDebounce(searchText, 200);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function useDebounce<T extends JsonSerializable>(value: T, delayMs?: number): T;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useStableValue } from "../internal/hook-utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Returns a debounced version of `value` that only updates after the value
|
|
5
|
+
* has stopped changing for `delayMs` milliseconds (default: 300ms).
|
|
6
|
+
*
|
|
7
|
+
* Useful for delaying expensive operations (e.g. search requests) until the
|
|
8
|
+
* user has finished typing.
|
|
9
|
+
*
|
|
10
|
+
* Object and array values are compared by deep equality (via `JSON.stringify`),
|
|
11
|
+
* so values must be JSON-serializable. Functions, Symbols, BigInts, and
|
|
12
|
+
* circular references are not supported.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const [searchText, setSearchText] = useState('');
|
|
17
|
+
* const debouncedQuery = useDebounce(searchText, 200);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useDebounce(value, delayMs = 300) {
|
|
21
|
+
const stableValue = useStableValue(value);
|
|
22
|
+
const [debouncedValue, setDebouncedValue] = useState(stableValue);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
setDebouncedValue(stableValue);
|
|
26
|
+
}, delayMs);
|
|
27
|
+
return () => {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
};
|
|
30
|
+
}, [stableValue, delayMs]);
|
|
31
|
+
return debouncedValue;
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface FetchSignal {
|
|
2
|
+
cancelled: boolean;
|
|
3
|
+
isRefetch: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface FetchResult<TData> {
|
|
6
|
+
data: TData;
|
|
7
|
+
cleanup?: () => void;
|
|
8
|
+
}
|
|
9
|
+
export interface FetchContext {
|
|
10
|
+
isRefetch: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface FetchLifecycleCallbacks<TData> {
|
|
13
|
+
onStart: (context: FetchContext) => void;
|
|
14
|
+
onSuccess: (data: TData, context: FetchContext) => void;
|
|
15
|
+
onError: (error: Error, context: FetchContext) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface UseFetchLifecycleOptions<TData> {
|
|
18
|
+
fetchFn: (signal: FetchSignal) => Promise<FetchResult<TData>>;
|
|
19
|
+
callbacks: FetchLifecycleCallbacks<TData>;
|
|
20
|
+
deps: React.DependencyList;
|
|
21
|
+
defaultErrorMessage?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface UseFetchLifecycleResult {
|
|
24
|
+
refetch: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Shared fetch/refetch/cleanup/abort lifecycle for data hooks.
|
|
28
|
+
*
|
|
29
|
+
* Manages:
|
|
30
|
+
* - Initial fetch on mount and when deps change
|
|
31
|
+
* - Cancellation of in-flight fetches on unmount or deps change
|
|
32
|
+
* - Refetch with abort signal and cleanup tracking
|
|
33
|
+
* - Error normalization
|
|
34
|
+
*/
|
|
35
|
+
export declare function useFetchLifecycle<TData>({ fetchFn, callbacks, deps, defaultErrorMessage, }: UseFetchLifecycleOptions<TData>): UseFetchLifecycleResult;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
function normalizeError(err, defaultMessage) {
|
|
3
|
+
return err instanceof Error ? err : new Error(defaultMessage);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Shared fetch/refetch/cleanup/abort lifecycle for data hooks.
|
|
7
|
+
*
|
|
8
|
+
* Manages:
|
|
9
|
+
* - Initial fetch on mount and when deps change
|
|
10
|
+
* - Cancellation of in-flight fetches on unmount or deps change
|
|
11
|
+
* - Refetch with abort signal and cleanup tracking
|
|
12
|
+
* - Error normalization
|
|
13
|
+
*/
|
|
14
|
+
export function useFetchLifecycle({ fetchFn, callbacks, deps, defaultErrorMessage = 'An error occurred', }) {
|
|
15
|
+
// Store latest versions in refs to avoid stale closures in refetch
|
|
16
|
+
const fetchFnRef = useRef(fetchFn);
|
|
17
|
+
fetchFnRef.current = fetchFn;
|
|
18
|
+
const callbacksRef = useRef(callbacks);
|
|
19
|
+
callbacksRef.current = callbacks;
|
|
20
|
+
const defaultErrorMessageRef = useRef(defaultErrorMessage);
|
|
21
|
+
defaultErrorMessageRef.current = defaultErrorMessage;
|
|
22
|
+
// Track in-flight refetch to support cancellation
|
|
23
|
+
const refetchAbortRef = useRef(null);
|
|
24
|
+
// Track refetch cleanup function to prevent memory leaks
|
|
25
|
+
const refetchCleanupRef = useRef(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
let cleanup = null;
|
|
29
|
+
const signal = { cancelled: false, isRefetch: false };
|
|
30
|
+
const fetchData = async () => {
|
|
31
|
+
const context = { isRefetch: false };
|
|
32
|
+
try {
|
|
33
|
+
callbacksRef.current.onStart(context);
|
|
34
|
+
const result = await fetchFnRef.current(signal);
|
|
35
|
+
if (!cancelled) {
|
|
36
|
+
callbacksRef.current.onSuccess(result.data, context);
|
|
37
|
+
cleanup = result.cleanup ?? null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (!cancelled) {
|
|
42
|
+
callbacksRef.current.onError(normalizeError(err, defaultErrorMessageRef.current), context);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
fetchData();
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
signal.cancelled = true;
|
|
50
|
+
// Call cleanup function to release resources
|
|
51
|
+
if (cleanup) {
|
|
52
|
+
cleanup();
|
|
53
|
+
}
|
|
54
|
+
// Clean up any active refetch subscription
|
|
55
|
+
if (refetchCleanupRef.current) {
|
|
56
|
+
refetchCleanupRef.current();
|
|
57
|
+
refetchCleanupRef.current = null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}, deps);
|
|
61
|
+
const refetch = useCallback(async () => {
|
|
62
|
+
// Cancel any in-flight refetch
|
|
63
|
+
if (refetchAbortRef.current) {
|
|
64
|
+
refetchAbortRef.current.cancelled = true;
|
|
65
|
+
}
|
|
66
|
+
// Clean up old refetch subscription to prevent memory leaks
|
|
67
|
+
if (refetchCleanupRef.current) {
|
|
68
|
+
refetchCleanupRef.current();
|
|
69
|
+
refetchCleanupRef.current = null;
|
|
70
|
+
}
|
|
71
|
+
// Create new abort signal for this refetch
|
|
72
|
+
const abortSignal = { cancelled: false, isRefetch: true };
|
|
73
|
+
refetchAbortRef.current = abortSignal;
|
|
74
|
+
const context = { isRefetch: true };
|
|
75
|
+
try {
|
|
76
|
+
callbacksRef.current.onStart(context);
|
|
77
|
+
const result = await fetchFnRef.current(abortSignal);
|
|
78
|
+
if (!abortSignal.cancelled) {
|
|
79
|
+
callbacksRef.current.onSuccess(result.data, context);
|
|
80
|
+
// Store cleanup for next refetch or unmount
|
|
81
|
+
refetchCleanupRef.current = result.cleanup ?? null;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// If cancelled, clean up immediately
|
|
85
|
+
if (result.cleanup) {
|
|
86
|
+
result.cleanup();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (!abortSignal.cancelled) {
|
|
92
|
+
callbacksRef.current.onError(normalizeError(err, defaultErrorMessageRef.current), context);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
// Clear the abort ref if this is still the current refetch
|
|
97
|
+
if (refetchAbortRef.current === abortSignal) {
|
|
98
|
+
refetchAbortRef.current = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, []);
|
|
102
|
+
return { refetch };
|
|
103
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { hubspot } from './hubspot.ts';
|
|
|
3
3
|
export { logger } from './logger.ts';
|
|
4
4
|
export * from './shared/types/index.ts';
|
|
5
5
|
export { Accordion, Alert, AutoGrid, BarChart, Box, Button, ButtonRow, Card, Checkbox, CurrencyInput, DateInput, DescriptionList, DescriptionListItem, Divider, Dropdown, EmptyState, ErrorState, Flex, Form, Heading, Icon, Illustration, Image, Inline, Input, LineChart, Link, List, LoadingButton, LoadingSpinner, Modal, ModalBody, ModalFooter, MultiSelect, NumberInput, Panel, PanelBody, PanelFooter, PanelSection, ProgressBar, RadioButton, ScoreCircle, SearchInput, Select, Spacer, Stack, Statistics, StatisticsItem, StatisticsTrend, StatusTag, StepIndicator, StepperInput, Tab, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, Tag, Text, TextArea, Textarea, Tile, TimeInput, Toggle, ToggleGroup, Tooltip, } from './shared/remoteComponents.tsx';
|
|
6
|
+
export { useDebounce } from './hooks/useDebounce.ts';
|
|
6
7
|
export { useExtensionContext } from './hooks/useExtensionContext.tsx';
|
|
7
8
|
export { useExtensionActions } from './hooks/useExtensionActions.tsx';
|
|
8
9
|
export { useExtensionApi } from './hooks/useExtensionApi.tsx';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { hubspot } from "./hubspot.js";
|
|
|
4
4
|
export { logger } from "./logger.js";
|
|
5
5
|
export * from "./shared/types/index.js";
|
|
6
6
|
export { Accordion, Alert, AutoGrid, BarChart, Box, Button, ButtonRow, Card, Checkbox, CurrencyInput, DateInput, DescriptionList, DescriptionListItem, Divider, Dropdown, EmptyState, ErrorState, Flex, Form, Heading, Icon, Illustration, Image, Inline, Input, LineChart, Link, List, LoadingButton, LoadingSpinner, Modal, ModalBody, ModalFooter, MultiSelect, NumberInput, Panel, PanelBody, PanelFooter, PanelSection, ProgressBar, RadioButton, ScoreCircle, SearchInput, Select, Spacer, Stack, Statistics, StatisticsItem, StatisticsTrend, StatusTag, StepIndicator, StepperInput, Tab, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, Tag, Text, TextArea, Textarea, Tile, TimeInput, Toggle, ToggleGroup, Tooltip, } from "./shared/remoteComponents.js";
|
|
7
|
+
export { useDebounce } from "./hooks/useDebounce.js";
|
|
7
8
|
export { useExtensionContext } from "./hooks/useExtensionContext.js";
|
|
8
9
|
export { useExtensionActions } from "./hooks/useExtensionActions.js";
|
|
9
10
|
export { useExtensionApi } from "./hooks/useExtensionApi.js";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { JsonSerializable } from '../shared/types/shared.ts';
|
|
2
|
+
import type { RendererMocksContext } from '../testing/internal/mocks/index.ts';
|
|
3
|
+
import type { RendererMocks } from '../testing/types.ts';
|
|
2
4
|
type AnyFunction = (...args: any[]) => any;
|
|
3
5
|
/**
|
|
4
6
|
* Creates a mock-aware hook function that can be used to mock the original hook function.
|
|
@@ -8,19 +10,24 @@ type AnyFunction = (...args: any[]) => any;
|
|
|
8
10
|
* @param originalHookFunction The original hook function to call if no mock is found
|
|
9
11
|
* @returns The mocked hook function or the original hook function if no mock is found
|
|
10
12
|
*/
|
|
11
|
-
export declare const createMockAwareHook: <THookName extends keyof
|
|
13
|
+
export declare const createMockAwareHook: <THookName extends keyof RendererMocks, THookFunction extends AnyFunction>(hookName: THookName, originalHookFunction: THookFunction) => THookFunction;
|
|
12
14
|
/**
|
|
13
15
|
* A hook that provides access to the Mocks context.
|
|
14
|
-
* Returns the mocks
|
|
16
|
+
* Returns the mocks context value if inside a MocksContextProvider, otherwise returns null.
|
|
15
17
|
*
|
|
16
|
-
* @returns The mocks
|
|
18
|
+
* @returns The mocks context value or null if not in a test environment.
|
|
17
19
|
*/
|
|
18
|
-
export declare function useMocksContext():
|
|
20
|
+
export declare function useMocksContext(): RendererMocksContext | null;
|
|
19
21
|
/**
|
|
20
22
|
* A React component that provides the Mocks context that can be used to provide mocks to the mock-aware hook functions.
|
|
21
23
|
*
|
|
22
24
|
* @param children The children to render.
|
|
23
25
|
* @returns The children wrapped in the Mocks context provider.
|
|
24
26
|
*/
|
|
25
|
-
export declare const MocksContextProvider: import("react").Provider<
|
|
27
|
+
export declare const MocksContextProvider: import("react").Provider<RendererMocksContext | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Stabilizes a value's reference identity across re-renders using deep
|
|
30
|
+
* comparison via JSON.stringify.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useStableValue<T extends JsonSerializable>(value: T): T;
|
|
26
33
|
export {};
|