@hubspot/ui-extensions 0.12.4 → 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__/hooks/useDebounce.spec.js +123 -0
- package/dist/__tests__/hooks/utils/useFetchLifecycle.spec.js +324 -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 +2 -2
- package/dist/crm/hooks/useAssociations.js +44 -137
- package/dist/crm/hooks/useCrmProperties.js +29 -125
- 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 +6 -0
- package/dist/internal/hook-utils.js +16 -1
- package/dist/{experimental/pages → pages}/components/page-routes.d.ts +1 -2
- package/dist/{experimental/pages → pages}/components/page-routes.js +0 -4
- package/dist/{experimental/pages → pages}/create-page-router.d.ts +1 -3
- package/dist/{experimental/pages → pages}/create-page-router.js +1 -3
- package/dist/{experimental/pages → pages}/create-page-router.test.js +2 -2
- package/dist/{experimental/pages → pages}/hooks.d.ts +0 -1
- package/dist/{experimental/pages → pages}/hooks.js +0 -1
- package/dist/pages/index.d.ts +6 -1
- package/dist/pages/index.js +4 -1
- package/dist/{experimental/pages → pages}/internal/page-router-internal-types.d.ts +1 -1
- package/dist/pages/internal/useAppPageLocation.d.ts +1 -0
- package/dist/{experimental/pages → pages}/internal/useAppPageLocation.js +2 -2
- package/dist/{experimental/pages → pages}/types.d.ts +1 -2
- package/dist/shared/types/actions.d.ts +12 -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/shared.d.ts +8 -0
- package/dist/testing/__tests__/createRenderer.spec.js +1 -1
- package/dist/testing/internal/mocks/index.d.ts +1 -1
- package/dist/testing/internal/mocks/mock-extension-point-api.js +21 -1
- package/dist/testing/types.d.ts +1 -1
- package/package.json +2 -2
- package/dist/experimental/pages/index.d.ts +0 -6
- package/dist/experimental/pages/index.js +0 -4
- package/dist/experimental/pages/internal/useAppPageLocation.d.ts +0 -1
- package/dist/shared/types/pages/app-pages-types.d.ts +0 -75
- package/dist/shared/types/pages/components/page-routes.d.ts +0 -115
- package/dist/shared/types/pages/index.d.ts +0 -1
- package/dist/shared/types/pages/index.js +0 -1
- package/dist/shared/types/pages.d.ts +0 -1
- /package/dist/{experimental/pages/create-page-router.test.d.ts → __tests__/hooks/useDebounce.spec.d.ts} +0 -0
- /package/dist/{experimental/pages/internal/trie-router.test.d.ts → __tests__/hooks/utils/useFetchLifecycle.spec.d.ts} +0 -0
- /package/dist/{experimental/pages/types.js → __tests__/internal/hook-utils.spec.d.ts} +0 -0
- /package/dist/{experimental/pages → pages}/components/index.d.ts +0 -0
- /package/dist/{experimental/pages → pages}/components/index.js +0 -0
- /package/dist/{shared/types/pages.js → pages/create-page-router.test.d.ts} +0 -0
- /package/dist/{experimental/pages → pages}/internal/app-page-route-context.d.ts +0 -0
- /package/dist/{experimental/pages → pages}/internal/app-page-route-context.js +0 -0
- /package/dist/{experimental/pages → pages}/internal/convert-page-routes-react-elements.d.ts +0 -0
- /package/dist/{experimental/pages → pages}/internal/convert-page-routes-react-elements.js +0 -0
- /package/dist/{experimental/pages → pages}/internal/page-router-internal-types.js +0 -0
- /package/dist/{experimental/pages → pages}/internal/trie-router.d.ts +0 -0
- /package/dist/{experimental/pages → pages}/internal/trie-router.js +0 -0
- /package/dist/{shared/types/pages/app-pages-types.js → pages/internal/trie-router.test.d.ts} +0 -0
- /package/dist/{experimental/pages → pages}/internal/trie-router.test.js +0 -0
- /package/dist/{shared/types/pages/components/page-routes.js → pages/types.js} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
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";
|
|
3
4
|
import { calculatePaginationFlags, DEFAULT_PAGE_SIZE, } from "../../utils/pagination.js";
|
|
4
5
|
import { fetchAssociations } from "../utils/fetchAssociations.js";
|
|
5
6
|
function createInitialState(pageSize) {
|
|
@@ -109,39 +110,8 @@ const DEFAULT_OPTIONS = {};
|
|
|
109
110
|
function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
110
111
|
const pageSize = config?.pageLength ?? DEFAULT_PAGE_SIZE;
|
|
111
112
|
const [state, dispatch] = useReducer(associationsReducer, useMemo(() => createInitialState(pageSize), [pageSize]));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
* Create stable references for config and options to prevent unnecessary re-renders and API calls.
|
|
116
|
-
* Then, external developers can pass inline objects without worrying about memoization
|
|
117
|
-
* We handle the deep equality comparison ourselves, and return the same object reference when content is equivalent.
|
|
118
|
-
*/
|
|
119
|
-
const lastConfigRef = useRef();
|
|
120
|
-
const lastConfigKeyRef = useRef();
|
|
121
|
-
const lastOptionsRef = useRef();
|
|
122
|
-
const lastOptionsKeyRef = useRef();
|
|
123
|
-
// Track in-flight refetch to support cancellation
|
|
124
|
-
const refetchAbortRef = useRef(null);
|
|
125
|
-
// Track refetch cleanup function to prevent memory leaks
|
|
126
|
-
const refetchCleanupRef = useRef(null);
|
|
127
|
-
const stableConfig = useMemo(() => {
|
|
128
|
-
const configKey = JSON.stringify(config);
|
|
129
|
-
if (configKey === lastConfigKeyRef.current) {
|
|
130
|
-
return lastConfigRef.current;
|
|
131
|
-
}
|
|
132
|
-
lastConfigKeyRef.current = configKey;
|
|
133
|
-
lastConfigRef.current = config;
|
|
134
|
-
return config;
|
|
135
|
-
}, [config]);
|
|
136
|
-
const stableOptions = useMemo(() => {
|
|
137
|
-
const optionsKey = JSON.stringify(options);
|
|
138
|
-
if (optionsKey === lastOptionsKeyRef.current) {
|
|
139
|
-
return lastOptionsRef.current;
|
|
140
|
-
}
|
|
141
|
-
lastOptionsKeyRef.current = optionsKey;
|
|
142
|
-
lastOptionsRef.current = options;
|
|
143
|
-
return options;
|
|
144
|
-
}, [options]);
|
|
113
|
+
const stableConfig = useStableValue(config);
|
|
114
|
+
const stableOptions = useStableValue(options);
|
|
145
115
|
// Pagination actions
|
|
146
116
|
const nextPage = useCallback(() => {
|
|
147
117
|
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
@@ -156,122 +126,59 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
|
156
126
|
}
|
|
157
127
|
}, [state.currentPage, state.hasMore]);
|
|
158
128
|
const reset = useCallback(() => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
let cancelled = false;
|
|
164
|
-
let cleanup = null;
|
|
165
|
-
const fetchData = async () => {
|
|
166
|
-
try {
|
|
167
|
-
dispatch({ type: 'FETCH_START' });
|
|
168
|
-
// Build request using current offset token
|
|
169
|
-
const request = {
|
|
170
|
-
toObjectType: stableConfig?.toObjectType,
|
|
171
|
-
properties: stableConfig?.properties,
|
|
172
|
-
pageLength: pageSize,
|
|
173
|
-
offset: state.currentOffset,
|
|
174
|
-
};
|
|
175
|
-
const result = await fetchAssociations(request, {
|
|
176
|
-
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
177
|
-
formattingOptions: stableOptions.formattingOptions,
|
|
178
|
-
});
|
|
179
|
-
if (!cancelled) {
|
|
180
|
-
dispatch({
|
|
181
|
-
type: 'FETCH_SUCCESS',
|
|
182
|
-
payload: {
|
|
183
|
-
results: result.data.results,
|
|
184
|
-
hasMore: result.data.hasMore,
|
|
185
|
-
nextOffset: result.data.nextOffset,
|
|
186
|
-
currentOffset: state.currentOffset,
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
cleanup = result.cleanup;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
if (!cancelled) {
|
|
194
|
-
const errorData = err instanceof Error
|
|
195
|
-
? err
|
|
196
|
-
: new Error('Failed to fetch associations');
|
|
197
|
-
dispatch({ type: 'FETCH_ERROR', payload: errorData });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
fetchData();
|
|
202
|
-
return () => {
|
|
203
|
-
cancelled = true;
|
|
204
|
-
// Call cleanup function to release resources
|
|
205
|
-
if (cleanup) {
|
|
206
|
-
cleanup();
|
|
207
|
-
}
|
|
208
|
-
// Clean up any active refetch subscription
|
|
209
|
-
if (refetchCleanupRef.current) {
|
|
210
|
-
refetchCleanupRef.current();
|
|
211
|
-
refetchCleanupRef.current = null;
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
}, [
|
|
215
|
-
stableConfig,
|
|
216
|
-
stableOptions,
|
|
217
|
-
state.currentPage,
|
|
218
|
-
state.currentOffset,
|
|
219
|
-
pageSize,
|
|
220
|
-
]);
|
|
221
|
-
const refetch = useCallback(async () => {
|
|
222
|
-
if (refetchAbortRef.current) {
|
|
223
|
-
refetchAbortRef.current.cancelled = true;
|
|
224
|
-
}
|
|
225
|
-
if (refetchCleanupRef.current) {
|
|
226
|
-
refetchCleanupRef.current();
|
|
227
|
-
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' });
|
|
228
133
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
dispatch({ type: 'REFETCH_START' });
|
|
233
|
-
// Build request using current offset token to refetch current page
|
|
134
|
+
}, [state.currentPage, state.hasMore]);
|
|
135
|
+
const { refetch } = useFetchLifecycle({
|
|
136
|
+
fetchFn: () => {
|
|
234
137
|
const request = {
|
|
235
138
|
toObjectType: stableConfig?.toObjectType,
|
|
236
139
|
properties: stableConfig?.properties,
|
|
237
140
|
pageLength: pageSize,
|
|
238
141
|
offset: state.currentOffset,
|
|
239
142
|
};
|
|
240
|
-
|
|
143
|
+
return fetchAssociations(request, {
|
|
241
144
|
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
242
145
|
formattingOptions: stableOptions.formattingOptions,
|
|
243
146
|
});
|
|
244
|
-
|
|
245
|
-
|
|
147
|
+
},
|
|
148
|
+
callbacks: {
|
|
149
|
+
onStart: ({ isRefetch }) => dispatch({ type: isRefetch ? 'REFETCH_START' : 'FETCH_START' }),
|
|
150
|
+
onSuccess: (data, { isRefetch }) => dispatch(isRefetch
|
|
151
|
+
? {
|
|
246
152
|
type: 'REFETCH_SUCCESS',
|
|
247
153
|
payload: {
|
|
248
|
-
results:
|
|
249
|
-
hasMore:
|
|
250
|
-
nextOffset:
|
|
154
|
+
results: data.results,
|
|
155
|
+
hasMore: data.hasMore,
|
|
156
|
+
nextOffset: data.nextOffset,
|
|
251
157
|
},
|
|
252
|
-
});
|
|
253
|
-
refetchCleanupRef.current = result.cleanup;
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
if (result.cleanup) {
|
|
257
|
-
result.cleanup();
|
|
258
158
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
});
|
|
275
182
|
// Calculate pagination flags
|
|
276
183
|
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
277
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,
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import type { JsonSerializable } from '../shared/types/shared.ts';
|
|
1
2
|
import type { RendererMocksContext } from '../testing/internal/mocks/index.ts';
|
|
2
3
|
import type { RendererMocks } from '../testing/types.ts';
|
|
3
4
|
type AnyFunction = (...args: any[]) => any;
|
|
@@ -24,4 +25,9 @@ export declare function useMocksContext(): RendererMocksContext | null;
|
|
|
24
25
|
* @returns The children wrapped in the Mocks context provider.
|
|
25
26
|
*/
|
|
26
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;
|
|
27
33
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react';
|
|
1
|
+
import { createContext, useContext, useRef } from 'react';
|
|
2
2
|
const ReactRenderMocksContext = createContext(null);
|
|
3
3
|
/**
|
|
4
4
|
* Creates a mock-aware hook function that can be used to mock the original hook function.
|
|
@@ -40,3 +40,18 @@ export function useMocksContext() {
|
|
|
40
40
|
* @returns The children wrapped in the Mocks context provider.
|
|
41
41
|
*/
|
|
42
42
|
export const MocksContextProvider = ReactRenderMocksContext.Provider;
|
|
43
|
+
/**
|
|
44
|
+
* Stabilizes a value's reference identity across re-renders using deep
|
|
45
|
+
* comparison via JSON.stringify.
|
|
46
|
+
*/
|
|
47
|
+
export function useStableValue(value) {
|
|
48
|
+
const stableRef = useRef({
|
|
49
|
+
key: JSON.stringify(value),
|
|
50
|
+
value,
|
|
51
|
+
});
|
|
52
|
+
const key = JSON.stringify(value);
|
|
53
|
+
if (key !== stableRef.current.key) {
|
|
54
|
+
stableRef.current = { key, value };
|
|
55
|
+
}
|
|
56
|
+
return stableRef.current.value;
|
|
57
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
-
import type { EmptyProps } from '
|
|
2
|
+
import type { EmptyProps } from '../../shared/types/shared.ts';
|
|
3
3
|
/**
|
|
4
4
|
* The props type for the layout component of [PageRoutes](https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensibility/ui-components/app-page-components/page-routes#pageroutes).
|
|
5
5
|
*/
|
|
@@ -72,7 +72,6 @@ export interface PageRoutesProps {
|
|
|
72
72
|
*
|
|
73
73
|
* See [PageRoutes](https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensibility/ui-components/app-page-components/page-routes#pageroutes) for more information.
|
|
74
74
|
*
|
|
75
|
-
* @experimental This component is experimental. Avoid using it in production due to potential breaking changes. Your feedback is valuable for improvements. Stay tuned for updates.
|
|
76
75
|
*/
|
|
77
76
|
export declare function PageRoutes(__props: PageRoutesProps): null;
|
|
78
77
|
export declare namespace PageRoutes {
|