@hubspot/ui-extensions 0.9.4 → 0.9.6

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.
@@ -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 module
13
- jest.mock('../../../experimental/crm/fetchAssociations');
14
- const mockFetchAssociations = jest.fn();
15
- import * as fetchAssociationsModule from '../../../experimental/crm/fetchAssociations';
16
- fetchAssociationsModule.fetchAssociations = mockFetchAssociations;
17
- describe('useAssociations', () => {
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() warning coming from @testing-library/react
23
+ // Suppress React act() warnings coming from @testing-library/react
21
24
  originalError = console.error;
22
25
  console.error = (...args) => {
23
- if (args[0]?.includes('ReactDOMTestUtils.act'))
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 return the initial values for results, error, isLoading, hasMore, and nextOffset', async () => {
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: 25,
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(true);
47
- expect(result.current.hasMore).toBe(false);
48
- expect(result.current.nextOffset).toBe(0);
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('successful data fetching', () => {
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: 25,
100
+ nextOffset: 10,
80
101
  },
81
102
  cleanup: jest.fn(),
82
103
  });
83
- const request = {
104
+ const { result } = renderHook(() => useAssociations({
84
105
  toObjectType: '0-1',
85
106
  properties: ['firstname', 'lastname'],
86
- pageLength: 25,
87
- offset: 0,
88
- };
89
- const { result } = renderHook(() => useAssociations(request));
107
+ pageLength: 10,
108
+ }));
90
109
  await waitFor(() => {
91
- expect(result.current.results).toEqual([
92
- {
93
- toObjectId: 1001,
94
- associationTypes: [
95
- { category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
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 request = {
133
+ const { result } = renderHook(() => useAssociations({
129
134
  toObjectType: '0-1',
130
135
  properties: ['firstname'],
131
- pageLength: 25,
132
- offset: 0,
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.hasMore).toBe(false);
138
- expect(result.current.nextOffset).toBe(0);
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
- it('should handle different object types correctly', async () => {
144
- mockFetchAssociations.mockResolvedValue({
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
- toObjectId: 2001,
149
- associationTypes: [
150
- {
151
- category: 'HUBSPOT_DEFINED',
152
- typeId: 2,
153
- label: 'Company Primary',
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: 25,
163
+ nextOffset: 20,
163
164
  },
164
165
  cleanup: jest.fn(),
165
166
  });
166
- const request = {
167
- toObjectType: '0-2',
168
- properties: ['name'],
169
- pageLength: 25,
170
- offset: 0,
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.results).toEqual([
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
- expect(mockFetchAssociations).toHaveBeenCalledWith(expect.objectContaining({
192
- toObjectType: '0-2',
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
- describe('error handling', () => {
197
- it('should handle fetch errors correctly', async () => {
198
- const errorMessage = 'Failed to fetch associations';
199
- mockFetchAssociations.mockRejectedValue(new Error(errorMessage));
200
- const request = {
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
- properties: ['firstname'],
203
- pageLength: 25,
204
- offset: 0,
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.error).toBeInstanceOf(Error);
209
- expect(result.current.error?.message).toBe(errorMessage);
210
- expect(result.current.results).toEqual([]);
211
- expect(result.current.isLoading).toBe(false);
212
- expect(result.current.hasMore).toBe(false);
213
- expect(result.current.nextOffset).toBe(0);
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
- describe('pagination handling', () => {
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
- toObjectId: 1025,
224
- associationTypes: [
225
- { category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
226
- ],
227
- properties: {
228
- firstname: 'Page 2 User',
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: 50,
278
+ nextOffset: 10,
234
279
  },
235
280
  cleanup: jest.fn(),
236
281
  });
237
- const request = {
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: 25,
241
- offset: 25, // Second page
242
- };
243
- const { result } = renderHook(() => useAssociations(request));
307
+ pageLength: 10,
308
+ }));
244
309
  await waitFor(() => {
245
- expect(result.current.results).toEqual([
246
- {
247
- toObjectId: 1025,
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 request = {
327
+ const config = {
273
328
  toObjectType: '0-1',
274
329
  properties: ['firstname', 'lastname'],
275
- pageLength: 25,
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(request, options));
341
+ renderHook(() => useAssociations(config, options));
288
342
  await waitFor(() => {
289
- expect(mockFetchAssociations).toHaveBeenCalledWith(request, options);
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 request = {
355
+ const config = {
302
356
  toObjectType: '0-1',
303
357
  properties: ['firstname'],
304
- pageLength: 25,
305
- offset: 0,
358
+ pageLength: 10,
306
359
  };
307
- renderHook(() => useAssociations(request));
360
+ renderHook(() => useAssociations(config));
308
361
  await waitFor(() => {
309
- expect(mockFetchAssociations).toHaveBeenCalledWith(request, {});
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 request = {
377
+ const config = {
325
378
  toObjectType: '0-1',
326
379
  properties: ['firstname'],
327
- pageLength: 25,
328
- offset: 0,
380
+ pageLength: 10,
329
381
  };
330
- const { unmount } = renderHook(() => useAssociations(request));
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 request', async () => {
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 request = {
400
+ const config = {
349
401
  toObjectType: '0-1',
350
402
  properties: ['firstname'],
351
- pageLength: 25,
352
- offset: 0,
403
+ pageLength: 10,
353
404
  };
354
- const { rerender } = renderHook(({ requestProp }) => useAssociations(requestProp), {
355
- initialProps: { requestProp: request },
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 request object content but different reference
361
- rerender({ requestProp: { ...request } });
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);
@@ -794,3 +794,39 @@ 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>>;
809
+ /**
810
+ * The `CurrencyInput` component renders a currency input field with proper formatting,
811
+ * currency symbols, and locale-specific display patterns. Commonly used within the `Form` component.
812
+ *
813
+ * **Links:**
814
+ *
815
+ * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/currency-input Docs}
816
+ */
817
+ export declare const CurrencyInput: "CurrencyInput" & {
818
+ readonly type?: "CurrencyInput" | undefined;
819
+ readonly props?: types.CurrencyInputProps | undefined;
820
+ readonly children?: true | undefined;
821
+ } & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"CurrencyInput", types.CurrencyInputProps, true>>;
822
+ /**
823
+ * The `Inline` component spreads aligns its children horizontally (along the x-axis).
824
+ *
825
+ * **Links:**
826
+ *
827
+ * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/inline Docs}
828
+ */ export declare const Inline: "Inline" & {
829
+ readonly type?: "Inline" | undefined;
830
+ readonly props?: types.InlineProps | undefined;
831
+ readonly children?: true | undefined;
832
+ } & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"Inline", types.InlineProps, true>>;
@@ -534,3 +534,27 @@ 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');
545
+ /**
546
+ * The `CurrencyInput` component renders a currency input field with proper formatting,
547
+ * currency symbols, and locale-specific display patterns. Commonly used within the `Form` component.
548
+ *
549
+ * **Links:**
550
+ *
551
+ * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/currency-input Docs}
552
+ */
553
+ export const CurrencyInput = createRemoteReactComponent('CurrencyInput');
554
+ /**
555
+ * The `Inline` component spreads aligns its children horizontally (along the x-axis).
556
+ *
557
+ * **Links:**
558
+ *
559
+ * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/inline Docs}
560
+ */ export const Inline = createRemoteReactComponent('Inline');
@@ -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 AssociationsResponse } from '../crm/fetchAssociations';
1
+ import { type FetchAssociationsRequest, type AssociationResult } from '../crm/fetchAssociations';
2
2
  import { type FetchCrmPropertiesOptions } from '../crm/fetchCrmProperties';
3
- export interface AssociationsState {
4
- results: AssociationsResponse['results'];
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
- hasMore: boolean;
8
- nextOffset: number;
20
+ pagination: UseAssociationsPagination;
9
21
  }
10
- export declare function useAssociations(request: FetchAssociationsRequest, options?: FetchCrmPropertiesOptions): AssociationsState;
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
- const initialState = {
5
- results: [],
6
- error: null,
7
- isLoading: true,
8
- hasMore: false,
9
- nextOffset: 0,
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
- nextOffset: 0,
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(request, options = DEFAULT_OPTIONS) {
43
- const [state, dispatch] = useReducer(associationsReducer, initialState);
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 request and options to prevent unnecessary re-renders and API calls.
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 lastRequestRef = useRef();
56
- const lastRequestKeyRef = useRef();
75
+ const lastConfigRef = useRef();
76
+ const lastConfigKeyRef = useRef();
57
77
  const lastOptionsRef = useRef();
58
78
  const lastOptionsKeyRef = useRef();
59
- const stableRequest = useMemo(() => {
60
- const requestKey = JSON.stringify(request);
61
- if (requestKey === lastRequestKeyRef.current) {
62
- return lastRequestRef.current;
79
+ const stableConfig = useMemo(() => {
80
+ const configKey = JSON.stringify(config);
81
+ if (configKey === lastConfigKeyRef.current) {
82
+ return lastConfigRef.current;
63
83
  }
64
- lastRequestKeyRef.current = requestKey;
65
- lastRequestRef.current = request;
66
- return request;
67
- }, [request]);
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
- const result = await fetchAssociations(stableRequest, stableOptions);
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
- }, [stableRequest, stableOptions]);
115
- return state;
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
  }
@@ -15,12 +15,6 @@ declare const MediaObject: "MediaObject" & {
15
15
  readonly children?: true | undefined;
16
16
  } & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"MediaObject", experimentalTypes.MediaObjectProps, true>>;
17
17
  /** @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. */
18
- declare const Inline: "Inline" & {
19
- readonly type?: "Inline" | undefined;
20
- readonly props?: experimentalTypes.InlineProps | undefined;
21
- readonly children?: true | undefined;
22
- } & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"Inline", experimentalTypes.InlineProps, true>>;
23
- /** @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. */
24
18
  declare const Stack2: "Stack2" & {
25
19
  readonly type?: "Stack2" | undefined;
26
20
  readonly props?: experimentalTypes.Stack2Props | undefined;
@@ -84,20 +78,6 @@ declare const FileInput: "FileInput" & {
84
78
  readonly props?: experimentalTypes.FileInputProps | undefined;
85
79
  readonly children?: true | undefined;
86
80
  } & 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
- /** @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
- declare const CurrencyInput: "CurrencyInput" & {
97
- readonly type?: "CurrencyInput" | undefined;
98
- readonly props?: experimentalTypes.CurrencyInputProps | undefined;
99
- readonly children?: true | undefined;
100
- } & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"CurrencyInput", experimentalTypes.CurrencyInputProps, true>>;
101
81
  /** @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. */
102
82
  declare const HeaderActions: "HeaderActions" & {
103
83
  readonly type?: "HeaderActions" | undefined;
@@ -116,4 +96,4 @@ declare const SecondaryHeaderActionButton: "SecondaryHeaderActionButton" & {
116
96
  readonly props?: experimentalTypes.HeaderActionButtonProps | undefined;
117
97
  readonly children?: true | undefined;
118
98
  } & 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, TimeInput, CurrencyInput, HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, };
99
+ export { Iframe, MediaObject, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput, HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, };
@@ -8,8 +8,6 @@ const MediaObject = createRemoteReactComponent('MediaObject', {
8
8
  fragmentProps: ['itemRight', 'itemLeft'],
9
9
  });
10
10
  /** @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. */
11
- const Inline = createRemoteReactComponent('Inline');
12
- /** @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. */
13
11
  const Stack2 = createRemoteReactComponent('Stack2');
14
12
  /** @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. */
15
13
  const Center = createRemoteReactComponent('Center');
@@ -37,12 +35,6 @@ const ExpandableText = createRemoteReactComponent('ExpandableText');
37
35
  */
38
36
  const Popover = createRemoteReactComponent('Popover');
39
37
  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
- /** @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
- const CurrencyInput = createRemoteReactComponent('CurrencyInput');
46
38
  /** @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. */
47
39
  const HeaderActions = createRemoteReactComponent('HeaderActions');
48
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. */
@@ -53,4 +45,4 @@ const PrimaryHeaderActionButton = createRemoteReactComponent('PrimaryHeaderActio
53
45
  const SecondaryHeaderActionButton = createRemoteReactComponent('SecondaryHeaderActionButton', {
54
46
  fragmentProps: ['overlay'],
55
47
  });
56
- export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput, TimeInput, CurrencyInput, HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, };
48
+ export { Iframe, MediaObject, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput, HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, };
@@ -19,16 +19,6 @@ export type FlexJustify = 'center' | 'end' | 'start' | 'around' | 'between';
19
19
  * @experimental do not use in production
20
20
  */
21
21
  export type FlexAlign = 'start' | 'center' | 'baseline' | 'end' | 'stretch';
22
- /**
23
- * @ignore
24
- * @experimental do not use in production
25
- */
26
- export interface InlineProps extends BaseLayout {
27
- justify?: FlexJustify;
28
- align?: FlexAlign;
29
- gap?: AllDistances;
30
- children?: ReactNode;
31
- }
32
22
  /**
33
23
  * @ignore
34
24
  * @experimental do not use in production
@@ -168,16 +158,6 @@ export interface FileInputProps {
168
158
  name: string;
169
159
  onChange: (event: any) => void;
170
160
  }
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
161
  /**
182
162
  * Generic collection of props for all inputs (experimental version)
183
163
  * @internal
@@ -261,76 +241,7 @@ export interface BaseInputProps<T = string, V = string> {
261
241
  /**
262
242
  * @ignore
263
243
  * @experimental do not use in production
264
- *
265
- * The values used to invoke events on the TimeInput component
266
244
  */
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
- /**
306
- * @ignore
307
- * @experimental do not use in production
308
- */
309
- type BaseInputForNumber = Omit<BaseInputProps<number, number>, 'onInput'>;
310
- /**
311
- * @ignore
312
- * @experimental do not use in production
313
- */
314
- export interface CurrencyInputProps extends BaseInputForNumber {
315
- /**
316
- * ISO 4217 currency code (e.g., "USD", "EUR", "JPY")
317
- * @defaultValue "USD"
318
- */
319
- currency?: string;
320
- /**
321
- * Sets the number of decimal places for the currency
322
- * If not provided, defaults to currency-specific precision
323
- */
324
- precision?: number;
325
- /**
326
- * Sets the lower bound of the input
327
- */
328
- min?: number;
329
- /**
330
- * Sets the upper bound of the input
331
- */
332
- max?: number;
333
- }
334
245
  /**
335
246
  * @ignore
336
247
  * @experimental do not use in production
package/dist/types.d.ts CHANGED
@@ -1084,6 +1084,31 @@ export interface NumberInputProps extends BaseInputForNumber {
1084
1084
  /** @deprecated use onChange instead. It doesn't guarantee valid format */
1085
1085
  onInput?: (value: number) => void;
1086
1086
  }
1087
+ /**
1088
+ * The props type for {@link !components.CurrencyInput}.
1089
+ *
1090
+ * @category Component Props
1091
+ */
1092
+ export interface CurrencyInputProps extends BaseInputForNumber {
1093
+ /**
1094
+ * ISO 4217 currency code (e.g., "USD", "EUR", "JPY")
1095
+ * @defaultValue "USD"
1096
+ */
1097
+ currency?: string;
1098
+ /**
1099
+ * Sets the number of decimal places for the currency
1100
+ * If not provided, defaults to currency-specific precision
1101
+ */
1102
+ precision?: number;
1103
+ /**
1104
+ * Sets the lower bound of the input
1105
+ */
1106
+ min?: number;
1107
+ /**
1108
+ * Sets the upper bound of the input
1109
+ */
1110
+ max?: number;
1111
+ }
1087
1112
  /**
1088
1113
  * The props type for {@link !components.StepperInput}.
1089
1114
  *
@@ -1186,6 +1211,59 @@ export interface DateInputProps extends BaseDateInputForDate {
1186
1211
  */
1187
1212
  todayButtonLabel?: string;
1188
1213
  }
1214
+ /**
1215
+ * Object that represents times.
1216
+ */
1217
+ export interface BaseTime {
1218
+ /** 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). */
1219
+ hours: number;
1220
+ /** The minutes for the time (0 to 59). */
1221
+ minutes: number;
1222
+ }
1223
+ /**
1224
+ * @ignore
1225
+ *
1226
+ * The values used to invoke events on the TimeInput component
1227
+ */
1228
+ export interface TimeInputEventsPayload extends BaseTime {
1229
+ }
1230
+ /**
1231
+ * @internal
1232
+ * @ignore
1233
+ * */
1234
+ type BaseTimeInputForTime = Omit<BaseInputProps<BaseTime | null, TimeInputEventsPayload>, 'onInput' | 'placeholder' | 'onChange'>;
1235
+ /**
1236
+ * The props type for {@link !components.TimeInput}.
1237
+ *
1238
+ * @category Component Props
1239
+ */
1240
+ export interface TimeInputProps extends BaseTimeInputForTime {
1241
+ /**
1242
+ * A callback function that is invoked when the value is changed.
1243
+ *
1244
+ * @event
1245
+ */
1246
+ onChange?: (value: TimeInputEventsPayload) => void;
1247
+ /**
1248
+ * Sets the earliest time that will be valid.
1249
+ */
1250
+ min?: BaseTime;
1251
+ /**
1252
+ * Sets the latest time that will be valid.
1253
+ */
1254
+ max?: BaseTime;
1255
+ /**
1256
+ * Sets the interval (in minutes) between the dropdown options.
1257
+ *
1258
+ * @defaultValue `30`
1259
+ */
1260
+ interval?: number;
1261
+ /**
1262
+ * Sets the timezone that the component will display alongside times in the TimeInput. This will not modify the available valid inputs.
1263
+ *
1264
+ */
1265
+ timezone?: 'userTz' | 'portalTz';
1266
+ }
1189
1267
  /**
1190
1268
  * The props type for {@link !components.SearchInput}.
1191
1269
  *
@@ -1303,6 +1381,36 @@ export interface BoxProps {
1303
1381
  */
1304
1382
  flex?: 'initial' | 'auto' | 'none' | number;
1305
1383
  }
1384
+ export type FlexJustify = 'center' | 'end' | 'start' | 'around' | 'between';
1385
+ export type FlexAlign = 'start' | 'center' | 'baseline' | 'end' | 'stretch';
1386
+ /**
1387
+ * The props type for {@link !components.Inline}.
1388
+ *
1389
+ * @category Component Props
1390
+ */
1391
+ export interface InlineProps {
1392
+ /**
1393
+ * Sets the content that will render inside the component. This prop is passed implicitly by providing sub-components.
1394
+ *
1395
+ */
1396
+ children?: ReactNode;
1397
+ /**
1398
+ * Distributes components along the main axis using the available free space.
1399
+ * @defaultValue `"start"`
1400
+ */
1401
+ justify?: FlexJustify;
1402
+ /**
1403
+ * Distributes components along the cross-axis using the available free space.
1404
+ * @defaultValue `"center"`
1405
+ */
1406
+ align?: FlexAlign;
1407
+ /**
1408
+ * Sets the spacing between components.
1409
+ *
1410
+ * @defaultValue `"flush"`
1411
+ */
1412
+ gap?: AllDistances;
1413
+ }
1306
1414
  /**
1307
1415
  * The props type for {@link !components.Link}.
1308
1416
  *
@@ -1617,15 +1725,6 @@ export interface BaseSelectProps {
1617
1725
  *
1618
1726
  */
1619
1727
  validationMessage?: string;
1620
- /**
1621
- * The options to display in the dropdown menu.
1622
- */
1623
- options: {
1624
- /** Will be used as the display text. **/
1625
- label: string;
1626
- /** Should be the option's unique identifier, which is submitted with the form. **/
1627
- value: string | number;
1628
- }[];
1629
1728
  /**
1630
1729
  * The variant type for the select.
1631
1730
  *
@@ -1643,7 +1742,7 @@ export interface SelectProps extends BaseSelectProps {
1643
1742
  * The value of the select input.
1644
1743
  *
1645
1744
  */
1646
- value?: string | number;
1745
+ value?: string | number | boolean;
1647
1746
  /**
1648
1747
  * A callback function that is invoked when the value is committed.
1649
1748
  *
@@ -1664,6 +1763,15 @@ export interface SelectProps extends BaseSelectProps {
1664
1763
  * @event
1665
1764
  */
1666
1765
  onInput?: (value: string) => void;
1766
+ /**
1767
+ * The options to display in the dropdown menu.
1768
+ */
1769
+ options: {
1770
+ /** Will be used as the display text. **/
1771
+ label: string;
1772
+ /** Should be the option's unique identifier, which is submitted with the form. **/
1773
+ value: string | number | boolean;
1774
+ }[];
1667
1775
  }
1668
1776
  /**
1669
1777
  * The props type for {@link !components.MultiSelect}.
@@ -1682,6 +1790,15 @@ export interface MultiSelectProps extends BaseSelectProps {
1682
1790
  * @event
1683
1791
  */
1684
1792
  onChange?: (value: NonNullable<MultiSelectProps['value']>) => void;
1793
+ /**
1794
+ * The options to display in the dropdown menu.
1795
+ */
1796
+ options: {
1797
+ /** Will be used as the display text. **/
1798
+ label: string;
1799
+ /** Should be the option's unique identifier, which is submitted with the form. **/
1800
+ value: string | number;
1801
+ }[];
1685
1802
  }
1686
1803
  /**
1687
1804
  * The props type for {@link !components.StatisticsTrend}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
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": "65046f5f3ce4c51652bc454e4425b57bd33aedbf"
69
+ "gitHead": "1b458008ea50e3fb395db1766239aea3abb6753e"
70
70
  }