@hubspot/ui-extensions 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/__generated__/version.d.ts +2 -0
  2. package/dist/__generated__/version.js +4 -0
  3. package/dist/__tests__/crm/hooks/useAssociations.spec.js +51 -0
  4. package/dist/__tests__/crm/utils/fetchAssociations.spec.js +0 -10
  5. package/dist/__tests__/experimental/hooks/useCrmSearch.spec.d.ts +1 -0
  6. package/dist/__tests__/experimental/hooks/useCrmSearch.spec.js +586 -0
  7. package/dist/__tests__/experimental/hooks/utils/fetchCrmSearch.spec.d.ts +1 -0
  8. package/dist/__tests__/experimental/hooks/utils/fetchCrmSearch.spec.js +217 -0
  9. package/dist/crm/hooks/useAssociations.d.ts +1 -1
  10. package/dist/crm/hooks/useAssociations.js +14 -5
  11. package/dist/crm/utils/fetchAssociations.js +0 -3
  12. package/dist/experimental/hooks/useCrmSearch.d.ts +2 -0
  13. package/dist/experimental/hooks/useCrmSearch.js +206 -0
  14. package/dist/experimental/hooks/utils/fetchCrmSearch.d.ts +6 -0
  15. package/dist/experimental/hooks/utils/fetchCrmSearch.js +39 -0
  16. package/dist/experimental/index.d.ts +1 -0
  17. package/dist/experimental/index.js +1 -0
  18. package/dist/shared/types/components/table.d.ts +16 -0
  19. package/dist/shared/types/context.d.ts +1 -0
  20. package/dist/shared/types/experimental.d.ts +1 -1
  21. package/dist/shared/types/hooks.d.ts +72 -0
  22. package/dist/shared/types/hooks.js +1 -0
  23. package/dist/shared/types/worker-globals.d.ts +5 -0
  24. package/dist/testing/internal/mocks/mock-hooks.js +26 -0
  25. package/dist/testing/internal/types-internal.d.ts +2 -0
  26. package/dist/testing/types.d.ts +10 -0
  27. package/dist/utils/pagination.d.ts +0 -9
  28. package/package.json +2 -1
