@hubspot/ui-extensions 0.11.5 → 0.12.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 +96 -0
- package/dist/__tests__/crm/hooks/useCrmProperties.spec.js +170 -1
- package/dist/crm/hooks/useAssociations.d.ts +2 -0
- package/dist/crm/hooks/useAssociations.js +87 -0
- package/dist/crm/hooks/useCrmProperties.d.ts +5 -1
- package/dist/crm/hooks/useCrmProperties.js +81 -2
- package/dist/hs-internal/__tests__/createRemoteComponentInternal.spec.d.ts +1 -0
- package/dist/hs-internal/__tests__/createRemoteComponentInternal.spec.js +139 -0
- package/dist/hs-internal/index.d.ts +35 -0
- package/dist/hs-internal/index.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/remoteComponents.d.ts +9 -0
- package/dist/shared/remoteComponents.js +9 -0
- package/dist/shared/types/components/accordion.d.ts +5 -5
- package/dist/shared/types/components/alert.d.ts +2 -2
- package/dist/shared/types/components/button-row.d.ts +5 -2
- package/dist/shared/types/components/button.d.ts +16 -10
- package/dist/shared/types/components/chart.d.ts +3 -3
- package/dist/shared/types/components/description-list.d.ts +2 -2
- package/dist/shared/types/components/dropdown.d.ts +8 -8
- package/dist/shared/types/components/empty-state.d.ts +5 -7
- package/dist/shared/types/components/error-state.d.ts +2 -2
- package/dist/shared/types/components/form.d.ts +2 -2
- package/dist/shared/types/components/heading.d.ts +1 -1
- package/dist/shared/types/components/icon.d.ts +4 -5
- package/dist/shared/types/components/illustration.d.ts +12 -0
- package/dist/shared/types/components/image.d.ts +9 -4
- package/dist/shared/types/components/index.d.ts +1 -0
- package/dist/shared/types/components/inputs.d.ts +61 -63
- package/dist/shared/types/components/layouts.d.ts +17 -24
- package/dist/shared/types/components/link.d.ts +8 -5
- package/dist/shared/types/components/loading-spinner.d.ts +3 -3
- package/dist/shared/types/components/modal.d.ts +5 -5
- package/dist/shared/types/components/panel.d.ts +7 -7
- package/dist/shared/types/components/progress-bar.d.ts +4 -4
- package/dist/shared/types/components/score.d.ts +13 -0
- package/dist/shared/types/components/score.js +1 -0
- package/dist/shared/types/components/selects.d.ts +11 -20
- package/dist/shared/types/components/statistics.d.ts +2 -2
- package/dist/shared/types/components/status-tag.d.ts +5 -5
- package/dist/shared/types/components/step-indicator.d.ts +5 -7
- package/dist/shared/types/components/table.d.ts +22 -12
- package/dist/shared/types/components/tabs.d.ts +10 -10
- package/dist/shared/types/components/tag.d.ts +2 -2
- package/dist/shared/types/components/text.d.ts +15 -21
- package/dist/shared/types/components/tile.d.ts +2 -2
- package/dist/shared/types/components/toggle.d.ts +12 -14
- package/dist/shared/types/components/toggleInputs.d.ts +26 -19
- package/dist/shared/types/components/tooltip.d.ts +1 -1
- package/dist/shared/types/crm.d.ts +52 -0
- package/dist/shared/types/http-requests.d.ts +2 -2
- package/dist/shared/types/index.d.ts +1 -1
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/shared.d.ts +128 -78
- package/dist/shared/types/shared.js +123 -78
- package/dist/shared/types/worker-globals.d.ts +11 -10
- package/dist/testing/__tests__/mocks.useAssociations.spec.js +92 -4
- package/dist/testing/__tests__/mocks.useCrmProperties.spec.js +55 -7
- package/dist/testing/internal/mocks/mock-hooks.js +4 -0
- package/package.json +4 -3
|
@@ -21,6 +21,16 @@ vi.mock('../../../crm/utils/fetchAssociations.ts', async (importOriginal) => {
|
|
|
21
21
|
});
|
|
22
22
|
// Get reference to the mocked function
|
|
23
23
|
const mockFetchAssociations = fetchAssociations;
|
|
24
|
+
// Shared test helpers
|
|
25
|
+
const createAssociation = (id) => ({
|
|
26
|
+
toObjectId: id,
|
|
27
|
+
associationTypes: [],
|
|
28
|
+
properties: {},
|
|
29
|
+
});
|
|
30
|
+
const mockResponse = (results, hasMore = false, nextOffset = 10) => ({
|
|
31
|
+
data: { results, hasMore, nextOffset },
|
|
32
|
+
cleanup: vi.fn(),
|
|
33
|
+
});
|
|
24
34
|
describe('useAssociations with Pagination', () => {
|
|
25
35
|
let originalError;
|
|
26
36
|
beforeAll(() => {
|
|
@@ -539,4 +549,90 @@ describe('useAssociations with Pagination', () => {
|
|
|
539
549
|
});
|
|
540
550
|
});
|
|
541
551
|
});
|
|
552
|
+
describe('refetch', () => {
|
|
553
|
+
it('should handle complete refetch lifecycle with loading states and data preservation', async () => {
|
|
554
|
+
const initialData = [createAssociation(1)];
|
|
555
|
+
const refetchedData = [createAssociation(2)];
|
|
556
|
+
let resolveRefetch;
|
|
557
|
+
const refetchPromise = new Promise((resolve) => {
|
|
558
|
+
resolveRefetch = resolve;
|
|
559
|
+
});
|
|
560
|
+
mockFetchAssociations
|
|
561
|
+
.mockResolvedValueOnce(mockResponse(initialData))
|
|
562
|
+
.mockImplementationOnce(() => refetchPromise);
|
|
563
|
+
const { result } = renderHook(() => useAssociations({ toObjectType: '0-1', pageLength: 10 }));
|
|
564
|
+
await waitFor(() => {
|
|
565
|
+
expect(result.current.results).toEqual(initialData);
|
|
566
|
+
expect(result.current.isLoading).toBe(false);
|
|
567
|
+
expect(result.current.isRefetching).toBe(false);
|
|
568
|
+
});
|
|
569
|
+
const refetchCall = result.current.refetch();
|
|
570
|
+
await waitFor(() => expect(result.current.isRefetching).toBe(true));
|
|
571
|
+
expect(result.current.results).toEqual(initialData);
|
|
572
|
+
resolveRefetch(mockResponse(refetchedData, true, 20));
|
|
573
|
+
await refetchCall;
|
|
574
|
+
await waitFor(() => {
|
|
575
|
+
expect(result.current.isRefetching).toBe(false);
|
|
576
|
+
expect(result.current.results).toEqual(refetchedData);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
it('should handle errors during refetch and allow recovery', async () => {
|
|
580
|
+
const initialData = [createAssociation(1)];
|
|
581
|
+
const recoveredData = [createAssociation(2)];
|
|
582
|
+
mockFetchAssociations
|
|
583
|
+
.mockResolvedValueOnce(mockResponse(initialData))
|
|
584
|
+
.mockRejectedValueOnce(new Error('Failed to refetch associations'))
|
|
585
|
+
.mockResolvedValueOnce(mockResponse(recoveredData));
|
|
586
|
+
const { result } = renderHook(() => useAssociations({ toObjectType: '0-1', pageLength: 10 }));
|
|
587
|
+
await waitFor(() => expect(result.current.results).toEqual(initialData));
|
|
588
|
+
await result.current.refetch();
|
|
589
|
+
await waitFor(() => {
|
|
590
|
+
expect(result.current.error?.message).toBe('Failed to refetch associations');
|
|
591
|
+
expect(result.current.results).toEqual(initialData);
|
|
592
|
+
});
|
|
593
|
+
await result.current.refetch();
|
|
594
|
+
await waitFor(() => {
|
|
595
|
+
expect(result.current.error).toBeNull();
|
|
596
|
+
expect(result.current.results).toEqual(recoveredData);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
it('should cancel in-flight refetch when called again', async () => {
|
|
600
|
+
const initialData = [createAssociation(1)];
|
|
601
|
+
const firstRefetchCleanup = vi.fn();
|
|
602
|
+
const secondRefetchCleanup = vi.fn();
|
|
603
|
+
mockFetchAssociations
|
|
604
|
+
.mockResolvedValueOnce(mockResponse(initialData))
|
|
605
|
+
.mockResolvedValueOnce({
|
|
606
|
+
...mockResponse([createAssociation(2)], false, 20),
|
|
607
|
+
cleanup: firstRefetchCleanup,
|
|
608
|
+
})
|
|
609
|
+
.mockResolvedValueOnce({
|
|
610
|
+
...mockResponse([createAssociation(3)], false, 30),
|
|
611
|
+
cleanup: secondRefetchCleanup,
|
|
612
|
+
});
|
|
613
|
+
const { result } = renderHook(() => useAssociations({ toObjectType: '0-1', pageLength: 10 }));
|
|
614
|
+
await waitFor(() => expect(result.current.results).toEqual(initialData));
|
|
615
|
+
await Promise.all([result.current.refetch(), result.current.refetch()]);
|
|
616
|
+
await waitFor(() => expect(result.current.results).toEqual([createAssociation(3)]));
|
|
617
|
+
expect(firstRefetchCleanup).toHaveBeenCalledTimes(1);
|
|
618
|
+
expect(secondRefetchCleanup).not.toHaveBeenCalled();
|
|
619
|
+
});
|
|
620
|
+
it('should maintain current page and pagination state during refetch', async () => {
|
|
621
|
+
mockFetchAssociations
|
|
622
|
+
.mockResolvedValueOnce(mockResponse([createAssociation(1)], true, 10))
|
|
623
|
+
.mockResolvedValueOnce(mockResponse([createAssociation(2)], true, 20))
|
|
624
|
+
.mockResolvedValueOnce(mockResponse([createAssociation(2)], true, 20));
|
|
625
|
+
const { result } = renderHook(() => useAssociations({ toObjectType: '0-1', pageLength: 10 }));
|
|
626
|
+
await waitFor(() => expect(result.current.pagination.currentPage).toBe(1));
|
|
627
|
+
result.current.pagination.nextPage();
|
|
628
|
+
await waitFor(() => expect(result.current.pagination.currentPage).toBe(2));
|
|
629
|
+
await result.current.refetch();
|
|
630
|
+
await waitFor(() => {
|
|
631
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
632
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
633
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
634
|
+
});
|
|
635
|
+
expect(mockFetchAssociations).toHaveBeenLastCalledWith(expect.objectContaining({ offset: 10 }), expect.any(Object));
|
|
636
|
+
});
|
|
637
|
+
});
|
|
542
638
|
});
|
|
@@ -21,7 +21,9 @@ describe('useCrmProperties', () => {
|
|
|
21
21
|
// Suppress React act() warning coming from @testing-library/react
|
|
22
22
|
originalError = console.error;
|
|
23
23
|
console.error = (...args) => {
|
|
24
|
-
if (args[0]
|
|
24
|
+
if (typeof args[0] === 'string' &&
|
|
25
|
+
(args[0].includes('ReactDOMTestUtils.act') ||
|
|
26
|
+
args[0].includes('was not wrapped in act')))
|
|
25
27
|
return;
|
|
26
28
|
originalError.call(console, ...args);
|
|
27
29
|
};
|
|
@@ -254,4 +256,171 @@ describe('useCrmProperties', () => {
|
|
|
254
256
|
});
|
|
255
257
|
expect(mockFetchCrmProperties).toHaveBeenCalledWith(2, expect.any(Function), {});
|
|
256
258
|
});
|
|
259
|
+
describe('refetch', () => {
|
|
260
|
+
it('should successfully refetch and update properties', async () => {
|
|
261
|
+
const initialData = { firstname: 'John', lastname: 'Doe' };
|
|
262
|
+
const refetchedData = { firstname: 'Jane', lastname: 'Smith' };
|
|
263
|
+
mockFetchCrmProperties
|
|
264
|
+
.mockResolvedValueOnce({
|
|
265
|
+
data: initialData,
|
|
266
|
+
cleanup: vi.fn(),
|
|
267
|
+
})
|
|
268
|
+
.mockResolvedValueOnce({
|
|
269
|
+
data: refetchedData,
|
|
270
|
+
cleanup: vi.fn(),
|
|
271
|
+
});
|
|
272
|
+
const propertyNames = ['firstname', 'lastname'];
|
|
273
|
+
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(result.current.properties).toEqual(initialData);
|
|
276
|
+
expect(result.current.isLoading).toBe(false);
|
|
277
|
+
expect(result.current.isRefetching).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
// Call refetch and wait for it to complete
|
|
280
|
+
await result.current.refetch();
|
|
281
|
+
// Wait for state to update with refetched data
|
|
282
|
+
await waitFor(() => {
|
|
283
|
+
expect(result.current.properties).toEqual(refetchedData);
|
|
284
|
+
expect(result.current.isRefetching).toBe(false);
|
|
285
|
+
expect(result.current.error).toBeNull();
|
|
286
|
+
});
|
|
287
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(2);
|
|
288
|
+
});
|
|
289
|
+
it('should set isRefetching to true during refetch', async () => {
|
|
290
|
+
const initialData = { firstname: 'John', lastname: 'Doe' };
|
|
291
|
+
const refetchedData = { firstname: 'Jane', lastname: 'Smith' };
|
|
292
|
+
let resolveRefetch;
|
|
293
|
+
const refetchPromise = new Promise((resolve) => {
|
|
294
|
+
resolveRefetch = resolve;
|
|
295
|
+
});
|
|
296
|
+
mockFetchCrmProperties
|
|
297
|
+
.mockResolvedValueOnce({
|
|
298
|
+
data: initialData,
|
|
299
|
+
cleanup: vi.fn(),
|
|
300
|
+
})
|
|
301
|
+
.mockImplementationOnce(() => refetchPromise);
|
|
302
|
+
const propertyNames = ['firstname', 'lastname'];
|
|
303
|
+
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
304
|
+
await waitFor(() => {
|
|
305
|
+
expect(result.current.properties).toEqual(initialData);
|
|
306
|
+
expect(result.current.isLoading).toBe(false);
|
|
307
|
+
expect(result.current.isRefetching).toBe(false);
|
|
308
|
+
});
|
|
309
|
+
// Call refetch but don't await it
|
|
310
|
+
const refetchCall = result.current.refetch();
|
|
311
|
+
// Check that isRefetching is true immediately after calling refetch
|
|
312
|
+
await waitFor(() => {
|
|
313
|
+
expect(result.current.isRefetching).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
// Verify properties haven't changed yet
|
|
316
|
+
expect(result.current.properties).toEqual(initialData);
|
|
317
|
+
expect(result.current.error).toBeNull();
|
|
318
|
+
// Resolve the refetch promise
|
|
319
|
+
resolveRefetch({
|
|
320
|
+
data: refetchedData,
|
|
321
|
+
cleanup: vi.fn(),
|
|
322
|
+
});
|
|
323
|
+
// Wait for refetch to complete
|
|
324
|
+
await refetchCall;
|
|
325
|
+
// Verify isRefetching is false and properties are updated
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(result.current.isRefetching).toBe(false);
|
|
328
|
+
expect(result.current.properties).toEqual(refetchedData);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
it('should handle refetch errors correctly', async () => {
|
|
332
|
+
const initialData = { firstname: 'John', lastname: 'Doe' };
|
|
333
|
+
const errorMessage = 'Failed to refetch CRM properties';
|
|
334
|
+
mockFetchCrmProperties
|
|
335
|
+
.mockResolvedValueOnce({
|
|
336
|
+
data: initialData,
|
|
337
|
+
cleanup: vi.fn(),
|
|
338
|
+
})
|
|
339
|
+
.mockRejectedValueOnce(new Error(errorMessage));
|
|
340
|
+
const propertyNames = ['firstname', 'lastname'];
|
|
341
|
+
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(result.current.properties).toEqual(initialData);
|
|
344
|
+
expect(result.current.isLoading).toBe(false);
|
|
345
|
+
expect(result.current.error).toBeNull();
|
|
346
|
+
});
|
|
347
|
+
// Call refetch and wait for it to complete (will throw error)
|
|
348
|
+
await result.current.refetch();
|
|
349
|
+
// Wait for error state to update
|
|
350
|
+
await waitFor(() => {
|
|
351
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
352
|
+
expect(result.current.error?.message).toBe(errorMessage);
|
|
353
|
+
expect(result.current.properties).toEqual(initialData);
|
|
354
|
+
expect(result.current.isRefetching).toBe(false);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
it('should clear error on successful refetch', async () => {
|
|
358
|
+
const errorMessage = 'Failed to fetch CRM properties';
|
|
359
|
+
const refetchedData = { firstname: 'Jane', lastname: 'Smith' };
|
|
360
|
+
mockFetchCrmProperties
|
|
361
|
+
.mockRejectedValueOnce(new Error(errorMessage))
|
|
362
|
+
.mockResolvedValueOnce({
|
|
363
|
+
data: refetchedData,
|
|
364
|
+
cleanup: vi.fn(),
|
|
365
|
+
});
|
|
366
|
+
const propertyNames = ['firstname', 'lastname'];
|
|
367
|
+
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
368
|
+
await waitFor(() => {
|
|
369
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
370
|
+
expect(result.current.error?.message).toBe(errorMessage);
|
|
371
|
+
expect(result.current.isLoading).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
// Call refetch and wait for it to complete
|
|
374
|
+
await result.current.refetch();
|
|
375
|
+
// Wait for error to clear and data to update
|
|
376
|
+
await waitFor(() => {
|
|
377
|
+
expect(result.current.error).toBeNull();
|
|
378
|
+
expect(result.current.properties).toEqual(refetchedData);
|
|
379
|
+
expect(result.current.isRefetching).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
it('should handle multiple rapid refetch calls correctly', async () => {
|
|
383
|
+
const initialData = { firstname: 'John', lastname: 'Doe' };
|
|
384
|
+
const firstRefetchData = { firstname: 'First', lastname: 'Refetch' };
|
|
385
|
+
const secondRefetchData = { firstname: 'Second', lastname: 'Refetch' };
|
|
386
|
+
const initialCleanup = vi.fn();
|
|
387
|
+
const firstRefetchCleanup = vi.fn();
|
|
388
|
+
const secondRefetchCleanup = vi.fn();
|
|
389
|
+
mockFetchCrmProperties
|
|
390
|
+
.mockResolvedValueOnce({
|
|
391
|
+
data: initialData,
|
|
392
|
+
cleanup: initialCleanup,
|
|
393
|
+
})
|
|
394
|
+
.mockResolvedValueOnce({
|
|
395
|
+
data: firstRefetchData,
|
|
396
|
+
cleanup: firstRefetchCleanup,
|
|
397
|
+
})
|
|
398
|
+
.mockResolvedValueOnce({
|
|
399
|
+
data: secondRefetchData,
|
|
400
|
+
cleanup: secondRefetchCleanup,
|
|
401
|
+
});
|
|
402
|
+
const propertyNames = ['firstname', 'lastname'];
|
|
403
|
+
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
404
|
+
// Wait for initial fetch to complete
|
|
405
|
+
await waitFor(() => {
|
|
406
|
+
expect(result.current.properties).toEqual(initialData);
|
|
407
|
+
expect(result.current.isLoading).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
// Call refetch twice rapidly (concurrently) - the second should cancel the first
|
|
410
|
+
const firstRefetch = result.current.refetch();
|
|
411
|
+
const secondRefetch = result.current.refetch();
|
|
412
|
+
// Wait for both to complete
|
|
413
|
+
await Promise.all([firstRefetch, secondRefetch]);
|
|
414
|
+
// Verify the final refetch's data is in state
|
|
415
|
+
await waitFor(() => {
|
|
416
|
+
expect(result.current.properties).toEqual(secondRefetchData);
|
|
417
|
+
expect(result.current.isRefetching).toBe(false);
|
|
418
|
+
expect(result.current.error).toBeNull();
|
|
419
|
+
});
|
|
420
|
+
// Verify cancellation behavior: first refetch was cancelled and cleaned up
|
|
421
|
+
expect(firstRefetchCleanup).toHaveBeenCalledTimes(1);
|
|
422
|
+
// Second refetch's cleanup should NOT be called yet (still active subscription)
|
|
423
|
+
expect(secondRefetchCleanup).not.toHaveBeenCalled();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
257
426
|
});
|
|
@@ -17,7 +17,9 @@ export interface UseAssociationsResult {
|
|
|
17
17
|
results: AssociationResult[];
|
|
18
18
|
error: Error | null;
|
|
19
19
|
isLoading: boolean;
|
|
20
|
+
isRefetching: boolean;
|
|
20
21
|
pagination: UseAssociationsPagination;
|
|
22
|
+
refetch: () => Promise<void>;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* A hook to fetch and manage associations between CRM objects with pagination support.
|
|
@@ -6,6 +6,7 @@ function createInitialState(pageSize) {
|
|
|
6
6
|
results: [],
|
|
7
7
|
error: null,
|
|
8
8
|
isLoading: true,
|
|
9
|
+
isRefetching: false,
|
|
9
10
|
currentPage: 1,
|
|
10
11
|
pageSize,
|
|
11
12
|
hasMore: false,
|
|
@@ -75,6 +76,27 @@ function associationsReducer(state, action) {
|
|
|
75
76
|
nextOffset: undefined,
|
|
76
77
|
offsetHistory: [],
|
|
77
78
|
};
|
|
79
|
+
case 'REFETCH_START':
|
|
80
|
+
return {
|
|
81
|
+
...state,
|
|
82
|
+
isRefetching: true,
|
|
83
|
+
error: null,
|
|
84
|
+
};
|
|
85
|
+
case 'REFETCH_SUCCESS':
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
isRefetching: false,
|
|
89
|
+
results: action.payload.results,
|
|
90
|
+
hasMore: action.payload.hasMore,
|
|
91
|
+
nextOffset: action.payload.nextOffset,
|
|
92
|
+
error: null,
|
|
93
|
+
};
|
|
94
|
+
case 'REFETCH_ERROR':
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
isRefetching: false,
|
|
98
|
+
error: action.payload,
|
|
99
|
+
};
|
|
78
100
|
default:
|
|
79
101
|
return state;
|
|
80
102
|
}
|
|
@@ -97,6 +119,10 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
|
97
119
|
const lastConfigKeyRef = useRef();
|
|
98
120
|
const lastOptionsRef = useRef();
|
|
99
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);
|
|
100
126
|
const stableConfig = useMemo(() => {
|
|
101
127
|
const configKey = JSON.stringify(config);
|
|
102
128
|
if (configKey === lastConfigKeyRef.current) {
|
|
@@ -178,6 +204,11 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
|
178
204
|
if (cleanup) {
|
|
179
205
|
cleanup();
|
|
180
206
|
}
|
|
207
|
+
// Clean up any active refetch subscription
|
|
208
|
+
if (refetchCleanupRef.current) {
|
|
209
|
+
refetchCleanupRef.current();
|
|
210
|
+
refetchCleanupRef.current = null;
|
|
211
|
+
}
|
|
181
212
|
};
|
|
182
213
|
}, [
|
|
183
214
|
stableConfig,
|
|
@@ -186,12 +217,68 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
|
|
|
186
217
|
state.currentOffset,
|
|
187
218
|
pageSize,
|
|
188
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;
|
|
227
|
+
}
|
|
228
|
+
const abortSignal = { cancelled: false };
|
|
229
|
+
refetchAbortRef.current = abortSignal;
|
|
230
|
+
try {
|
|
231
|
+
dispatch({ type: 'REFETCH_START' });
|
|
232
|
+
// Build request using current offset token to refetch current page
|
|
233
|
+
const request = {
|
|
234
|
+
toObjectType: stableConfig?.toObjectType,
|
|
235
|
+
properties: stableConfig?.properties,
|
|
236
|
+
pageLength: pageSize,
|
|
237
|
+
offset: state.currentOffset,
|
|
238
|
+
};
|
|
239
|
+
const result = await fetchAssociations(request, {
|
|
240
|
+
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
241
|
+
formattingOptions: stableOptions.formattingOptions,
|
|
242
|
+
});
|
|
243
|
+
if (!abortSignal.cancelled) {
|
|
244
|
+
dispatch({
|
|
245
|
+
type: 'REFETCH_SUCCESS',
|
|
246
|
+
payload: {
|
|
247
|
+
results: result.data.results,
|
|
248
|
+
hasMore: result.data.hasMore,
|
|
249
|
+
nextOffset: result.data.nextOffset,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
refetchCleanupRef.current = result.cleanup;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
if (result.cleanup) {
|
|
256
|
+
result.cleanup();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
if (!abortSignal.cancelled) {
|
|
262
|
+
const errorData = err instanceof Error
|
|
263
|
+
? err
|
|
264
|
+
: new Error('Failed to refetch associations');
|
|
265
|
+
dispatch({ type: 'REFETCH_ERROR', payload: errorData });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
if (refetchAbortRef.current === abortSignal) {
|
|
270
|
+
refetchAbortRef.current = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}, [stableConfig, stableOptions, state.currentOffset, pageSize]);
|
|
189
274
|
// Calculate pagination flags
|
|
190
275
|
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
191
276
|
return {
|
|
192
277
|
results: state.results,
|
|
193
278
|
error: state.error,
|
|
194
279
|
isLoading: state.isLoading,
|
|
280
|
+
isRefetching: state.isRefetching,
|
|
281
|
+
refetch,
|
|
195
282
|
pagination: {
|
|
196
283
|
hasNextPage: paginationFlags.hasNextPage,
|
|
197
284
|
hasPreviousPage: paginationFlags.hasPreviousPage,
|
|
@@ -3,10 +3,14 @@ export interface CrmPropertiesState {
|
|
|
3
3
|
properties: Record<string, string | null>;
|
|
4
4
|
error: Error | null;
|
|
5
5
|
isLoading: boolean;
|
|
6
|
+
isRefetching: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface UseCrmPropertiesResult extends CrmPropertiesState {
|
|
9
|
+
refetch: () => Promise<void>;
|
|
6
10
|
}
|
|
7
11
|
/**
|
|
8
12
|
* A hook for using and managing CRM properties.
|
|
9
13
|
*/
|
|
10
|
-
declare function useCrmPropertiesInternal(propertyNames: string[], options?: FetchCrmPropertiesOptions):
|
|
14
|
+
declare function useCrmPropertiesInternal(propertyNames: string[], options?: FetchCrmPropertiesOptions): UseCrmPropertiesResult;
|
|
11
15
|
export declare const useCrmProperties: typeof useCrmPropertiesInternal;
|
|
12
16
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useEffect, useReducer, useMemo, useRef } from 'react';
|
|
1
|
+
import { useEffect, useReducer, useMemo, useRef, useCallback } from 'react';
|
|
2
2
|
import { fetchCrmProperties, } from "../utils/fetchCrmProperties.js";
|
|
3
3
|
import { createMockAwareHook } from "../../internal/hook-utils.js";
|
|
4
4
|
const initialState = {
|
|
5
5
|
properties: {},
|
|
6
6
|
error: null,
|
|
7
7
|
isLoading: true,
|
|
8
|
+
isRefetching: false,
|
|
8
9
|
};
|
|
9
10
|
function crmPropertiesReducer(state, action) {
|
|
10
11
|
switch (action.type) {
|
|
@@ -28,6 +29,25 @@ function crmPropertiesReducer(state, action) {
|
|
|
28
29
|
error: action.payload,
|
|
29
30
|
properties: {},
|
|
30
31
|
};
|
|
32
|
+
case 'REFETCH_START':
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
isRefetching: true,
|
|
36
|
+
error: null,
|
|
37
|
+
};
|
|
38
|
+
case 'REFETCH_SUCCESS':
|
|
39
|
+
return {
|
|
40
|
+
...state,
|
|
41
|
+
isRefetching: false,
|
|
42
|
+
properties: action.payload,
|
|
43
|
+
error: null,
|
|
44
|
+
};
|
|
45
|
+
case 'REFETCH_ERROR':
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
isRefetching: false,
|
|
49
|
+
error: action.payload,
|
|
50
|
+
};
|
|
31
51
|
default:
|
|
32
52
|
return state;
|
|
33
53
|
}
|
|
@@ -49,6 +69,10 @@ function useCrmPropertiesInternal(propertyNames, options = DEFAULT_OPTIONS) {
|
|
|
49
69
|
const lastPropertyNamesKeyRef = useRef();
|
|
50
70
|
const lastOptionsRef = useRef();
|
|
51
71
|
const lastOptionsKeyRef = useRef();
|
|
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);
|
|
52
76
|
const stablePropertyNames = useMemo(() => {
|
|
53
77
|
if (!Array.isArray(propertyNames)) {
|
|
54
78
|
return propertyNames;
|
|
@@ -104,8 +128,63 @@ function useCrmPropertiesInternal(propertyNames, options = DEFAULT_OPTIONS) {
|
|
|
104
128
|
if (cleanup) {
|
|
105
129
|
cleanup();
|
|
106
130
|
}
|
|
131
|
+
// Clean up any active refetch subscription
|
|
132
|
+
if (refetchCleanupRef.current) {
|
|
133
|
+
refetchCleanupRef.current();
|
|
134
|
+
refetchCleanupRef.current = null;
|
|
135
|
+
}
|
|
107
136
|
};
|
|
108
137
|
}, [stablePropertyNames, stableOptions]);
|
|
109
|
-
|
|
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]);
|
|
185
|
+
return {
|
|
186
|
+
...state,
|
|
187
|
+
refetch,
|
|
188
|
+
};
|
|
110
189
|
}
|
|
111
190
|
export const useCrmProperties = createMockAwareHook('useCrmProperties', useCrmPropertiesInternal);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|