@hubspot/ui-extensions 0.9.3 → 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.d.ts +1 -0
- package/dist/__tests__/experimental/crm/fetchAssociations.spec.js +442 -0
- package/dist/__tests__/experimental/crm/fetchCrmProperties.spec.js +1 -0
- package/dist/__tests__/experimental/hooks/useAssociations.spec.d.ts +1 -0
- package/dist/__tests__/experimental/hooks/useAssociations.spec.js +419 -0
- package/dist/coreComponents.d.ts +12 -0
- package/dist/coreComponents.js +8 -0
- package/dist/experimental/crm/fetchAssociations.d.ts +45 -0
- package/dist/experimental/crm/fetchAssociations.js +64 -0
- package/dist/experimental/crm/fetchCrmProperties.d.ts +1 -0
- package/dist/experimental/crm/fetchCrmProperties.js +18 -13
- package/dist/experimental/hooks/useAssociations.d.ts +22 -0
- package/dist/experimental/hooks/useAssociations.js +176 -0
- package/dist/experimental/hooks/useCrmProperties.js +42 -20
- package/dist/experimental/index.d.ts +20 -9
- package/dist/experimental/index.js +12 -5
- package/dist/experimental/types.d.ts +21 -54
- package/dist/types.d.ts +68 -1
- package/dist/types.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { useAssociations } from '../../../experimental/hooks/useAssociations';
|
|
3
|
+
import { fetchAssociations } from '../../../experimental/crm/fetchAssociations';
|
|
4
|
+
// Mock the logger module
|
|
5
|
+
jest.mock('../../../logger', () => ({
|
|
6
|
+
logger: {
|
|
7
|
+
debug: jest.fn(),
|
|
8
|
+
info: jest.fn(),
|
|
9
|
+
warn: jest.fn(),
|
|
10
|
+
error: jest.fn(),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
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', () => {
|
|
21
|
+
let originalError;
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
// Suppress React act() warnings coming from @testing-library/react
|
|
24
|
+
originalError = console.error;
|
|
25
|
+
console.error = (...args) => {
|
|
26
|
+
if (typeof args[0] === 'string' &&
|
|
27
|
+
(args[0].includes('ReactDOMTestUtils.act') ||
|
|
28
|
+
args[0].includes('was not wrapped in act')))
|
|
29
|
+
return;
|
|
30
|
+
originalError.call(console, ...args);
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// Reset the mock before each test
|
|
35
|
+
mockFetchAssociations.mockReset();
|
|
36
|
+
});
|
|
37
|
+
afterAll(() => {
|
|
38
|
+
console.error = originalError;
|
|
39
|
+
});
|
|
40
|
+
describe('initial state', () => {
|
|
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
|
+
});
|
|
50
|
+
const { result } = renderHook(() => useAssociations({
|
|
51
|
+
toObjectType: '0-1',
|
|
52
|
+
properties: ['firstname', 'lastname'],
|
|
53
|
+
pageLength: 10,
|
|
54
|
+
}));
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(result.current.results).toEqual([]);
|
|
57
|
+
expect(result.current.error).toBeNull();
|
|
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
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('data fetching', () => {
|
|
84
|
+
it('should successfully fetch and return associations with pagination info', async () => {
|
|
85
|
+
mockFetchAssociations.mockResolvedValue({
|
|
86
|
+
data: {
|
|
87
|
+
results: [
|
|
88
|
+
{
|
|
89
|
+
toObjectId: 1001,
|
|
90
|
+
associationTypes: [
|
|
91
|
+
{ category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
|
|
92
|
+
],
|
|
93
|
+
properties: {
|
|
94
|
+
firstname: 'John',
|
|
95
|
+
lastname: 'Doe',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
hasMore: true,
|
|
100
|
+
nextOffset: 10,
|
|
101
|
+
},
|
|
102
|
+
cleanup: jest.fn(),
|
|
103
|
+
});
|
|
104
|
+
const { result } = renderHook(() => useAssociations({
|
|
105
|
+
toObjectType: '0-1',
|
|
106
|
+
properties: ['firstname', 'lastname'],
|
|
107
|
+
pageLength: 10,
|
|
108
|
+
}));
|
|
109
|
+
await waitFor(() => {
|
|
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);
|
|
115
|
+
expect(result.current.isLoading).toBe(false);
|
|
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));
|
|
123
|
+
});
|
|
124
|
+
it('should handle empty results correctly', async () => {
|
|
125
|
+
mockFetchAssociations.mockResolvedValue({
|
|
126
|
+
data: {
|
|
127
|
+
results: [],
|
|
128
|
+
hasMore: false,
|
|
129
|
+
nextOffset: 0,
|
|
130
|
+
},
|
|
131
|
+
cleanup: jest.fn(),
|
|
132
|
+
});
|
|
133
|
+
const { result } = renderHook(() => useAssociations({
|
|
134
|
+
toObjectType: '0-1',
|
|
135
|
+
properties: ['firstname'],
|
|
136
|
+
pageLength: 5,
|
|
137
|
+
}));
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(result.current.results).toEqual([]);
|
|
140
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
141
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
142
|
+
expect(result.current.isLoading).toBe(false);
|
|
143
|
+
expect(result.current.error).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('pagination actions', () => {
|
|
148
|
+
it('should navigate to next page correctly', async () => {
|
|
149
|
+
// First page response
|
|
150
|
+
mockFetchAssociations.mockResolvedValueOnce({
|
|
151
|
+
data: {
|
|
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: {} }],
|
|
162
|
+
hasMore: false,
|
|
163
|
+
nextOffset: 20,
|
|
164
|
+
},
|
|
165
|
+
cleanup: jest.fn(),
|
|
166
|
+
});
|
|
167
|
+
const { result } = renderHook(() => useAssociations({
|
|
168
|
+
toObjectType: '0-1',
|
|
169
|
+
pageLength: 10,
|
|
170
|
+
}));
|
|
171
|
+
// Wait for initial load
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
174
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
175
|
+
});
|
|
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));
|
|
187
|
+
});
|
|
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({
|
|
217
|
+
toObjectType: '0-1',
|
|
218
|
+
pageLength: 10,
|
|
219
|
+
}));
|
|
220
|
+
// Wait for initial load (page 1)
|
|
221
|
+
await waitFor(() => {
|
|
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);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
it('should reset to first page correctly', async () => {
|
|
239
|
+
// Initial page 1 - has more pages
|
|
240
|
+
mockFetchAssociations.mockResolvedValue({
|
|
241
|
+
data: {
|
|
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: [],
|
|
277
|
+
hasMore: false,
|
|
278
|
+
nextOffset: 10,
|
|
279
|
+
},
|
|
280
|
+
cleanup: jest.fn(),
|
|
281
|
+
});
|
|
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({
|
|
305
|
+
toObjectType: '0-1',
|
|
306
|
+
properties: ['firstname'],
|
|
307
|
+
pageLength: 10,
|
|
308
|
+
}));
|
|
309
|
+
await waitFor(() => {
|
|
310
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
311
|
+
expect(result.current.error?.message).toBe(errorMessage);
|
|
312
|
+
expect(result.current.results).toEqual([]);
|
|
313
|
+
expect(result.current.isLoading).toBe(false);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
describe('options handling', () => {
|
|
318
|
+
it('should pass formatting options to fetchAssociations', async () => {
|
|
319
|
+
mockFetchAssociations.mockResolvedValue({
|
|
320
|
+
data: {
|
|
321
|
+
results: [],
|
|
322
|
+
hasMore: false,
|
|
323
|
+
nextOffset: 0,
|
|
324
|
+
},
|
|
325
|
+
cleanup: jest.fn(),
|
|
326
|
+
});
|
|
327
|
+
const config = {
|
|
328
|
+
toObjectType: '0-1',
|
|
329
|
+
properties: ['firstname', 'lastname'],
|
|
330
|
+
pageLength: 10,
|
|
331
|
+
};
|
|
332
|
+
const options = {
|
|
333
|
+
propertiesToFormat: ['firstname'],
|
|
334
|
+
formattingOptions: {
|
|
335
|
+
date: {
|
|
336
|
+
format: 'MM/DD/YYYY',
|
|
337
|
+
relative: true,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
renderHook(() => useAssociations(config, options));
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(expect.objectContaining(config), options);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
it('should use default empty options when no options provided', async () => {
|
|
347
|
+
mockFetchAssociations.mockResolvedValue({
|
|
348
|
+
data: {
|
|
349
|
+
results: [],
|
|
350
|
+
hasMore: false,
|
|
351
|
+
nextOffset: 0,
|
|
352
|
+
},
|
|
353
|
+
cleanup: jest.fn(),
|
|
354
|
+
});
|
|
355
|
+
const config = {
|
|
356
|
+
toObjectType: '0-1',
|
|
357
|
+
properties: ['firstname'],
|
|
358
|
+
pageLength: 10,
|
|
359
|
+
};
|
|
360
|
+
renderHook(() => useAssociations(config));
|
|
361
|
+
await waitFor(() => {
|
|
362
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(expect.objectContaining(config), {});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
describe('lifecycle management', () => {
|
|
367
|
+
it('should call cleanup function on unmount', async () => {
|
|
368
|
+
const mockCleanup = jest.fn();
|
|
369
|
+
mockFetchAssociations.mockResolvedValue({
|
|
370
|
+
data: {
|
|
371
|
+
results: [],
|
|
372
|
+
hasMore: false,
|
|
373
|
+
nextOffset: 0,
|
|
374
|
+
},
|
|
375
|
+
cleanup: mockCleanup,
|
|
376
|
+
});
|
|
377
|
+
const config = {
|
|
378
|
+
toObjectType: '0-1',
|
|
379
|
+
properties: ['firstname'],
|
|
380
|
+
pageLength: 10,
|
|
381
|
+
};
|
|
382
|
+
const { unmount } = renderHook(() => useAssociations(config));
|
|
383
|
+
await waitFor(() => {
|
|
384
|
+
expect(mockFetchAssociations).toHaveBeenCalled();
|
|
385
|
+
});
|
|
386
|
+
unmount();
|
|
387
|
+
await waitFor(() => {
|
|
388
|
+
expect(mockCleanup).toHaveBeenCalled();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
it('should handle stable reference optimization for config', async () => {
|
|
392
|
+
mockFetchAssociations.mockResolvedValue({
|
|
393
|
+
data: {
|
|
394
|
+
results: [],
|
|
395
|
+
hasMore: false,
|
|
396
|
+
nextOffset: 0,
|
|
397
|
+
},
|
|
398
|
+
cleanup: jest.fn(),
|
|
399
|
+
});
|
|
400
|
+
const config = {
|
|
401
|
+
toObjectType: '0-1',
|
|
402
|
+
properties: ['firstname'],
|
|
403
|
+
pageLength: 10,
|
|
404
|
+
};
|
|
405
|
+
const { rerender } = renderHook(({ configProp }) => useAssociations(configProp), {
|
|
406
|
+
initialProps: { configProp: config },
|
|
407
|
+
});
|
|
408
|
+
await waitFor(() => {
|
|
409
|
+
expect(mockFetchAssociations).toHaveBeenCalledTimes(1);
|
|
410
|
+
});
|
|
411
|
+
// Rerender with the same config object content but different reference
|
|
412
|
+
rerender({ configProp: { ...config } });
|
|
413
|
+
await waitFor(() => {
|
|
414
|
+
// Should not call fetchAssociations again due to stable reference optimization
|
|
415
|
+
expect(mockFetchAssociations).toHaveBeenCalledTimes(1);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
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');
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
};
|
|
18
|
+
export type AssociationResult = {
|
|
19
|
+
toObjectId: number;
|
|
20
|
+
associationTypes: {
|
|
21
|
+
category: string;
|
|
22
|
+
typeId: number;
|
|
23
|
+
label: string;
|
|
24
|
+
}[];
|
|
25
|
+
properties: {
|
|
26
|
+
[key: string]: string | null;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type AssociationsResponse = {
|
|
30
|
+
results: AssociationResult[];
|
|
31
|
+
hasMore: boolean;
|
|
32
|
+
nextOffset: number;
|
|
33
|
+
};
|
|
34
|
+
export type FetchAssociationsRequest = {
|
|
35
|
+
toObjectType: string;
|
|
36
|
+
properties?: string[];
|
|
37
|
+
pageLength?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
};
|
|
40
|
+
export interface FetchAssociationsResult {
|
|
41
|
+
data: AssociationsResponse;
|
|
42
|
+
error?: string;
|
|
43
|
+
cleanup: () => void;
|
|
44
|
+
}
|
|
45
|
+
export declare const fetchAssociations: (request: FetchAssociationsRequest, options?: FetchCrmPropertiesOptions) => Promise<FetchAssociationsResult>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
}
|
|
23
|
+
function isAssociationsResponse(data) {
|
|
24
|
+
if (data === null ||
|
|
25
|
+
typeof data !== 'object' ||
|
|
26
|
+
!Array.isArray(data.results) ||
|
|
27
|
+
typeof data.hasMore !== 'boolean' ||
|
|
28
|
+
typeof data.nextOffset !== 'number') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return data.results.every((result) => result !== null &&
|
|
32
|
+
typeof result === 'object' &&
|
|
33
|
+
typeof result.toObjectId === 'number' &&
|
|
34
|
+
Array.isArray(result.associationTypes) &&
|
|
35
|
+
result.properties !== null &&
|
|
36
|
+
typeof result.properties === 'object');
|
|
37
|
+
}
|
|
38
|
+
export const fetchAssociations = async (request, options) => {
|
|
39
|
+
let response;
|
|
40
|
+
let result;
|
|
41
|
+
try {
|
|
42
|
+
// eslint-disable-next-line hubspot-dev/no-confusing-browser-globals
|
|
43
|
+
response = await self.fetchAssociations(request, options);
|
|
44
|
+
result = await response.json();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw error instanceof Error
|
|
48
|
+
? error
|
|
49
|
+
: new Error('Failed to fetch associations: Unknown error');
|
|
50
|
+
}
|
|
51
|
+
if (result.error) {
|
|
52
|
+
throw new Error(result.error);
|
|
53
|
+
}
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`Failed to fetch associations: ${response.statusText}`);
|
|
56
|
+
}
|
|
57
|
+
if (!isAssociationsResponse(result.data)) {
|
|
58
|
+
throw new Error('Invalid response format');
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
data: result.data,
|
|
62
|
+
cleanup: result.cleanup || (() => { }),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
@@ -19,6 +19,7 @@ export type FetchCrmPropertiesOptions = {
|
|
|
19
19
|
};
|
|
20
20
|
export interface FetchCrmPropertiesResult {
|
|
21
21
|
data: Record<string, string | null>;
|
|
22
|
+
error?: string;
|
|
22
23
|
cleanup: () => void;
|
|
23
24
|
}
|
|
24
25
|
export declare const fetchCrmProperties: (propertyNames: string[], propertiesUpdatedCallback: (properties: Record<string, string | null>) => void, options?: FetchCrmPropertiesOptions) => Promise<FetchCrmPropertiesResult>;
|
|
@@ -12,22 +12,27 @@ function isCrmPropertiesResponse(data) {
|
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
export const fetchCrmProperties = async (propertyNames, propertiesUpdatedCallback, options) => {
|
|
15
|
+
let response;
|
|
16
|
+
let result;
|
|
15
17
|
try {
|
|
16
18
|
// eslint-disable-next-line hubspot-dev/no-confusing-browser-globals
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
throw new Error(`Failed to fetch CRM properties: ${response.statusText}`);
|
|
20
|
-
}
|
|
21
|
-
const result = await response.json();
|
|
22
|
-
if (!isCrmPropertiesResponse(result.data)) {
|
|
23
|
-
throw new Error('Invalid response format');
|
|
24
|
-
}
|
|
25
|
-
return result;
|
|
19
|
+
response = await self.fetchCrmProperties(propertyNames, propertiesUpdatedCallback, options);
|
|
20
|
+
result = await response.json();
|
|
26
21
|
}
|
|
27
22
|
catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
// Only handle network/parsing errors, not our validation errors
|
|
24
|
+
throw error instanceof Error
|
|
25
|
+
? error
|
|
26
|
+
: new Error('Failed to fetch CRM properties: Unknown error');
|
|
32
27
|
}
|
|
28
|
+
if (result.error) {
|
|
29
|
+
throw new Error(result.error);
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`Failed to fetch CRM properties: ${response.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
if (!isCrmPropertiesResponse(result.data)) {
|
|
35
|
+
throw new Error('Invalid response format');
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
33
38
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FetchAssociationsRequest, type AssociationResult } from '../crm/fetchAssociations';
|
|
2
|
+
import { type FetchCrmPropertiesOptions } from '../crm/fetchCrmProperties';
|
|
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[];
|
|
18
|
+
error: Error | null;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
pagination: UseAssociationsPagination;
|
|
21
|
+
}
|
|
22
|
+
export declare function useAssociations(config: Omit<FetchAssociationsRequest, 'offset'>, options?: UseAssociationsOptions): UseAssociationsResult;
|