@@ -0,0 +1,217 @@
1
+ import { vi } from 'vitest';
2
+ import { fetchCrmSearch } from "../../../../experimental/hooks/utils/fetchCrmSearch.js";
3
+ const mockFetchCrmSearch = vi.fn();
4
+ // Mock the global self object to match the hsWorkerAPI shape
5
+ Object.defineProperty(global, 'self', {
6
+ value: {
7
+ __HUBSPOT_EXTENSION_WORKER__: true,
8
+ hsWorkerAPI: {
9
+ fetchCrmSearch: mockFetchCrmSearch,
10
+ },
11
+ },
12
+ writable: true,
13
+ });
14
+ describe('fetchCrmSearch', () => {
15
+ const createMockResponse = (data, overrides = {}) => ({
16
+ ok: true,
17
+ json: vi.fn().mockResolvedValue({ data, cleanup: vi.fn() }),
18
+ ...overrides,
19
+ });
20
+ const createBasicRequest = (overrides = {}) => ({
21
+ objectType: '0-1',
22
+ properties: ['firstname', 'lastname'],
23
+ ...overrides,
24
+ });
25
+ const createValidResponseData = (overrides = {}) => ({
26
+ results: [
27
+ {
28
+ objectId: 1001,
29
+ properties: { firstname: 'John', lastname: 'Doe' },
30
+ },
31
+ ],
32
+ total: 1,
33
+ hasMore: false,
34
+ ...overrides,
35
+ });
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ });
39
+ describe('basic functionality', () => {
40
+ it('should successfully fetch CRM search results', async () => {
41
+ const data = createValidResponseData();
42
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse(data));
43
+ const request = createBasicRequest();
44
+ const result = await fetchCrmSearch(request);
45
+ expect(result).toHaveProperty('data');
46
+ expect(result).toHaveProperty('cleanup');
47
+ expect(typeof result.cleanup).toBe('function');
48
+ expect(result.data).toEqual(data);
49
+ });
50
+ it('should return a Promise', () => {
51
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse(createValidResponseData()));
52
+ const result = fetchCrmSearch(createBasicRequest());
53
+ expect(result).toBeInstanceOf(Promise);
54
+ });
55
+ it('should support all request fields (query, filterGroups, sorts, pageSize, after)', async () => {
56
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse(createValidResponseData({ hasMore: true, after: 'cursor-2' })));
57
+ const request = {
58
+ objectType: '0-1',
59
+ properties: ['firstname'],
60
+ query: 'john',
61
+ filterGroups: [
62
+ {
63
+ filters: [
64
+ { propertyName: 'lifecyclestage', operator: 'EQ', value: 'lead' },
65
+ ],
66
+ },
67
+ ],
68
+ sorts: [
69
+ { propertyName: 'createdate', direction: 'DESCENDING' },
70
+ ],
71
+ pageSize: 25,
72
+ after: 'cursor-1',
73
+ };
74
+ await fetchCrmSearch(request);
75
+ expect(mockFetchCrmSearch).toHaveBeenCalledWith(request, undefined);
76
+ });
77
+ });
78
+ describe('error handling', () => {
79
+ it('should throw an error when fetch fails', async () => {
80
+ mockFetchCrmSearch.mockRejectedValueOnce(new Error('Network error'));
81
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Network error');
82
+ });
83
+ it('should handle unknown errors with a default message', async () => {
84
+ mockFetchCrmSearch.mockRejectedValueOnce('Unknown error');
85
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Failed to fetch CRM search results: Unknown error');
86
+ });
87
+ it('should throw when server returns an error field', async () => {
88
+ mockFetchCrmSearch.mockResolvedValueOnce({
89
+ ok: true,
90
+ json: vi.fn().mockResolvedValue({
91
+ error: 'Permission denied',
92
+ cleanup: vi.fn(),
93
+ }),
94
+ });
95
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Permission denied');
96
+ });
97
+ });
98
+ describe('response validation', () => {
99
+ it('should throw error when results field is missing', async () => {
100
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse({ total: 0, hasMore: false }) // missing results
101
+ );
102
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
103
+ });
104
+ it('should throw error when results is not an array', async () => {
105
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse({
106
+ results: 'not an array',
107
+ total: 0,
108
+ hasMore: false,
109
+ }));
110
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
111
+ });
112
+ it('should throw error when total is not a number', async () => {
113
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse({ results: [], total: 'zero', hasMore: false }));
114
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
115
+ });
116
+ it('should throw error when hasMore is not a boolean', async () => {
117
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse({ results: [], total: 0, hasMore: 'yes' }));
118
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
119
+ });
120
+ it('should throw error when data is null', async () => {
121
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse(null));
122
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
123
+ });
124
+ it('should throw error when a result is missing objectId', async () => {
125
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse({
126
+ results: [{ properties: {} }],
127
+ total: 1,
128
+ hasMore: false,
129
+ }));
130
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
131
+ });
132
+ it('should throw error when a result objectId is not a number', async () => {
133
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse({
134
+ results: [{ objectId: '1001', properties: {} }],
135
+ total: 1,
136
+ hasMore: false,
137
+ }));
138
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
139
+ });
140
+ it('should throw error when a result properties field is not an object', async () => {
141
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse({
142
+ results: [{ objectId: 1, properties: null }],
143
+ total: 1,
144
+ hasMore: false,
145
+ }));
146
+ await expect(fetchCrmSearch(createBasicRequest())).rejects.toThrow('Invalid response format');
147
+ });
148
+ });
149
+ describe('API integration', () => {
150
+ it('should call hsWorkerAPI.fetchCrmSearch with correct parameters and no options', async () => {
151
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse(createValidResponseData()));
152
+ const request = createBasicRequest({ pageSize: 50 });
153
+ await fetchCrmSearch(request);
154
+ expect(mockFetchCrmSearch).toHaveBeenCalledWith(request, undefined);
155
+ });
156
+ it('should return the cleanup function provided by the API response', async () => {
157
+ const mockCleanup = vi.fn();
158
+ mockFetchCrmSearch.mockResolvedValueOnce({
159
+ ok: true,
160
+ json: vi.fn().mockResolvedValue({
161
+ data: createValidResponseData(),
162
+ cleanup: mockCleanup,
163
+ }),
164
+ });
165
+ const result = await fetchCrmSearch(createBasicRequest());
166
+ expect(result.cleanup).toBe(mockCleanup);
167
+ result.cleanup();
168
+ expect(mockCleanup).toHaveBeenCalledTimes(1);
169
+ });
170
+ it('should provide a default cleanup function when none is provided', async () => {
171
+ mockFetchCrmSearch.mockResolvedValueOnce({
172
+ ok: true,
173
+ json: vi.fn().mockResolvedValue({ data: createValidResponseData() }),
174
+ });
175
+ const result = await fetchCrmSearch(createBasicRequest());
176
+ expect(typeof result.cleanup).toBe('function');
177
+ expect(() => result.cleanup()).not.toThrow();
178
+ });
179
+ });
180
+ describe('options handling', () => {
181
+ it('should pass formatting options to the underlying fetch function', async () => {
182
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse(createValidResponseData()));
183
+ const options = {
184
+ propertiesToFormat: ['firstname'],
185
+ formattingOptions: {
186
+ date: { format: 'MM/dd/yyyy' },
187
+ currency: { addSymbol: true },
188
+ },
189
+ };
190
+ await fetchCrmSearch(createBasicRequest(), options);
191
+ expect(mockFetchCrmSearch).toHaveBeenCalledWith(createBasicRequest(), options);
192
+ });
193
+ it('should handle propertiesToFormat set to "all"', async () => {
194
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse(createValidResponseData()));
195
+ const options = {
196
+ propertiesToFormat: 'all',
197
+ formattingOptions: { dateTime: { relative: true } },
198
+ };
199
+ await fetchCrmSearch(createBasicRequest(), options);
200
+ expect(mockFetchCrmSearch).toHaveBeenCalledWith(createBasicRequest(), options);
201
+ });
202
+ it('should preserve error handling with formatting options', async () => {
203
+ mockFetchCrmSearch.mockRejectedValueOnce(new Error('Network error'));
204
+ const options = {
205
+ propertiesToFormat: ['firstname'],
206
+ formattingOptions: { date: { format: 'MM/dd/yyyy' } },
207
+ };
208
+ await expect(fetchCrmSearch(createBasicRequest(), options)).rejects.toThrow('Network error');
209
+ expect(mockFetchCrmSearch).toHaveBeenCalledWith(createBasicRequest(), options);
210
+ });
211
+ it('should preserve response validation with formatting options', async () => {
212
+ mockFetchCrmSearch.mockResolvedValueOnce(createMockResponse('Invalid response') // data should be an object
213
+ );
214
+ await expect(fetchCrmSearch(createBasicRequest(), { propertiesToFormat: 'all' })).rejects.toThrow('Invalid response format');
215
+ });
216
+ });
217
+ });
@@ -1,4 +1,4 @@
1
- import { type PaginationResult } from '../../utils/pagination.ts';
1
+ import type { PaginationResult } from '../../shared/types/hooks.ts';
2
2
  import type { AssociationResult, FetchAssociationsRequest } from '../utils/fetchAssociations.ts';
