@hubspot/ui-extensions 0.9.4 → 0.9.5
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__/experimental/crm/fetchAssociations.spec.js +47 -1
- package/dist/__tests__/experimental/hooks/useAssociations.spec.js +231 -180
- package/dist/coreComponents.d.ts +12 -0
- package/dist/coreComponents.js +8 -0
- package/dist/experimental/crm/fetchAssociations.d.ts +16 -0
- package/dist/experimental/crm/fetchAssociations.js +22 -0
- package/dist/experimental/hooks/useAssociations.d.ts +18 -6
- package/dist/experimental/hooks/useAssociations.js +88 -28
- package/dist/experimental/index.d.ts +1 -9
- package/dist/experimental/index.js +1 -5
- package/dist/experimental/types.d.ts +0 -54
- package/dist/types.d.ts +53 -0
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@ Object.defineProperty(global, 'self', {
|
|
|
8
8
|
value: mockSelf,
|
|
9
9
|
writable: true,
|
|
10
10
|
});
|
|
11
|
-
import { fetchAssociations } from '../../../experimental/crm/fetchAssociations';
|
|
11
|
+
import { fetchAssociations, pageToOffset, offsetToPage, calculatePaginationFlags, } from '../../../experimental/crm/fetchAssociations';
|
|
12
12
|
describe('fetchAssociations', () => {
|
|
13
13
|
// Helper functions
|
|
14
14
|
const createMockResponse = (data, overrides = {}) => ({
|
|
@@ -393,4 +393,50 @@ describe('fetchAssociations', () => {
|
|
|
393
393
|
await expect(fetchAssociations(request, options)).rejects.toThrow('Invalid response format');
|
|
394
394
|
});
|
|
395
395
|
});
|
|
396
|
+
describe('pagination utilities', () => {
|
|
397
|
+
describe('pageToOffset', () => {
|
|
398
|
+
it('should convert page numbers to offsets', () => {
|
|
399
|
+
expect(pageToOffset(1, 10)).toBe(0);
|
|
400
|
+
expect(pageToOffset(3, 10)).toBe(20);
|
|
401
|
+
expect(pageToOffset(2, 25)).toBe(25);
|
|
402
|
+
});
|
|
403
|
+
it('should always return 0 for page 1', () => {
|
|
404
|
+
expect(pageToOffset(1, 10)).toBe(0);
|
|
405
|
+
expect(pageToOffset(1, 50)).toBe(0);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
describe('offsetToPage', () => {
|
|
409
|
+
it('should convert offsets to page numbers', () => {
|
|
410
|
+
expect(offsetToPage(0, 10)).toBe(1);
|
|
411
|
+
expect(offsetToPage(20, 10)).toBe(3);
|
|
412
|
+
expect(offsetToPage(15, 10)).toBe(2); // partial page
|
|
413
|
+
});
|
|
414
|
+
it('should always return 1 for offset 0', () => {
|
|
415
|
+
expect(offsetToPage(0, 10)).toBe(1);
|
|
416
|
+
expect(offsetToPage(0, 25)).toBe(1);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
describe('calculatePaginationFlags', () => {
|
|
420
|
+
it('should calculate pagination flags correctly', () => {
|
|
421
|
+
// Page 1 scenarios
|
|
422
|
+
expect(calculatePaginationFlags(1, true)).toEqual({
|
|
423
|
+
hasNextPage: true,
|
|
424
|
+
hasPreviousPage: false,
|
|
425
|
+
});
|
|
426
|
+
expect(calculatePaginationFlags(1, false)).toEqual({
|
|
427
|
+
hasNextPage: false,
|
|
428
|
+
hasPreviousPage: false,
|
|
429
|
+
});
|
|
430
|
+
// Page 2+ scenarios
|
|
431
|
+
expect(calculatePaginationFlags(2, true)).toEqual({
|
|
432
|
+
hasNextPage: true,
|
|
433
|
+
hasPreviousPage: true,
|
|
434
|
+
});
|
|
435
|
+
expect(calculatePaginationFlags(2, false)).toEqual({
|
|
436
|
+
hasNextPage: false,
|
|
437
|
+
hasPreviousPage: true,
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|
|
396
442
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
2
|
import { useAssociations } from '../../../experimental/hooks/useAssociations';
|
|
3
|
+
import { fetchAssociations } from '../../../experimental/crm/fetchAssociations';
|
|
3
4
|
// Mock the logger module
|
|
4
5
|
jest.mock('../../../logger', () => ({
|
|
5
6
|
logger: {
|
|
@@ -9,18 +10,22 @@ jest.mock('../../../logger', () => ({
|
|
|
9
10
|
error: jest.fn(),
|
|
10
11
|
},
|
|
11
12
|
}));
|
|
12
|
-
// Mock the fetchAssociations
|
|
13
|
-
jest.mock('../../../experimental/crm/fetchAssociations')
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
// Mock the fetchAssociations function, keep utility functions
|
|
14
|
+
jest.mock('../../../experimental/crm/fetchAssociations', () => ({
|
|
15
|
+
...jest.requireActual('../../../experimental/crm/fetchAssociations'),
|
|
16
|
+
fetchAssociations: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
// Get reference to the mocked function
|
|
19
|
+
const mockFetchAssociations = fetchAssociations;
|
|
20
|
+
describe('useAssociations with Pagination', () => {
|
|
18
21
|
let originalError;
|
|
19
22
|
beforeAll(() => {
|
|
20
|
-
// Suppress React act()
|
|
23
|
+
// Suppress React act() warnings coming from @testing-library/react
|
|
21
24
|
originalError = console.error;
|
|
22
25
|
console.error = (...args) => {
|
|
23
|
-
if (args[0]
|
|
26
|
+
if (typeof args[0] === 'string' &&
|
|
27
|
+
(args[0].includes('ReactDOMTestUtils.act') ||
|
|
28
|
+
args[0].includes('was not wrapped in act')))
|
|
24
29
|
return;
|
|
25
30
|
originalError.call(console, ...args);
|
|
26
31
|
};
|
|
@@ -33,24 +38,50 @@ describe('useAssociations', () => {
|
|
|
33
38
|
console.error = originalError;
|
|
34
39
|
});
|
|
35
40
|
describe('initial state', () => {
|
|
36
|
-
it('should
|
|
41
|
+
it('should initialize with proper pagination state', async () => {
|
|
42
|
+
mockFetchAssociations.mockResolvedValue({
|
|
43
|
+
data: {
|
|
44
|
+
results: [],
|
|
45
|
+
hasMore: false,
|
|
46
|
+
nextOffset: 0,
|
|
47
|
+
},
|
|
48
|
+
cleanup: jest.fn(),
|
|
49
|
+
});
|
|
37
50
|
const { result } = renderHook(() => useAssociations({
|
|
38
51
|
toObjectType: '0-1',
|
|
39
52
|
properties: ['firstname', 'lastname'],
|
|
40
|
-
pageLength:
|
|
41
|
-
offset: 0,
|
|
53
|
+
pageLength: 10,
|
|
42
54
|
}));
|
|
43
55
|
await waitFor(() => {
|
|
44
56
|
expect(result.current.results).toEqual([]);
|
|
45
57
|
expect(result.current.error).toBeNull();
|
|
46
|
-
expect(result.current.isLoading).toBe(
|
|
47
|
-
expect(result.current.
|
|
48
|
-
expect(result.current.
|
|
58
|
+
expect(result.current.isLoading).toBe(false);
|
|
59
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
60
|
+
expect(result.current.pagination.pageSize).toBe(10);
|
|
61
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
62
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it('should use default page size when not specified', async () => {
|
|
66
|
+
mockFetchAssociations.mockResolvedValue({
|
|
67
|
+
data: {
|
|
68
|
+
results: [],
|
|
69
|
+
hasMore: false,
|
|
70
|
+
nextOffset: 0,
|
|
71
|
+
},
|
|
72
|
+
cleanup: jest.fn(),
|
|
73
|
+
});
|
|
74
|
+
const { result } = renderHook(() => useAssociations({
|
|
75
|
+
toObjectType: '0-1',
|
|
76
|
+
properties: ['firstname'],
|
|
77
|
+
}));
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(result.current.pagination.pageSize).toBe(10); // DEFAULT_PAGE_SIZE
|
|
49
80
|
});
|
|
50
81
|
});
|
|
51
82
|
});
|
|
52
|
-
describe('
|
|
53
|
-
it('should successfully fetch and return associations', async () => {
|
|
83
|
+
describe('data fetching', () => {
|
|
84
|
+
it('should successfully fetch and return associations with pagination info', async () => {
|
|
54
85
|
mockFetchAssociations.mockResolvedValue({
|
|
55
86
|
data: {
|
|
56
87
|
results: [
|
|
@@ -64,57 +95,31 @@ describe('useAssociations', () => {
|
|
|
64
95
|
lastname: 'Doe',
|
|
65
96
|
},
|
|
66
97
|
},
|
|
67
|
-
{
|
|
68
|
-
toObjectId: 1002,
|
|
69
|
-
associationTypes: [
|
|
70
|
-
{ category: 'USER_DEFINED', typeId: 100, label: 'Custom' },
|
|
71
|
-
],
|
|
72
|
-
properties: {
|
|
73
|
-
firstname: 'Jane',
|
|
74
|
-
lastname: 'Smith',
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
98
|
],
|
|
78
99
|
hasMore: true,
|
|
79
|
-
nextOffset:
|
|
100
|
+
nextOffset: 10,
|
|
80
101
|
},
|
|
81
102
|
cleanup: jest.fn(),
|
|
82
103
|
});
|
|
83
|
-
const
|
|
104
|
+
const { result } = renderHook(() => useAssociations({
|
|
84
105
|
toObjectType: '0-1',
|
|
85
106
|
properties: ['firstname', 'lastname'],
|
|
86
|
-
pageLength:
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
const { result } = renderHook(() => useAssociations(request));
|
|
107
|
+
pageLength: 10,
|
|
108
|
+
}));
|
|
90
109
|
await waitFor(() => {
|
|
91
|
-
expect(result.current.results).
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
],
|
|
97
|
-
properties: {
|
|
98
|
-
firstname: 'John',
|
|
99
|
-
lastname: 'Doe',
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
toObjectId: 1002,
|
|
104
|
-
associationTypes: [
|
|
105
|
-
{ category: 'USER_DEFINED', typeId: 100, label: 'Custom' },
|
|
106
|
-
],
|
|
107
|
-
properties: {
|
|
108
|
-
firstname: 'Jane',
|
|
109
|
-
lastname: 'Smith',
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
]);
|
|
113
|
-
expect(result.current.error).toBeNull();
|
|
110
|
+
expect(result.current.results).toHaveLength(1);
|
|
111
|
+
expect(result.current.results[0].toObjectId).toBe(1001);
|
|
112
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
113
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
114
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
114
115
|
expect(result.current.isLoading).toBe(false);
|
|
115
|
-
expect(result.current.hasMore).toBe(true);
|
|
116
|
-
expect(result.current.nextOffset).toBe(25);
|
|
117
116
|
});
|
|
117
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(expect.objectContaining({
|
|
118
|
+
toObjectType: '0-1',
|
|
119
|
+
properties: ['firstname', 'lastname'],
|
|
120
|
+
pageLength: 10,
|
|
121
|
+
offset: 0,
|
|
122
|
+
}), expect.any(Object));
|
|
118
123
|
});
|
|
119
124
|
it('should handle empty results correctly', async () => {
|
|
120
125
|
mockFetchAssociations.mockResolvedValue({
|
|
@@ -125,136 +130,186 @@ describe('useAssociations', () => {
|
|
|
125
130
|
},
|
|
126
131
|
cleanup: jest.fn(),
|
|
127
132
|
});
|
|
128
|
-
const
|
|
133
|
+
const { result } = renderHook(() => useAssociations({
|
|
129
134
|
toObjectType: '0-1',
|
|
130
135
|
properties: ['firstname'],
|
|
131
|
-
pageLength:
|
|
132
|
-
|
|
133
|
-
};
|
|
134
|
-
const { result } = renderHook(() => useAssociations(request));
|
|
136
|
+
pageLength: 5,
|
|
137
|
+
}));
|
|
135
138
|
await waitFor(() => {
|
|
136
139
|
expect(result.current.results).toEqual([]);
|
|
137
|
-
expect(result.current.
|
|
138
|
-
expect(result.current.
|
|
140
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
141
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
139
142
|
expect(result.current.isLoading).toBe(false);
|
|
140
143
|
expect(result.current.error).toBeNull();
|
|
141
144
|
});
|
|
142
145
|
});
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
});
|
|
147
|
+
describe('pagination actions', () => {
|
|
148
|
+
it('should navigate to next page correctly', async () => {
|
|
149
|
+
// First page response
|
|
150
|
+
mockFetchAssociations.mockResolvedValueOnce({
|
|
145
151
|
data: {
|
|
146
|
-
results: [
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
properties: {
|
|
157
|
-
name: 'Test Company',
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
],
|
|
152
|
+
results: [{ toObjectId: 1, associationTypes: [], properties: {} }],
|
|
153
|
+
hasMore: true,
|
|
154
|
+
nextOffset: 10,
|
|
155
|
+
},
|
|
156
|
+
cleanup: jest.fn(),
|
|
157
|
+
});
|
|
158
|
+
// Second page response
|
|
159
|
+
mockFetchAssociations.mockResolvedValueOnce({
|
|
160
|
+
data: {
|
|
161
|
+
results: [{ toObjectId: 2, associationTypes: [], properties: {} }],
|
|
161
162
|
hasMore: false,
|
|
162
|
-
nextOffset:
|
|
163
|
+
nextOffset: 20,
|
|
163
164
|
},
|
|
164
165
|
cleanup: jest.fn(),
|
|
165
166
|
});
|
|
166
|
-
const
|
|
167
|
-
toObjectType: '0-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
};
|
|
172
|
-
const { result } = renderHook(() => useAssociations(request));
|
|
167
|
+
const { result } = renderHook(() => useAssociations({
|
|
168
|
+
toObjectType: '0-1',
|
|
169
|
+
pageLength: 10,
|
|
170
|
+
}));
|
|
171
|
+
// Wait for initial load
|
|
173
172
|
await waitFor(() => {
|
|
174
|
-
expect(result.current.
|
|
175
|
-
|
|
176
|
-
toObjectId: 2001,
|
|
177
|
-
associationTypes: [
|
|
178
|
-
{
|
|
179
|
-
category: 'HUBSPOT_DEFINED',
|
|
180
|
-
typeId: 2,
|
|
181
|
-
label: 'Company Primary',
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
-
properties: {
|
|
185
|
-
name: 'Test Company',
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
]);
|
|
189
|
-
expect(result.current.isLoading).toBe(false);
|
|
173
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
174
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
190
175
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
176
|
+
// Navigate to next page
|
|
177
|
+
result.current.pagination.nextPage();
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
180
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
181
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
// Verify API was called with correct offset for page 2
|
|
184
|
+
expect(mockFetchAssociations).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
185
|
+
offset: 10, // (page 2 - 1) * pageSize 10 = 10
|
|
186
|
+
}), expect.any(Object));
|
|
194
187
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
188
|
+
it('should navigate to previous page correctly', async () => {
|
|
189
|
+
// First call: page 1 with hasMore: true (allows next page)
|
|
190
|
+
mockFetchAssociations.mockResolvedValueOnce({
|
|
191
|
+
data: {
|
|
192
|
+
results: [{ toObjectId: 1, associationTypes: [], properties: {} }],
|
|
193
|
+
hasMore: true,
|
|
194
|
+
nextOffset: 10,
|
|
195
|
+
},
|
|
196
|
+
cleanup: jest.fn(),
|
|
197
|
+
});
|
|
198
|
+
// Second call: page 2 with hasMore: false
|
|
199
|
+
mockFetchAssociations.mockResolvedValueOnce({
|
|
200
|
+
data: {
|
|
201
|
+
results: [{ toObjectId: 2, associationTypes: [], properties: {} }],
|
|
202
|
+
hasMore: false,
|
|
203
|
+
nextOffset: 20,
|
|
204
|
+
},
|
|
205
|
+
cleanup: jest.fn(),
|
|
206
|
+
});
|
|
207
|
+
// Third call: back to page 1
|
|
208
|
+
mockFetchAssociations.mockResolvedValueOnce({
|
|
209
|
+
data: {
|
|
210
|
+
results: [{ toObjectId: 1, associationTypes: [], properties: {} }],
|
|
211
|
+
hasMore: true,
|
|
212
|
+
nextOffset: 10,
|
|
213
|
+
},
|
|
214
|
+
cleanup: jest.fn(),
|
|
215
|
+
});
|
|
216
|
+
const { result } = renderHook(() => useAssociations({
|
|
201
217
|
toObjectType: '0-1',
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
206
|
-
const { result } = renderHook(() => useAssociations(request));
|
|
218
|
+
pageLength: 10,
|
|
219
|
+
}));
|
|
220
|
+
// Wait for initial load (page 1)
|
|
207
221
|
await waitFor(() => {
|
|
208
|
-
expect(result.current.
|
|
209
|
-
expect(result.current.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
222
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
223
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
// Go to page 2
|
|
226
|
+
result.current.pagination.nextPage();
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
229
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
// Go back to previous page
|
|
232
|
+
result.current.pagination.previousPage();
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
235
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
214
236
|
});
|
|
215
237
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
it('should handle pagination correctly', async () => {
|
|
238
|
+
it('should reset to first page correctly', async () => {
|
|
239
|
+
// Initial page 1 - has more pages
|
|
219
240
|
mockFetchAssociations.mockResolvedValue({
|
|
220
241
|
data: {
|
|
221
|
-
results: [
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
results: [],
|
|
243
|
+
hasMore: true,
|
|
244
|
+
nextOffset: 10,
|
|
245
|
+
},
|
|
246
|
+
cleanup: jest.fn(),
|
|
247
|
+
});
|
|
248
|
+
const { result } = renderHook(() => useAssociations({
|
|
249
|
+
toObjectType: '0-1',
|
|
250
|
+
pageLength: 10,
|
|
251
|
+
}));
|
|
252
|
+
// Wait for initial load
|
|
253
|
+
await waitFor(() => {
|
|
254
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
255
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
// Navigate to page 2, then page 3
|
|
258
|
+
result.current.pagination.nextPage();
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
261
|
+
});
|
|
262
|
+
result.current.pagination.nextPage();
|
|
263
|
+
await waitFor(() => {
|
|
264
|
+
expect(result.current.pagination.currentPage).toBe(3);
|
|
265
|
+
});
|
|
266
|
+
// Reset to first page
|
|
267
|
+
result.current.pagination.reset();
|
|
268
|
+
await waitFor(() => {
|
|
269
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
270
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
it('should not allow navigation beyond boundaries', async () => {
|
|
274
|
+
mockFetchAssociations.mockResolvedValue({
|
|
275
|
+
data: {
|
|
276
|
+
results: [],
|
|
232
277
|
hasMore: false,
|
|
233
|
-
nextOffset:
|
|
278
|
+
nextOffset: 10,
|
|
234
279
|
},
|
|
235
280
|
cleanup: jest.fn(),
|
|
236
281
|
});
|
|
237
|
-
const
|
|
282
|
+
const { result } = renderHook(() => useAssociations({
|
|
283
|
+
toObjectType: '0-1',
|
|
284
|
+
pageLength: 10,
|
|
285
|
+
}));
|
|
286
|
+
await waitFor(() => {
|
|
287
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
288
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
// Try to go to next page when there isn't one
|
|
291
|
+
result.current.pagination.nextPage();
|
|
292
|
+
// Should stay on page 1
|
|
293
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
294
|
+
// Try to go to previous page from page 1
|
|
295
|
+
result.current.pagination.previousPage();
|
|
296
|
+
// Should stay on page 1
|
|
297
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
describe('error handling', () => {
|
|
301
|
+
it('should handle fetch errors correctly', async () => {
|
|
302
|
+
const errorMessage = 'Failed to fetch associations';
|
|
303
|
+
mockFetchAssociations.mockRejectedValue(new Error(errorMessage));
|
|
304
|
+
const { result } = renderHook(() => useAssociations({
|
|
238
305
|
toObjectType: '0-1',
|
|
239
306
|
properties: ['firstname'],
|
|
240
|
-
pageLength:
|
|
241
|
-
|
|
242
|
-
};
|
|
243
|
-
const { result } = renderHook(() => useAssociations(request));
|
|
307
|
+
pageLength: 10,
|
|
308
|
+
}));
|
|
244
309
|
await waitFor(() => {
|
|
245
|
-
expect(result.current.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
associationTypes: [
|
|
249
|
-
{ category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
|
|
250
|
-
],
|
|
251
|
-
properties: {
|
|
252
|
-
firstname: 'Page 2 User',
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
]);
|
|
256
|
-
expect(result.current.hasMore).toBe(false);
|
|
257
|
-
expect(result.current.nextOffset).toBe(50);
|
|
310
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
311
|
+
expect(result.current.error?.message).toBe(errorMessage);
|
|
312
|
+
expect(result.current.results).toEqual([]);
|
|
258
313
|
expect(result.current.isLoading).toBe(false);
|
|
259
314
|
});
|
|
260
315
|
});
|
|
@@ -269,11 +324,10 @@ describe('useAssociations', () => {
|
|
|
269
324
|
},
|
|
270
325
|
cleanup: jest.fn(),
|
|
271
326
|
});
|
|
272
|
-
const
|
|
327
|
+
const config = {
|
|
273
328
|
toObjectType: '0-1',
|
|
274
329
|
properties: ['firstname', 'lastname'],
|
|
275
|
-
pageLength:
|
|
276
|
-
offset: 0,
|
|
330
|
+
pageLength: 10,
|
|
277
331
|
};
|
|
278
332
|
const options = {
|
|
279
333
|
propertiesToFormat: ['firstname'],
|
|
@@ -284,9 +338,9 @@ describe('useAssociations', () => {
|
|
|
284
338
|
},
|
|
285
339
|
},
|
|
286
340
|
};
|
|
287
|
-
renderHook(() => useAssociations(
|
|
341
|
+
renderHook(() => useAssociations(config, options));
|
|
288
342
|
await waitFor(() => {
|
|
289
|
-
expect(mockFetchAssociations).toHaveBeenCalledWith(
|
|
343
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(expect.objectContaining(config), options);
|
|
290
344
|
});
|
|
291
345
|
});
|
|
292
346
|
it('should use default empty options when no options provided', async () => {
|
|
@@ -298,15 +352,14 @@ describe('useAssociations', () => {
|
|
|
298
352
|
},
|
|
299
353
|
cleanup: jest.fn(),
|
|
300
354
|
});
|
|
301
|
-
const
|
|
355
|
+
const config = {
|
|
302
356
|
toObjectType: '0-1',
|
|
303
357
|
properties: ['firstname'],
|
|
304
|
-
pageLength:
|
|
305
|
-
offset: 0,
|
|
358
|
+
pageLength: 10,
|
|
306
359
|
};
|
|
307
|
-
renderHook(() => useAssociations(
|
|
360
|
+
renderHook(() => useAssociations(config));
|
|
308
361
|
await waitFor(() => {
|
|
309
|
-
expect(mockFetchAssociations).toHaveBeenCalledWith(
|
|
362
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(expect.objectContaining(config), {});
|
|
310
363
|
});
|
|
311
364
|
});
|
|
312
365
|
});
|
|
@@ -321,13 +374,12 @@ describe('useAssociations', () => {
|
|
|
321
374
|
},
|
|
322
375
|
cleanup: mockCleanup,
|
|
323
376
|
});
|
|
324
|
-
const
|
|
377
|
+
const config = {
|
|
325
378
|
toObjectType: '0-1',
|
|
326
379
|
properties: ['firstname'],
|
|
327
|
-
pageLength:
|
|
328
|
-
offset: 0,
|
|
380
|
+
pageLength: 10,
|
|
329
381
|
};
|
|
330
|
-
const { unmount } = renderHook(() => useAssociations(
|
|
382
|
+
const { unmount } = renderHook(() => useAssociations(config));
|
|
331
383
|
await waitFor(() => {
|
|
332
384
|
expect(mockFetchAssociations).toHaveBeenCalled();
|
|
333
385
|
});
|
|
@@ -336,7 +388,7 @@ describe('useAssociations', () => {
|
|
|
336
388
|
expect(mockCleanup).toHaveBeenCalled();
|
|
337
389
|
});
|
|
338
390
|
});
|
|
339
|
-
it('should handle stable reference optimization for
|
|
391
|
+
it('should handle stable reference optimization for config', async () => {
|
|
340
392
|
mockFetchAssociations.mockResolvedValue({
|
|
341
393
|
data: {
|
|
342
394
|
results: [],
|
|
@@ -345,20 +397,19 @@ describe('useAssociations', () => {
|
|
|
345
397
|
},
|
|
346
398
|
cleanup: jest.fn(),
|
|
347
399
|
});
|
|
348
|
-
const
|
|
400
|
+
const config = {
|
|
349
401
|
toObjectType: '0-1',
|
|
350
402
|
properties: ['firstname'],
|
|
351
|
-
pageLength:
|
|
352
|
-
offset: 0,
|
|
403
|
+
pageLength: 10,
|
|
353
404
|
};
|
|
354
|
-
const { rerender } = renderHook(({
|
|
355
|
-
initialProps: {
|
|
405
|
+
const { rerender } = renderHook(({ configProp }) => useAssociations(configProp), {
|
|
406
|
+
initialProps: { configProp: config },
|
|
356
407
|
});
|
|
357
408
|
await waitFor(() => {
|
|
358
409
|
expect(mockFetchAssociations).toHaveBeenCalledTimes(1);
|
|
359
410
|
});
|
|
360
|
-
// Rerender with the same
|
|
361
|
-
rerender({
|
|
411
|
+
// Rerender with the same config object content but different reference
|
|
412
|
+
rerender({ configProp: { ...config } });
|
|
362
413
|
await waitFor(() => {
|
|
363
414
|
// Should not call fetchAssociations again due to stable reference optimization
|
|
364
415
|
expect(mockFetchAssociations).toHaveBeenCalledTimes(1);
|
package/dist/coreComponents.d.ts
CHANGED
|
@@ -794,3 +794,15 @@ export declare const SearchInput: "SearchInput" & {
|
|
|
794
794
|
readonly props?: types.SearchInputProps | undefined;
|
|
795
795
|
readonly children?: true | undefined;
|
|
796
796
|
} & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"SearchInput", types.SearchInputProps, true>>;
|
|
797
|
+
/**
|
|
798
|
+
* The `TimeInput` component renders an input field where a user can select a time. Commonly used within the `Form` component.
|
|
799
|
+
*
|
|
800
|
+
* **Links:**
|
|
801
|
+
*
|
|
802
|
+
* - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/time-input Docs}
|
|
803
|
+
*/
|
|
804
|
+
export declare const TimeInput: "TimeInput" & {
|
|
805
|
+
readonly type?: "TimeInput" | undefined;
|
|
806
|
+
readonly props?: types.TimeInputProps | undefined;
|
|
807
|
+
readonly children?: true | undefined;
|
|
808
|
+
} & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"TimeInput", types.TimeInputProps, true>>;
|
package/dist/coreComponents.js
CHANGED
|
@@ -534,3 +534,11 @@ export const Tooltip = createRemoteReactComponent('Tooltip');
|
|
|
534
534
|
* - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/search-input SearchInput Docs}
|
|
535
535
|
*/
|
|
536
536
|
export const SearchInput = createRemoteReactComponent('SearchInput');
|
|
537
|
+
/**
|
|
538
|
+
* The `TimeInput` component renders an input field where a user can select a time. Commonly used within the `Form` component.
|
|
539
|
+
*
|
|
540
|
+
* **Links:**
|
|
541
|
+
*
|
|
542
|
+
* - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/time-input Docs}
|
|
543
|
+
*/
|
|
544
|
+
export const TimeInput = createRemoteReactComponent('TimeInput');
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { FetchCrmPropertiesOptions } from './fetchCrmProperties';
|
|
2
|
+
export declare const DEFAULT_PAGE_SIZE = 10;
|
|
3
|
+
/**
|
|
4
|
+
* Convert page number and page size to offset for API calls
|
|
5
|
+
*/
|
|
6
|
+
export declare function pageToOffset(page: number, pageSize: number): number;
|
|
7
|
+
/**
|
|
8
|
+
* Convert offset and page size to page number
|
|
9
|
+
*/
|
|
10
|
+
export declare function offsetToPage(offset: number, pageSize: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Calculate pagination flags based on current page and API hasMore flag
|
|
13
|
+
*/
|
|
14
|
+
export declare function calculatePaginationFlags(currentPage: number, hasMore: boolean): {
|
|
15
|
+
hasNextPage: boolean;
|
|
16
|
+
hasPreviousPage: boolean;
|
|
17
|
+
};
|
|
2
18
|
export type AssociationResult = {
|
|
3
19
|
toObjectId: number;
|
|
4
20
|
associationTypes: {
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
export const DEFAULT_PAGE_SIZE = 10;
|
|
2
|
+
/**
|
|
3
|
+
* Convert page number and page size to offset for API calls
|
|
4
|
+
*/
|
|
5
|
+
export function pageToOffset(page, pageSize) {
|
|
6
|
+
return (page - 1) * pageSize;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Convert offset and page size to page number
|
|
10
|
+
*/
|
|
11
|
+
export function offsetToPage(offset, pageSize) {
|
|
12
|
+
return Math.floor(offset / pageSize) + 1;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Calculate pagination flags based on current page and API hasMore flag
|
|
16
|
+
*/
|
|
17
|
+
export function calculatePaginationFlags(currentPage, hasMore) {
|
|
18
|
+
return {
|
|
19
|
+
hasNextPage: hasMore,
|
|
20
|
+
hasPreviousPage: currentPage > 1,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
1
23
|
function isAssociationsResponse(data) {
|
|
2
24
|
if (data === null ||
|
|
3
25
|
typeof data !== 'object' ||
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { type FetchAssociationsRequest, type
|
|
1
|
+
import { type FetchAssociationsRequest, type AssociationResult } from '../crm/fetchAssociations';
|
|
2
2
|
import { type FetchCrmPropertiesOptions } from '../crm/fetchCrmProperties';
|
|
3
|
-
export interface
|
|
4
|
-
|
|
3
|
+
export interface UseAssociationsOptions {
|
|
4
|
+
propertiesToFormat?: 'all' | string[];
|
|
5
|
+
formattingOptions?: FetchCrmPropertiesOptions['formattingOptions'];
|
|
6
|
+
}
|
|
7
|
+
export interface UseAssociationsPagination {
|
|
8
|
+
hasNextPage: boolean;
|
|
9
|
+
hasPreviousPage: boolean;
|
|
10
|
+
currentPage: number;
|
|
11
|
+
pageSize: number;
|
|
12
|
+
nextPage: () => void;
|
|
13
|
+
previousPage: () => void;
|
|
14
|
+
reset: () => void;
|
|
15
|
+
}
|
|
16
|
+
export interface UseAssociationsResult {
|
|
17
|
+
results: AssociationResult[];
|
|
5
18
|
error: Error | null;
|
|
6
19
|
isLoading: boolean;
|
|
7
|
-
|
|
8
|
-
nextOffset: number;
|
|
20
|
+
pagination: UseAssociationsPagination;
|
|
9
21
|
}
|
|
10
|
-
export declare function useAssociations(
|
|
22
|
+
export declare function useAssociations(config: Omit<FetchAssociationsRequest, 'offset'>, options?: UseAssociationsOptions): UseAssociationsResult;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { useEffect, useReducer, useMemo, useRef } from 'react';
|
|
1
|
+
import { useEffect, useReducer, useMemo, useRef, useCallback } from 'react';
|
|
2
2
|
import { logger } from '../../logger';
|
|
3
|
-
import { fetchAssociations, } from '../crm/fetchAssociations';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import { fetchAssociations, DEFAULT_PAGE_SIZE, pageToOffset, calculatePaginationFlags, } from '../crm/fetchAssociations';
|
|
4
|
+
function createInitialState(pageSize) {
|
|
5
|
+
return {
|
|
6
|
+
results: [],
|
|
7
|
+
error: null,
|
|
8
|
+
isLoading: true,
|
|
9
|
+
currentPage: 1,
|
|
10
|
+
pageSize,
|
|
11
|
+
hasMore: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
11
14
|
function associationsReducer(state, action) {
|
|
12
15
|
switch (action.type) {
|
|
13
16
|
case 'FETCH_START':
|
|
@@ -22,7 +25,6 @@ function associationsReducer(state, action) {
|
|
|
22
25
|
isLoading: false,
|
|
23
26
|
results: action.payload.results,
|
|
24
27
|
hasMore: action.payload.hasMore,
|
|
25
|
-
nextOffset: action.payload.nextOffset,
|
|
26
28
|
error: null,
|
|
27
29
|
};
|
|
28
30
|
case 'FETCH_ERROR':
|
|
@@ -32,15 +34,33 @@ function associationsReducer(state, action) {
|
|
|
32
34
|
error: action.payload,
|
|
33
35
|
results: [],
|
|
34
36
|
hasMore: false,
|
|
35
|
-
|
|
37
|
+
};
|
|
38
|
+
case 'NEXT_PAGE':
|
|
39
|
+
return {
|
|
40
|
+
...state,
|
|
41
|
+
currentPage: state.currentPage + 1,
|
|
42
|
+
};
|
|
43
|
+
case 'PREVIOUS_PAGE':
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
currentPage: Math.max(1, state.currentPage - 1),
|
|
47
|
+
};
|
|
48
|
+
case 'RESET':
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
currentPage: 1,
|
|
52
|
+
results: [],
|
|
53
|
+
hasMore: false,
|
|
54
|
+
error: null,
|
|
36
55
|
};
|
|
37
56
|
default:
|
|
38
57
|
return state;
|
|
39
58
|
}
|
|
40
59
|
}
|
|
41
60
|
const DEFAULT_OPTIONS = {};
|
|
42
|
-
export function useAssociations(
|
|
43
|
-
const
|
|
61
|
+
export function useAssociations(config, options = DEFAULT_OPTIONS) {
|
|
62
|
+
const pageSize = config.pageLength ?? DEFAULT_PAGE_SIZE;
|
|
63
|
+
const [state, dispatch] = useReducer(associationsReducer, useMemo(() => createInitialState(pageSize), [pageSize]));
|
|
44
64
|
// Log experimental warning once on mount
|
|
45
65
|
useEffect(() => {
|
|
46
66
|
logger.warn('useAssociations is an experimental hook and might change or be removed in the future.');
|
|
@@ -48,23 +68,23 @@ export function useAssociations(request, options = DEFAULT_OPTIONS) {
|
|
|
48
68
|
/**
|
|
49
69
|
* HOOK OPTIMIZATION:
|
|
50
70
|
*
|
|
51
|
-
* Create stable references for
|
|
71
|
+
* Create stable references for config and options to prevent unnecessary re-renders and API calls.
|
|
52
72
|
* Then, external developers can pass inline objects without worrying about memoization
|
|
53
73
|
* We handle the deep equality comparison ourselves, and return the same object reference when content is equivalent.
|
|
54
74
|
*/
|
|
55
|
-
const
|
|
56
|
-
const
|
|
75
|
+
const lastConfigRef = useRef();
|
|
76
|
+
const lastConfigKeyRef = useRef();
|
|
57
77
|
const lastOptionsRef = useRef();
|
|
58
78
|
const lastOptionsKeyRef = useRef();
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
return
|
|
79
|
+
const stableConfig = useMemo(() => {
|
|
80
|
+
const configKey = JSON.stringify(config);
|
|
81
|
+
if (configKey === lastConfigKeyRef.current) {
|
|
82
|
+
return lastConfigRef.current;
|
|
63
83
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return
|
|
67
|
-
}, [
|
|
84
|
+
lastConfigKeyRef.current = configKey;
|
|
85
|
+
lastConfigRef.current = config;
|
|
86
|
+
return config;
|
|
87
|
+
}, [config]);
|
|
68
88
|
const stableOptions = useMemo(() => {
|
|
69
89
|
const optionsKey = JSON.stringify(options);
|
|
70
90
|
if (optionsKey === lastOptionsKeyRef.current) {
|
|
@@ -74,6 +94,22 @@ export function useAssociations(request, options = DEFAULT_OPTIONS) {
|
|
|
74
94
|
lastOptionsRef.current = options;
|
|
75
95
|
return options;
|
|
76
96
|
}, [options]);
|
|
97
|
+
// Pagination actions
|
|
98
|
+
const nextPage = useCallback(() => {
|
|
99
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
100
|
+
if (paginationFlags.hasNextPage) {
|
|
101
|
+
dispatch({ type: 'NEXT_PAGE' });
|
|
102
|
+
}
|
|
103
|
+
}, [state.currentPage, state.hasMore]);
|
|
104
|
+
const previousPage = useCallback(() => {
|
|
105
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
106
|
+
if (paginationFlags.hasPreviousPage) {
|
|
107
|
+
dispatch({ type: 'PREVIOUS_PAGE' });
|
|
108
|
+
}
|
|
109
|
+
}, [state.currentPage, state.hasMore]);
|
|
110
|
+
const reset = useCallback(() => {
|
|
111
|
+
dispatch({ type: 'RESET' });
|
|
112
|
+
}, []);
|
|
77
113
|
// Fetch the associations
|
|
78
114
|
useEffect(() => {
|
|
79
115
|
let cancelled = false;
|
|
@@ -81,14 +117,23 @@ export function useAssociations(request, options = DEFAULT_OPTIONS) {
|
|
|
81
117
|
const fetchData = async () => {
|
|
82
118
|
try {
|
|
83
119
|
dispatch({ type: 'FETCH_START' });
|
|
84
|
-
|
|
120
|
+
// Build request using current page and pagination utilities
|
|
121
|
+
const request = {
|
|
122
|
+
toObjectType: stableConfig.toObjectType,
|
|
123
|
+
properties: stableConfig.properties,
|
|
124
|
+
pageLength: pageSize,
|
|
125
|
+
offset: pageToOffset(state.currentPage, pageSize),
|
|
126
|
+
};
|
|
127
|
+
const result = await fetchAssociations(request, {
|
|
128
|
+
propertiesToFormat: stableOptions.propertiesToFormat,
|
|
129
|
+
formattingOptions: stableOptions.formattingOptions,
|
|
130
|
+
});
|
|
85
131
|
if (!cancelled) {
|
|
86
132
|
dispatch({
|
|
87
133
|
type: 'FETCH_SUCCESS',
|
|
88
134
|
payload: {
|
|
89
135
|
results: result.data.results,
|
|
90
136
|
hasMore: result.data.hasMore,
|
|
91
|
-
nextOffset: result.data.nextOffset,
|
|
92
137
|
},
|
|
93
138
|
});
|
|
94
139
|
cleanup = result.cleanup;
|
|
@@ -111,6 +156,21 @@ export function useAssociations(request, options = DEFAULT_OPTIONS) {
|
|
|
111
156
|
cleanup();
|
|
112
157
|
}
|
|
113
158
|
};
|
|
114
|
-
}, [
|
|
115
|
-
|
|
159
|
+
}, [stableConfig, stableOptions, state.currentPage, pageSize]);
|
|
160
|
+
// Calculate pagination flags
|
|
161
|
+
const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
|
|
162
|
+
return {
|
|
163
|
+
results: state.results,
|
|
164
|
+
error: state.error,
|
|
165
|
+
isLoading: state.isLoading,
|
|
166
|
+
pagination: {
|
|
167
|
+
hasNextPage: paginationFlags.hasNextPage,
|
|
168
|
+
hasPreviousPage: paginationFlags.hasPreviousPage,
|
|
169
|
+
currentPage: state.currentPage,
|
|
170
|
+
pageSize: state.pageSize,
|
|
171
|
+
nextPage,
|
|
172
|
+
previousPage,
|
|
173
|
+
reset,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
116
176
|
}
|
|
@@ -84,14 +84,6 @@ declare const FileInput: "FileInput" & {
|
|
|
84
84
|
readonly props?: experimentalTypes.FileInputProps | undefined;
|
|
85
85
|
readonly children?: true | undefined;
|
|
86
86
|
} & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"FileInput", experimentalTypes.FileInputProps, true>>;
|
|
87
|
-
/**
|
|
88
|
-
* The TimeInput component renders an input field where a user can select a time.
|
|
89
|
-
*/
|
|
90
|
-
declare const TimeInput: "TimeInput" & {
|
|
91
|
-
readonly type?: "TimeInput" | undefined;
|
|
92
|
-
readonly props?: experimentalTypes.TimeInputProps | undefined;
|
|
93
|
-
readonly children?: true | undefined;
|
|
94
|
-
} & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"TimeInput", experimentalTypes.TimeInputProps, true>>;
|
|
95
87
|
/** @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. */
|
|
96
88
|
declare const CurrencyInput: "CurrencyInput" & {
|
|
97
89
|
readonly type?: "CurrencyInput" | undefined;
|
|
@@ -116,4 +108,4 @@ declare const SecondaryHeaderActionButton: "SecondaryHeaderActionButton" & {
|
|
|
116
108
|
readonly props?: experimentalTypes.HeaderActionButtonProps | undefined;
|
|
117
109
|
readonly children?: true | undefined;
|
|
118
110
|
} & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"SecondaryHeaderActionButton", experimentalTypes.HeaderActionButtonProps, true>>;
|
|
119
|
-
export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput,
|
|
111
|
+
export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput, CurrencyInput, HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, };
|
|
@@ -37,10 +37,6 @@ const ExpandableText = createRemoteReactComponent('ExpandableText');
|
|
|
37
37
|
*/
|
|
38
38
|
const Popover = createRemoteReactComponent('Popover');
|
|
39
39
|
const FileInput = createRemoteReactComponent('FileInput');
|
|
40
|
-
/**
|
|
41
|
-
* The TimeInput component renders an input field where a user can select a time.
|
|
42
|
-
*/
|
|
43
|
-
const TimeInput = createRemoteReactComponent('TimeInput');
|
|
44
40
|
/** @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. */
|
|
45
41
|
const CurrencyInput = createRemoteReactComponent('CurrencyInput');
|
|
46
42
|
/** @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. */
|
|
@@ -53,4 +49,4 @@ const PrimaryHeaderActionButton = createRemoteReactComponent('PrimaryHeaderActio
|
|
|
53
49
|
const SecondaryHeaderActionButton = createRemoteReactComponent('SecondaryHeaderActionButton', {
|
|
54
50
|
fragmentProps: ['overlay'],
|
|
55
51
|
});
|
|
56
|
-
export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput,
|
|
52
|
+
export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput, CurrencyInput, HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, };
|
|
@@ -168,16 +168,6 @@ export interface FileInputProps {
|
|
|
168
168
|
name: string;
|
|
169
169
|
onChange: (event: any) => void;
|
|
170
170
|
}
|
|
171
|
-
/**
|
|
172
|
-
* @ignore
|
|
173
|
-
* @experimental do not use in production
|
|
174
|
-
*/
|
|
175
|
-
export interface BaseTime {
|
|
176
|
-
/** The hour for the time (0 to 23) in 24-hour format (e.g. 0 = 12:00 AM, 9 = 9:00 AM, 15 = 3:00 PM). */
|
|
177
|
-
hours: number;
|
|
178
|
-
/** The minutes for the time (0 to 59). */
|
|
179
|
-
minutes: number;
|
|
180
|
-
}
|
|
181
171
|
/**
|
|
182
172
|
* Generic collection of props for all inputs (experimental version)
|
|
183
173
|
* @internal
|
|
@@ -258,50 +248,6 @@ export interface BaseInputProps<T = string, V = string> {
|
|
|
258
248
|
*/
|
|
259
249
|
onFocus?: (value: V) => void;
|
|
260
250
|
}
|
|
261
|
-
/**
|
|
262
|
-
* @ignore
|
|
263
|
-
* @experimental do not use in production
|
|
264
|
-
*
|
|
265
|
-
* The values used to invoke events on the TimeInput component
|
|
266
|
-
*/
|
|
267
|
-
export interface TimeInputEventsPayload extends BaseTime {
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* @internal
|
|
271
|
-
* @ignore
|
|
272
|
-
* */
|
|
273
|
-
type BaseTimeInputForTime = Omit<BaseInputProps<BaseTime | null, TimeInputEventsPayload>, 'onInput' | 'placeholder' | 'onChange'>;
|
|
274
|
-
/**
|
|
275
|
-
* @ignore
|
|
276
|
-
* @experimental do not use in production
|
|
277
|
-
*/
|
|
278
|
-
export interface TimeInputProps extends BaseTimeInputForTime {
|
|
279
|
-
/**
|
|
280
|
-
* A callback function that is invoked when the value is changed.
|
|
281
|
-
*
|
|
282
|
-
* @event
|
|
283
|
-
*/
|
|
284
|
-
onChange?: (value: TimeInputEventsPayload) => void;
|
|
285
|
-
/**
|
|
286
|
-
* Sets the earliest time that will be valid.
|
|
287
|
-
*/
|
|
288
|
-
min?: BaseTime;
|
|
289
|
-
/**
|
|
290
|
-
* Sets the latest time that will be valid.
|
|
291
|
-
*/
|
|
292
|
-
max?: BaseTime;
|
|
293
|
-
/**
|
|
294
|
-
* Sets the interval (in minutes) between the dropdown options.
|
|
295
|
-
*
|
|
296
|
-
* @defaultValue `30`
|
|
297
|
-
*/
|
|
298
|
-
interval?: number;
|
|
299
|
-
/**
|
|
300
|
-
* Sets the timezone that the component will display alongside times in the TimePicker. This will not adjust the available valid inputs.
|
|
301
|
-
*
|
|
302
|
-
*/
|
|
303
|
-
timezone?: 'userTz' | 'portalTz';
|
|
304
|
-
}
|
|
305
251
|
/**
|
|
306
252
|
* @ignore
|
|
307
253
|
* @experimental do not use in production
|
package/dist/types.d.ts
CHANGED
|
@@ -1186,6 +1186,59 @@ export interface DateInputProps extends BaseDateInputForDate {
|
|
|
1186
1186
|
*/
|
|
1187
1187
|
todayButtonLabel?: string;
|
|
1188
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Object that represents times.
|
|
1191
|
+
*/
|
|
1192
|
+
export interface BaseTime {
|
|
1193
|
+
/** The hour for the time (0 to 23) in 24-hour format (e.g. 0 = 12:00 AM, 9 = 9:00 AM, 15 = 3:00 PM). */
|
|
1194
|
+
hours: number;
|
|
1195
|
+
/** The minutes for the time (0 to 59). */
|
|
1196
|
+
minutes: number;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* @ignore
|
|
1200
|
+
*
|
|
1201
|
+
* The values used to invoke events on the TimeInput component
|
|
1202
|
+
*/
|
|
1203
|
+
export interface TimeInputEventsPayload extends BaseTime {
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* @internal
|
|
1207
|
+
* @ignore
|
|
1208
|
+
* */
|
|
1209
|
+
type BaseTimeInputForTime = Omit<BaseInputProps<BaseTime | null, TimeInputEventsPayload>, 'onInput' | 'placeholder' | 'onChange'>;
|
|
1210
|
+
/**
|
|
1211
|
+
* The props type for {@link !components.TimeInput}.
|
|
1212
|
+
*
|
|
1213
|
+
* @category Component Props
|
|
1214
|
+
*/
|
|
1215
|
+
export interface TimeInputProps extends BaseTimeInputForTime {
|
|
1216
|
+
/**
|
|
1217
|
+
* A callback function that is invoked when the value is changed.
|
|
1218
|
+
*
|
|
1219
|
+
* @event
|
|
1220
|
+
*/
|
|
1221
|
+
onChange?: (value: TimeInputEventsPayload) => void;
|
|
1222
|
+
/**
|
|
1223
|
+
* Sets the earliest time that will be valid.
|
|
1224
|
+
*/
|
|
1225
|
+
min?: BaseTime;
|
|
1226
|
+
/**
|
|
1227
|
+
* Sets the latest time that will be valid.
|
|
1228
|
+
*/
|
|
1229
|
+
max?: BaseTime;
|
|
1230
|
+
/**
|
|
1231
|
+
* Sets the interval (in minutes) between the dropdown options.
|
|
1232
|
+
*
|
|
1233
|
+
* @defaultValue `30`
|
|
1234
|
+
*/
|
|
1235
|
+
interval?: number;
|
|
1236
|
+
/**
|
|
1237
|
+
* Sets the timezone that the component will display alongside times in the TimeInput. This will not modify the available valid inputs.
|
|
1238
|
+
*
|
|
1239
|
+
*/
|
|
1240
|
+
timezone?: 'userTz' | 'portalTz';
|
|
1241
|
+
}
|
|
1189
1242
|
/**
|
|
1190
1243
|
* The props type for {@link !components.SearchInput}.
|
|
1191
1244
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/ui-extensions",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "^29.1.1",
|
|
67
67
|
"typescript": "5.0.4"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "8344193a13ab5874924b02d67413dad052558ecf"
|
|
70
70
|
}
|