@hubspot/ui-extensions 0.12.4 → 0.13.1
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/__generated__/version.d.ts +2 -0
- package/dist/__generated__/version.js +4 -0
- package/dist/__tests__/crm/hooks/useAssociations.spec.js +79 -0
- package/dist/__tests__/crm/utils/fetchAssociations.spec.js +0 -10
- package/dist/__tests__/experimental/hooks/useCrmSearch.spec.js +586 -0
- package/dist/__tests__/experimental/hooks/utils/fetchCrmSearch.spec.js +217 -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 +3 -3
- package/dist/crm/hooks/useAssociations.js +55 -139
- package/dist/crm/hooks/useCrmProperties.js +29 -125
- package/dist/crm/utils/fetchAssociations.js +0 -3
- package/dist/experimental/hooks/useCrmSearch.d.ts +2 -0
- package/dist/experimental/hooks/useCrmSearch.js +206 -0
- package/dist/experimental/hooks/utils/fetchCrmSearch.d.ts +6 -0
- package/dist/experimental/hooks/utils/fetchCrmSearch.js +39 -0
- package/dist/experimental/index.d.ts +1 -0
- package/dist/experimental/index.js +1 -0
- 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/pages/types.js +1 -0
- package/dist/shared/types/actions.d.ts +12 -2
- package/dist/shared/types/components/table.d.ts +16 -0
- package/dist/shared/types/context.d.ts +6 -0
- package/dist/shared/types/crm.d.ts +4 -0
- package/dist/shared/types/experimental.d.ts +1 -1
- package/dist/shared/types/extension-points.d.ts +9 -3
- package/dist/shared/types/extension-points.js +1 -0
- package/dist/shared/types/hooks.d.ts +72 -0
- package/dist/shared/types/hooks.js +1 -0
- package/dist/shared/types/shared.d.ts +8 -0
- package/dist/shared/types/worker-globals.d.ts +5 -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/internal/mocks/mock-hooks.js +26 -0
- package/dist/testing/internal/types-internal.d.ts +2 -0
- package/dist/testing/types.d.ts +11 -1
- package/dist/utils/pagination.d.ts +0 -9
- package/package.json +3 -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.d.ts +0 -1
- /package/dist/{experimental/pages/create-page-router.test.d.ts → __tests__/experimental/hooks/useCrmSearch.spec.d.ts} +0 -0
- /package/dist/{experimental/pages/internal/trie-router.test.d.ts → __tests__/experimental/hooks/utils/fetchCrmSearch.spec.d.ts} +0 -0
- /package/dist/{experimental/pages/types.js → __tests__/hooks/useDebounce.spec.d.ts} +0 -0
- /package/dist/{shared/types/pages.js → __tests__/hooks/utils/useFetchLifecycle.spec.d.ts} +0 -0
- /package/dist/{shared/types/pages/app-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/components/page-routes.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/index.js → pages/internal/trie-router.test.d.ts} +0 -0
- /package/dist/{experimental/pages → pages}/internal/trie-router.test.js +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PaginationResult } from '../../shared/types/hooks.ts';
|
|
2
2
|
import type { AssociationResult, FetchAssociationsRequest } from '../utils/fetchAssociations.ts';
|
|
3
3
|
import { type FetchCrmPropertiesOptions } from '../utils/fetchCrmProperties.ts';
|
|
4
|
-
export
|
|
4
|
+
export type UseAssociationsOptions = {
|
|
5
5
|
propertiesToFormat?: 'all' | string[];
|
|
6
6
|
formattingOptions?: FetchCrmPropertiesOptions['formattingOptions'];
|
|
7
|
-
}
|
|
7
|
+
};
|
|
8
8
|
export interface UseAssociationsResult {
|
|
9
9
|
results: AssociationResult[];
|
|
10
10
|
error: Error | null;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useReducer, useState } 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
|
-
function createInitialState(
|
|
6
|
+
function createInitialState() {
|
|
6
7
|
return {
|
|
7
8
|
results: [],
|
|
8
9
|
error: null,
|
|
9
10
|
isLoading: true,
|
|
10
11
|
isRefetching: false,
|
|
11
12
|
currentPage: 1,
|
|
12
|
-
pageSize,
|
|
13
13
|
hasMore: false,
|
|
14
14
|
currentOffset: undefined,
|
|
15
15
|
nextOffset: undefined,
|
|
@@ -71,6 +71,7 @@ function associationsReducer(state, action) {
|
|
|
71
71
|
...state,
|
|
72
72
|
currentPage: 1,
|
|
73
73
|
results: [],
|
|
74
|
+
isLoading: true,
|
|
74
75
|
hasMore: false,
|
|
75
76
|
error: null,
|
|
76
77
|
currentOffset: undefined,
|
|
@@ -108,40 +109,18 @@ const DEFAULT_OPTIONS = {};
|
|
|
108
109
|
*/
|
|
109
110
|
function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
110
111
|
const pageSize = config?.pageLength ?? DEFAULT_PAGE_SIZE;
|
|
111
|
-
const [state, dispatch] = useReducer(associationsReducer,
|
|
112
|
+
const [state, dispatch] = useReducer(associationsReducer, createInitialState());
|
|
112
113
|
/**
|
|
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.
|
|
114
|
+
* Reset pagination when pageSize changes.
|
|
115
|
+
* Render-phase update (not useEffect) avoids committing a render with stale paginated results.
|
|
118
116
|
*/
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
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]);
|
|
117
|
+
const [prevPageSize, setPrevPageSize] = useState(pageSize);
|
|
118
|
+
if (prevPageSize !== pageSize) {
|
|
119
|
+
setPrevPageSize(pageSize);
|
|
120
|
+
dispatch({ type: 'RESET' });
|
|
121
|
+
}
|
|
122
|
+
const stableConfig = useStableValue(config);
|
|
123
|
+
const stableOptions = useStableValue(options);
|
|
145
124
|
// Pagination actions
|
|
146
125
|
const nextPage = useCallback(() => {
|
|
147
126
|
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
@@ -156,122 +135,59 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
|
156
135
|
}
|
|
157
136
|
}, [state.currentPage, state.hasMore]);
|
|
158
137
|
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;
|
|
138
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
139
|
+
// Only reset to first page if we're not already on the first page
|
|
140
|
+
if (paginationFlags.hasPreviousPage) {
|
|
141
|
+
dispatch({ type: 'RESET' });
|
|
228
142
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
dispatch({ type: 'REFETCH_START' });
|
|
233
|
-
// Build request using current offset token to refetch current page
|
|
143
|
+
}, [state.currentPage, state.hasMore]);
|
|
144
|
+
const { refetch } = useFetchLifecycle({
|
|
145
|
+
fetchFn: () => {
|
|
234
146
|
const request = {
|
|
235
147
|
toObjectType: stableConfig?.toObjectType,
|
|
236
148
|
properties: stableConfig?.properties,
|
|
237
149
|
pageLength: pageSize,
|
|
238
150
|
offset: state.currentOffset,
|
|
239
151
|
};
|
|
240
|
-
|
|
152
|
+
return fetchAssociations(request, {
|
|
241
153
|
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
242
154
|
formattingOptions: stableOptions.formattingOptions,
|
|
243
155
|
});
|
|
244
|
-
|
|
245
|
-
|
|
156
|
+
},
|
|
157
|
+
callbacks: {
|
|
158
|
+
onStart: ({ isRefetch }) => dispatch({ type: isRefetch ? 'REFETCH_START' : 'FETCH_START' }),
|
|
159
|
+
onSuccess: (data, { isRefetch }) => dispatch(isRefetch
|
|
160
|
+
? {
|
|
246
161
|
type: 'REFETCH_SUCCESS',
|
|
247
162
|
payload: {
|
|
248
|
-
results:
|
|
249
|
-
hasMore:
|
|
250
|
-
nextOffset:
|
|
163
|
+
results: data.results,
|
|
164
|
+
hasMore: data.hasMore,
|
|
165
|
+
nextOffset: data.nextOffset,
|
|
251
166
|
},
|
|
252
|
-
});
|
|
253
|
-
refetchCleanupRef.current = result.cleanup;
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
if (result.cleanup) {
|
|
257
|
-
result.cleanup();
|
|
258
167
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
168
|
+
: {
|
|
169
|
+
type: 'FETCH_SUCCESS',
|
|
170
|
+
payload: {
|
|
171
|
+
results: data.results,
|
|
172
|
+
hasMore: data.hasMore,
|
|
173
|
+
nextOffset: data.nextOffset,
|
|
174
|
+
currentOffset: state.currentOffset,
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
onError: (error, { isRefetch }) => dispatch({
|
|
178
|
+
type: isRefetch ? 'REFETCH_ERROR' : 'FETCH_ERROR',
|
|
179
|
+
payload: error,
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
deps: [
|
|
183
|
+
stableConfig,
|
|
184
|
+
stableOptions,
|
|
185
|
+
state.currentPage,
|
|
186
|
+
state.currentOffset,
|
|
187
|
+
pageSize,
|
|
188
|
+
],
|
|
189
|
+
defaultErrorMessage: 'Failed to fetch associations',
|
|
190
|
+
});
|
|
275
191
|
// Calculate pagination flags
|
|
276
192
|
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
277
193
|
return {
|
|
@@ -284,7 +200,7 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
|
284
200
|
hasNextPage: paginationFlags.hasNextPage,
|
|
285
201
|
hasPreviousPage: paginationFlags.hasPreviousPage,
|
|
286
202
|
currentPage: state.currentPage,
|
|
287
|
-
pageSize
|
|
203
|
+
pageSize,
|
|
288
204
|
nextPage,
|
|
289
205
|
previousPage,
|
|
290
206
|
reset,
|
|
@@ -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,
|
|
@@ -28,9 +28,6 @@ export const fetchAssociations = async (request, options) => {
|
|
|
28
28
|
if (result.error) {
|
|
29
29
|
throw new Error(result.error);
|
|
30
30
|
}
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
throw new Error(`Failed to fetch associations: ${response.statusText}`);
|
|
33
|
-
}
|
|
34
31
|
if (!isAssociationsResponse(result.data)) {
|
|
35
32
|
throw new Error('Invalid response format');
|
|
36
33
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useCallback, useReducer, useState } from 'react';
|
|
2
|
+
import { useFetchLifecycle } from "../../hooks/utils/useFetchLifecycle.js";
|
|
3
|
+
import { useStableValue } from "../../internal/hook-utils.js";
|
|
4
|
+
import { calculatePaginationFlags, DEFAULT_PAGE_SIZE, } from "../../utils/pagination.js";
|
|
5
|
+
import { fetchCrmSearch } from "./utils/fetchCrmSearch.js";
|
|
6
|
+
function createInitialState() {
|
|
7
|
+
return {
|
|
8
|
+
results: [],
|
|
9
|
+
total: 0,
|
|
10
|
+
error: null,
|
|
11
|
+
isLoading: true,
|
|
12
|
+
isRefetching: false,
|
|
13
|
+
currentPage: 1,
|
|
14
|
+
hasMore: false,
|
|
15
|
+
currentCursor: undefined,
|
|
16
|
+
nextCursor: undefined,
|
|
17
|
+
offsetHistory: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function crmSearchReducer(state, action) {
|
|
21
|
+
switch (action.type) {
|
|
22
|
+
case 'FETCH_START':
|
|
23
|
+
return {
|
|
24
|
+
...state,
|
|
25
|
+
isLoading: true,
|
|
26
|
+
error: null,
|
|
27
|
+
};
|
|
28
|
+
case 'FETCH_SUCCESS':
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
isLoading: false,
|
|
32
|
+
results: action.payload.results,
|
|
33
|
+
total: action.payload.total,
|
|
34
|
+
hasMore: action.payload.hasMore,
|
|
35
|
+
currentCursor: action.payload.currentCursor,
|
|
36
|
+
nextCursor: action.payload.nextCursor,
|
|
37
|
+
error: null,
|
|
38
|
+
};
|
|
39
|
+
case 'FETCH_ERROR':
|
|
40
|
+
return {
|
|
41
|
+
...state,
|
|
42
|
+
isLoading: false,
|
|
43
|
+
error: action.payload,
|
|
44
|
+
results: [],
|
|
45
|
+
total: 0,
|
|
46
|
+
hasMore: false,
|
|
47
|
+
currentCursor: undefined,
|
|
48
|
+
nextCursor: undefined,
|
|
49
|
+
};
|
|
50
|
+
case 'NEXT_PAGE':
|
|
51
|
+
return {
|
|
52
|
+
...state,
|
|
53
|
+
currentPage: state.currentPage + 1,
|
|
54
|
+
offsetHistory: state.currentCursor !== undefined
|
|
55
|
+
? [...state.offsetHistory, state.currentCursor]
|
|
56
|
+
: state.offsetHistory,
|
|
57
|
+
currentCursor: state.nextCursor,
|
|
58
|
+
nextCursor: undefined,
|
|
59
|
+
};
|
|
60
|
+
case 'PREVIOUS_PAGE': {
|
|
61
|
+
const newPage = Math.max(1, state.currentPage - 1);
|
|
62
|
+
const newHistory = [...state.offsetHistory];
|
|
63
|
+
const previousCursor = newHistory.pop();
|
|
64
|
+
return {
|
|
65
|
+
...state,
|
|
66
|
+
currentPage: newPage,
|
|
67
|
+
offsetHistory: newHistory,
|
|
68
|
+
currentCursor: previousCursor,
|
|
69
|
+
nextCursor: undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
case 'RESET':
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
currentPage: 1,
|
|
76
|
+
results: [],
|
|
77
|
+
total: 0,
|
|
78
|
+
isLoading: true,
|
|
79
|
+
hasMore: false,
|
|
80
|
+
error: null,
|
|
81
|
+
currentCursor: undefined,
|
|
82
|
+
nextCursor: undefined,
|
|
83
|
+
offsetHistory: [],
|
|
84
|
+
};
|
|
85
|
+
case 'REFETCH_START':
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
isRefetching: true,
|
|
89
|
+
error: null,
|
|
90
|
+
};
|
|
91
|
+
case 'REFETCH_SUCCESS':
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
isRefetching: false,
|
|
95
|
+
results: action.payload.results,
|
|
96
|
+
total: action.payload.total,
|
|
97
|
+
hasMore: action.payload.hasMore,
|
|
98
|
+
nextCursor: action.payload.nextCursor,
|
|
99
|
+
error: null,
|
|
100
|
+
};
|
|
101
|
+
case 'REFETCH_ERROR':
|
|
102
|
+
return {
|
|
103
|
+
...state,
|
|
104
|
+
isRefetching: false,
|
|
105
|
+
error: action.payload,
|
|
106
|
+
};
|
|
107
|
+
default:
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const DEFAULT_OPTIONS = {};
|
|
112
|
+
export function useCrmSearch(config, options = DEFAULT_OPTIONS) {
|
|
113
|
+
const pageSize = config?.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
114
|
+
const [state, dispatch] = useReducer(crmSearchReducer, createInitialState());
|
|
115
|
+
// Reset pagination when pageSize changes.
|
|
116
|
+
// Render-phase update (not useEffect) avoids committing a render with stale paginated results.
|
|
117
|
+
const [prevPageSize, setPrevPageSize] = useState(pageSize);
|
|
118
|
+
if (prevPageSize !== pageSize) {
|
|
119
|
+
setPrevPageSize(pageSize);
|
|
120
|
+
dispatch({ type: 'RESET' });
|
|
121
|
+
}
|
|
122
|
+
const stableConfig = useStableValue(config);
|
|
123
|
+
const stableOptions = useStableValue(options);
|
|
124
|
+
const nextPage = useCallback(() => {
|
|
125
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
126
|
+
if (paginationFlags.hasNextPage) {
|
|
127
|
+
dispatch({ type: 'NEXT_PAGE' });
|
|
128
|
+
}
|
|
129
|
+
}, [state.currentPage, state.hasMore]);
|
|
130
|
+
const previousPage = useCallback(() => {
|
|
131
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
132
|
+
if (paginationFlags.hasPreviousPage) {
|
|
133
|
+
dispatch({ type: 'PREVIOUS_PAGE' });
|
|
134
|
+
}
|
|
135
|
+
}, [state.currentPage, state.hasMore]);
|
|
136
|
+
const reset = useCallback(() => {
|
|
137
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
138
|
+
if (paginationFlags.hasPreviousPage) {
|
|
139
|
+
dispatch({ type: 'RESET' });
|
|
140
|
+
}
|
|
141
|
+
}, [state.currentPage, state.hasMore]);
|
|
142
|
+
const { refetch } = useFetchLifecycle({
|
|
143
|
+
fetchFn: () => {
|
|
144
|
+
const request = {
|
|
145
|
+
objectType: stableConfig?.objectType,
|
|
146
|
+
properties: stableConfig?.properties,
|
|
147
|
+
query: stableConfig?.query,
|
|
148
|
+
filterGroups: stableConfig?.filterGroups,
|
|
149
|
+
sorts: stableConfig?.sorts,
|
|
150
|
+
pageSize,
|
|
151
|
+
after: state.currentCursor,
|
|
152
|
+
};
|
|
153
|
+
return fetchCrmSearch(request, {
|
|
154
|
+
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
155
|
+
formattingOptions: stableOptions.formattingOptions,
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
callbacks: {
|
|
159
|
+
onStart: ({ isRefetch }) => dispatch({ type: isRefetch ? 'REFETCH_START' : 'FETCH_START' }),
|
|
160
|
+
onSuccess: (data, { isRefetch }) => dispatch(isRefetch
|
|
161
|
+
? {
|
|
162
|
+
type: 'REFETCH_SUCCESS',
|
|
163
|
+
payload: {
|
|
164
|
+
results: data.results,
|
|
165
|
+
total: data.total,
|
|
166
|
+
hasMore: data.hasMore,
|
|
167
|
+
nextCursor: data.after,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: {
|
|
171
|
+
type: 'FETCH_SUCCESS',
|
|
172
|
+
payload: {
|
|
173
|
+
results: data.results,
|
|
174
|
+
total: data.total,
|
|
175
|
+
hasMore: data.hasMore,
|
|
176
|
+
nextCursor: data.after,
|
|
177
|
+
currentCursor: state.currentCursor,
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
onError: (error, { isRefetch }) => dispatch({
|
|
181
|
+
type: isRefetch ? 'REFETCH_ERROR' : 'FETCH_ERROR',
|
|
182
|
+
payload: error,
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
deps: [stableConfig, stableOptions, state.currentCursor],
|
|
186
|
+
defaultErrorMessage: 'Failed to fetch CRM search results',
|
|
187
|
+
});
|
|
188
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
189
|
+
return {
|
|
190
|
+
results: state.results,
|
|
191
|
+
total: state.total,
|
|
192
|
+
error: state.error,
|
|
193
|
+
isLoading: state.isLoading,
|
|
194
|
+
isRefetching: state.isRefetching,
|
|
195
|
+
refetch,
|
|
196
|
+
pagination: {
|
|
197
|
+
hasNextPage: paginationFlags.hasNextPage,
|
|
198
|
+
hasPreviousPage: paginationFlags.hasPreviousPage,
|
|
199
|
+
currentPage: state.currentPage,
|
|
200
|
+
pageSize,
|
|
201
|
+
nextPage,
|
|
202
|
+
previousPage,
|
|
203
|
+
reset,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CrmSearchResponse, FetchCrmSearchRequest, FetchHookOptions } from '../../../shared/types/hooks.ts';
|
|
2
|
+
export interface FetchCrmSearchResult {
|
|
3
|
+
data: CrmSearchResponse;
|
|
4
|
+
cleanup: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare const fetchCrmSearch: (request: FetchCrmSearchRequest, options?: FetchHookOptions) => Promise<FetchCrmSearchResult>;
|