3
3
  import { type FetchCrmPropertiesOptions } from '../utils/fetchCrmProperties.ts';
4
4
  export type UseAssociationsOptions = {
@@ -1,16 +1,15 @@
1
- import { useCallback, useMemo, useReducer } from 'react';
1
+ import { useCallback, useReducer, useState } from 'react';
2
2
  import { useFetchLifecycle } from "../../hooks/utils/useFetchLifecycle.js";
3
3
  import { createMockAwareHook, useStableValue, } from "../../internal/hook-utils.js";
4
4
  import { calculatePaginationFlags, DEFAULT_PAGE_SIZE, } from "../../utils/pagination.js";
5
5
  import { fetchAssociations } from "../utils/fetchAssociations.js";
6
- function createInitialState(pageSize) {
6
+ function createInitialState() {
7
7
  return {
8
8
  results: [],
9
9
  error: null,
10
10
  isLoading: true,
11
11
  isRefetching: false,
12
12
  currentPage: 1,
13
- pageSize,
14
13
  hasMore: false,
15
14
  currentOffset: undefined,
16
15
  nextOffset: undefined,
@@ -72,6 +71,7 @@ function associationsReducer(state, action) {
72
71
  ...state,
73
72
  currentPage: 1,
74
73
  results: [],
74
+ isLoading: true,
75
75
  hasMore: false,
76
76
  error: null,
77
77
  currentOffset: undefined,
@@ -109,7 +109,16 @@ const DEFAULT_OPTIONS = {};
109
109
  */
110
110
  function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
111
111
  const pageSize = config?.pageLength ?? DEFAULT_PAGE_SIZE;
112
- const [state, dispatch] = useReducer(associationsReducer, useMemo(() => createInitialState(pageSize), [pageSize]));
112
+ const [state, dispatch] = useReducer(associationsReducer, createInitialState());
113
+ /**
114
+ * Reset pagination when pageSize changes.
115
+ * Render-phase update (not useEffect) avoids committing a render with stale paginated results.
116
+ */
117
+ const [prevPageSize, setPrevPageSize] = useState(pageSize);
118
+ if (prevPageSize !== pageSize) {
119
+ setPrevPageSize(pageSize);
120
+ dispatch({ type: 'RESET' });
121
+ }
113
122
  const stableConfig = useStableValue(config);
114
123
  const stableOptions = useStableValue(options);
115
124
  // Pagination actions
@@ -191,7 +200,7 @@ function useAssociationsInternal(config, options = DEFAULT_OPTIONS) {
191
200
  hasNextPage: paginationFlags.hasNextPage,
192
201
  hasPreviousPage: paginationFlags.hasPreviousPage,
193
202
  currentPage: state.currentPage,
194
- pageSize: state.pageSize,
203
+ pageSize,
195
204
  nextPage,
196
205
  previousPage,
197
206
  reset,
@@ -28,9 +28,6 @@ export const fetchAssociations = async (request, options) => {
28
28
  if (result.error) {
29
29
  throw new Error(result.error);
30
30
  }
31
- if (!response.ok) {
32
- throw new Error(`Failed to fetch associations: ${response.statusText}`);
33
- }
34
31
  if (!isAssociationsResponse(result.data)) {
35
32
  throw new Error('Invalid response format');
36
33
  }
@@ -0,0 +1,2 @@
1
+ import type { FetchCrmSearchRequest, UseCrmSearchOptions, UseCrmSearchResult } from '../../shared/types/hooks.ts';
2
+ export declare function useCrmSearch(config: Omit<FetchCrmSearchRequest, 'after'>, options?: UseCrmSearchOptions): UseCrmSearchResult;
@@ -0,0 +1,206 @@
1
+ import { useCallback, useReducer, useState } from 'react';
2
+ import { useFetchLifecycle } from "../../hooks/utils/useFetchLifecycle.js";
3
+ import { useStableValue } from "../../internal/hook-utils.js";
4
+ import { calculatePaginationFlags, DEFAULT_PAGE_SIZE, } from "../../utils/pagination.js";
5
+ import { fetchCrmSearch } from "./utils/fetchCrmSearch.js";
6
+ function createInitialState() {
7
+ return {
8
+ results: [],
9
+ total: 0,
10
+ error: null,
11
+ isLoading: true,
12
+ isRefetching: false,
13
+ currentPage: 1,
14
+ hasMore: false,
15
+ currentCursor: undefined,
16
+ nextCursor: undefined,
17
+ offsetHistory: [],
18
+ };
19
+ }
20
+ function crmSearchReducer(state, action) {
21
+ switch (action.type) {
22
+ case 'FETCH_START':
23
+ return {
24
+ ...state,
25
+ isLoading: true,
26
+ error: null,
27
+ };
28
+ case 'FETCH_SUCCESS':
29
+ return {
30
+ ...state,
31
+ isLoading: false,
32
+ results: action.payload.results,
33
+ total: action.payload.total,
34
+ hasMore: action.payload.hasMore,
35
+ currentCursor: action.payload.currentCursor,
36
+ nextCursor: action.payload.nextCursor,
37
+ error: null,
38
+ };
39
+ case 'FETCH_ERROR':
40
+ return {
41
+ ...state,
42
+ isLoading: false,
43
+ error: action.payload,
44
+ results: [],
45
+ total: 0,
46
+ hasMore: false,
47
+ currentCursor: undefined,
48
+ nextCursor: undefined,
49
+ };
50
+ case 'NEXT_PAGE':
51
+ return {
52
+ ...state,
53
+ currentPage: state.currentPage + 1,
54
+ offsetHistory: state.currentCursor !== undefined
55
+ ? [...state.offsetHistory, state.currentCursor]
56
+ : state.offsetHistory,
57
+ currentCursor: state.nextCursor,
58
+ nextCursor: undefined,
59
+ };
60
+ case 'PREVIOUS_PAGE': {
61
+ const newPage = Math.max(1, state.currentPage - 1);
62
+ const newHistory = [...state.offsetHistory];
63
+ const previousCursor = newHistory.pop();
64
+ return {
65
+ ...state,
66
+ currentPage: newPage,
67
+ offsetHistory: newHistory,
68
+ currentCursor: previousCursor,
69
+ nextCursor: undefined,
70
+ };
71
+ }
72
+ case 'RESET':
73
+ return {
74
+ ...state,
75
+ currentPage: 1,
76
+ results: [],
77
+ total: 0,
78
+ isLoading: true,
79
+ hasMore: false,
80
+ error: null,
81
+ currentCursor: undefined,
82
+ nextCursor: undefined,
83
+ offsetHistory: [],
84
+ };
85
+ case 'REFETCH_START':
86
+ return {
87
+ ...state,
88
+ isRefetching: true,
89
+ error: null,
90
+ };
91
+ case 'REFETCH_SUCCESS':
92
+ return {
93
+ ...state,
94
+ isRefetching: false,
95
+ results: action.payload.results,
96
+ total: action.payload.total,
97
+ hasMore: action.payload.hasMore,
98
+ nextCursor: action.payload.nextCursor,
99
+ error: null,
100
+ };
101
+ case 'REFETCH_ERROR':
102
+ return {
103
+ ...state,
104
+ isRefetching: false,
105
+ error: action.payload,
106
+ };
107
+ default:
108
+ return state;
109
+ }
110
+ }
111
+ const DEFAULT_OPTIONS = {};
112
+ export function useCrmSearch(config, options = DEFAULT_OPTIONS) {
113
+ const pageSize = config?.pageSize ?? DEFAULT_PAGE_SIZE;
114
+ const [state, dispatch] = useReducer(crmSearchReducer, createInitialState());
115
+ // Reset pagination when pageSize changes.
116
+ // Render-phase update (not useEffect) avoids committing a render with stale paginated results.
117
+ const [prevPageSize, setPrevPageSize] = useState(pageSize);
118
+ if (prevPageSize !== pageSize) {
119
+ setPrevPageSize(pageSize);
120
+ dispatch({ type: 'RESET' });
121
+ }
122
+ const stableConfig = useStableValue(config);
123
+ const stableOptions = useStableValue(options);
124
+ const nextPage = useCallback(() => {
125
+ const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
126
+ if (paginationFlags.hasNextPage) {
127
+ dispatch({ type: 'NEXT_PAGE' });
128
+ }
129
+ }, [state.currentPage, state.hasMore]);
130
+ const previousPage = useCallback(() => {
131
+ const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
132
+ if (paginationFlags.hasPreviousPage) {
133
+ dispatch({ type: 'PREVIOUS_PAGE' });
134
+ }
135
+ }, [state.currentPage, state.hasMore]);
136
+ const reset = useCallback(() => {
137
+ const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
138
+ if (paginationFlags.hasPreviousPage) {
139
+ dispatch({ type: 'RESET' });
140
+ }
141
+ }, [state.currentPage, state.hasMore]);
142
+ const { refetch } = useFetchLifecycle({
143
+ fetchFn: () => {
144
+ const request = {
145
+ objectType: stableConfig?.objectType,
146
+ properties: stableConfig?.properties,
147
+ query: stableConfig?.query,
148
+ filterGroups: stableConfig?.filterGroups,
149
+ sorts: stableConfig?.sorts,
150
+ pageSize,
151
+ after: state.currentCursor,
152
+ };
153
+ return fetchCrmSearch(request, {
154
+ propertiesToFormat: stableOptions.propertiesToFormat,
155
+ formattingOptions: stableOptions.formattingOptions,
156
+ });
157
+ },
158
+ callbacks: {
159
+ onStart: ({ isRefetch }) => dispatch({ type: isRefetch ? 'REFETCH_START' : 'FETCH_START' }),
160
+ onSuccess: (data, { isRefetch }) => dispatch(isRefetch
161
+ ? {
162
+ type: 'REFETCH_SUCCESS',
163
+ payload: {
164
+ results: data.results,
165
+ total: data.total,
166
+ hasMore: data.hasMore,
167
+ nextCursor: data.after,
168
+ },
169
+ }
170
+ : {
171
+ type: 'FETCH_SUCCESS',
172
+ payload: {
173
+ results: data.results,
174
+ total: data.total,
175
+ hasMore: data.hasMore,
176
+ nextCursor: data.after,
177
+ currentCursor: state.currentCursor,
178
+ },
179
+ }),
180
+ onError: (error, { isRefetch }) => dispatch({
181
+ type: isRefetch ? 'REFETCH_ERROR' : 'FETCH_ERROR',
182
+ payload: error,
183
+ }),
184
+ },
185
+ deps: [stableConfig, stableOptions, state.currentCursor],
186
+ defaultErrorMessage: 'Failed to fetch CRM search results',
187
+ });
188
+ const paginationFlags = calculatePaginationFlags(state.currentPage, state.hasMore);
189
+ return {
190
+ results: state.results,
191
+ total: state.total,
192
+ error: state.error,
193
+ isLoading: state.isLoading,
194
+ isRefetching: state.isRefetching,
195
+ refetch,
196
+ pagination: {
197
+ hasNextPage: paginationFlags.hasNextPage,
198
+ hasPreviousPage: paginationFlags.hasPreviousPage,
199
+ currentPage: state.currentPage,
200
+ pageSize,
201
+ nextPage,
202
+ previousPage,
203
+ reset,
204
+ },
205
+ };
206
+ }
@@ -0,0 +1,6 @@
1
+ import type { CrmSearchResponse, FetchCrmSearchRequest, FetchHookOptions } from '../../../shared/types/hooks.ts';
2
+ export interface FetchCrmSearchResult {
3
+ data: CrmSearchResponse;
4
+ cleanup: () => void;
5
+ }
6
+ export declare const fetchCrmSearch: (request: FetchCrmSearchRequest, options?: FetchHookOptions) => Promise<FetchCrmSearchResult>;
@@ -0,0 +1,39 @@
1
+ import { getWorkerGlobals } from "../../../internal/global-utils.js";
2
+ function isCrmSearchResponse(data) {
3
+ const d = data;
4
+ if (d === null ||
5
+ typeof d !== 'object' ||
6
+ !Array.isArray(d.results) ||
7
+ typeof d.total !== 'number' ||
8
+ typeof d.hasMore !== 'boolean') {
9
+ return false;
10
+ }
11
+ return d.results.every((result) => result !== null &&
12
+ typeof result === 'object' &&
13
+ typeof result.objectId === 'number' &&
14
+ result.properties !== null &&
15
+ typeof result.properties === 'object');
16
+ }
17
+ export const fetchCrmSearch = async (request, options) => {
18
+ let response;
19
+ let result;
20
+ try {
21
+ response = await getWorkerGlobals().hsWorkerAPI.fetchCrmSearch(request, options);
22
+ result = await response.json();
23
+ }
24
+ catch (error) {
25
+ throw error instanceof Error
26
+ ? error
27
+ : new Error('Failed to fetch CRM search results: Unknown error');
28
+ }
29
+ if (result.error) {
30
+ throw new Error(result.error);
31
+ }
32
+ if (!isCrmSearchResponse(result.data)) {
33
+ throw new Error('Invalid response format');
34
+ }
35
+ return {
36
+ data: result.data,
37
+ cleanup: result.cleanup || (() => { }),
38
+ };
39
+ };
@@ -1,4 +1,5 @@
1
1
  export { useCrmProperties } from '../crm/hooks/useCrmProperties.ts';
2
2
  export { useAssociations } from '../crm/hooks/useAssociations.ts';
3
+ export { useCrmSearch } from './hooks/useCrmSearch.ts';
3
4
  export { Iframe, MediaObject, Stack2, Center, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput, } from '../shared/remoteComponents.tsx';
4
5
  export type * from '../shared/types/experimental.ts';
@@ -1,3 +1,4 @@
1
1
  export { useCrmProperties } from "../crm/hooks/useCrmProperties.js";
2
2
  export { useAssociations } from "../crm/hooks/useAssociations.js";
3
+ export { useCrmSearch } from "./hooks/useCrmSearch.js";
3
4
  export { Iframe, MediaObject, Stack2, Center, GridItem, Grid, SettingsView, ExpandableText, Popover, FileInput, } from "../shared/remoteComponents.js";
@@ -70,6 +70,14 @@ export interface SortedTableHeaderProps extends BaseTableHeaderProps {
70
70
  * @category Component Props
71
71
  */
72
72
  export type TableHeaderProps = SortedTableHeaderProps | UnSortedTableHeaderProps;
73
+ /**
74
+ * Controls row height and vertical padding for every cell in the table.
75
+ *
76
+ * - `'default'`: generous row height and padding around content.
77
+ * - `'condensed'`: moderate row height and padding around content.
78
+ * - `'compact'`: no min row height, minimal padding. Rows collapse to content height.
79
+ */
80
+ export type TableDensity = 'default' | 'condensed' | 'compact';
73
81
  interface BaseTableProps extends BaseComponentProps {
74
82
  /**
75
83
  * When set to `false`, the table will not include borders.
@@ -83,6 +91,14 @@ interface BaseTableProps extends BaseComponentProps {
83
91
  * @defaultValue `false`
84
92
  */
85
93
  flush?: boolean;
94
+ /**
95
+ * Sets row height and vertical padding for all header, body, and footer cells.
96
+ * Use `'condensed'` for denser data views and `'compact'` when rows should be
97
+ * sized only to their content.
98
+ *
99
+ * @defaultValue `'default'`
100
+ */
101
+ density?: TableDensity;
86
102
  /**
87
103
  * Sets the content that will render inside the component. This prop is passed implicitly by providing sub-components. The children should be one of Table's subcomponents.
88
104
  */
@@ -18,6 +18,7 @@ export interface UserContext {
18
18
  roles: string[];
19
19
  teams: Team[];
20
20
  locale?: string;
21
+ language?: string;
21
22
  permissions: Permission[];
22
23
  }
23
24
  /** @ignore */
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { ReactionsHandler } from './reactions.ts';
3
3
  import type { AllDistances, BaseComponentProps, ExtensionEvent } from './shared.ts';
4
+ export type { CrmSearchFilter, CrmSearchFilterGroup, CrmSearchResponse, CrmSearchResultItem, CrmSearchSort, FetchCrmSearchRequest, UseCrmSearchOptions, UseCrmSearchResult, FormattingOptions, } from './hooks.ts';
4
5
  /**
5
6
  * @ignore
6
7
  * @experimental do not use in production
@@ -233,4 +234,3 @@ export interface BaseInputProps<T = string, V = string> {
233
234
  */
234
235
  onFocus?: (value: V) => void;
235
236
  }
236
- export {};