@hubspot/ui-extensions 0.8.52 → 0.8.54

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.
@@ -0,0 +1,83 @@
1
+ // Set up the mock before importing the module
2
+ const mockFetchCrmProperties = jest.fn();
3
+ const mockSelf = {
4
+ fetchCrmProperties: mockFetchCrmProperties,
5
+ };
6
+ // Mock the global self object
7
+ Object.defineProperty(global, 'self', {
8
+ value: mockSelf,
9
+ writable: true,
10
+ });
11
+ import { fetchCrmProperties, } from '../../../experimental/crm/fetchCrmProperties';
12
+ describe('fetchCrmProperties', () => {
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+ it('successfully fetches CRM properties', async () => {
17
+ const mockApiResponse = {
18
+ firstname: 'Test value for firstname',
19
+ lastname: 'Test value for lastname',
20
+ };
21
+ const mockResponse = {
22
+ ok: true,
23
+ json: jest.fn().mockResolvedValue(mockApiResponse),
24
+ };
25
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
26
+ const propertyNames = ['firstname', 'lastname'];
27
+ const result = await fetchCrmProperties(propertyNames, jest.fn());
28
+ expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames, expect.any(Function));
29
+ expect(result).toEqual({
30
+ firstname: 'Test value for firstname',
31
+ lastname: 'Test value for lastname',
32
+ });
33
+ });
34
+ it('throws an error when response is not OK', async () => {
35
+ const mockResponse = {
36
+ ok: false,
37
+ statusText: 'Not Found',
38
+ };
39
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
40
+ const propertyNames = ['firstname'];
41
+ await expect(fetchCrmProperties(propertyNames, jest.fn())).rejects.toThrow('Failed to fetch CRM properties: Not Found');
42
+ });
43
+ it('throws an error when fetch fails', async () => {
44
+ mockFetchCrmProperties.mockRejectedValue(new Error('Network error'));
45
+ const propertyNames = ['firstname'];
46
+ await expect(fetchCrmProperties(propertyNames, jest.fn())).rejects.toThrow('Network error');
47
+ });
48
+ it('throws an error if the response is not an object', async () => {
49
+ const mockApiResponse = 'Invalid response';
50
+ const mockResponse = {
51
+ ok: true,
52
+ json: jest.fn().mockResolvedValue(mockApiResponse),
53
+ };
54
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
55
+ const propertyNames = ['firstname'];
56
+ await expect(fetchCrmProperties(propertyNames, jest.fn())).rejects.toThrow('Invalid response format');
57
+ });
58
+ it('passes the propertiesUpdatedCallback and allows it to be called', async () => {
59
+ let capturedCallback;
60
+ const mockApiResponse = {
61
+ firstname: 'Initial',
62
+ lastname: 'Initial',
63
+ };
64
+ const mockResponse = {
65
+ ok: true,
66
+ json: jest.fn().mockResolvedValue(mockApiResponse),
67
+ };
68
+ mockFetchCrmProperties.mockImplementation((propertyNames, callback) => {
69
+ capturedCallback = callback;
70
+ return Promise.resolve(mockResponse);
71
+ });
72
+ const propertyNames = ['firstname', 'lastname'];
73
+ const mockCallback = jest.fn();
74
+ await fetchCrmProperties(propertyNames, mockCallback);
75
+ expect(typeof capturedCallback).toBe('function');
76
+ // Simulate the callback being called with new properties
77
+ const updatedProps = { firstname: 'Updated', lastname: 'Updated' };
78
+ if (capturedCallback) {
79
+ capturedCallback(updatedProps);
80
+ }
81
+ expect(mockCallback).toHaveBeenCalledWith(updatedProps);
82
+ });
83
+ });
@@ -0,0 +1,95 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { useCrmProperties } from '../../../experimental/hooks/useCrmProperties';
3
+ // Mock the logger module
4
+ jest.mock('../../../logger', () => ({
5
+ logger: {
6
+ debug: jest.fn(),
7
+ info: jest.fn(),
8
+ warn: jest.fn(),
9
+ error: jest.fn(),
10
+ },
11
+ }));
12
+ // Mock the fetchCrmProperties module
13
+ jest.mock('../../../experimental/crm/fetchCrmProperties');
14
+ const mockFetchCrmProperties = jest.fn();
15
+ import * as fetchCrmPropertiesModule from '../../../experimental/crm/fetchCrmProperties';
16
+ fetchCrmPropertiesModule.fetchCrmProperties = mockFetchCrmProperties;
17
+ describe('useCrmProperties', () => {
18
+ let originalError;
19
+ beforeAll(() => {
20
+ // Suppress React act() warning coming from @testing-library/react
21
+ originalError = console.error;
22
+ console.error = (...args) => {
23
+ if (args[0]?.includes('ReactDOMTestUtils.act'))
24
+ return;
25
+ originalError.call(console, ...args);
26
+ };
27
+ });
28
+ beforeEach(() => {
29
+ // Reset the mock before each test
30
+ mockFetchCrmProperties.mockReset();
31
+ });
32
+ afterAll(() => {
33
+ console.error = originalError;
34
+ });
35
+ it('should return the initial values for properties, error, and isLoading', async () => {
36
+ const { result } = renderHook(() => useCrmProperties(['firstname', 'lastname']));
37
+ await waitFor(() => {
38
+ expect(result.current.properties).toEqual({});
39
+ expect(result.current.error).toBeNull();
40
+ expect(result.current.isLoading).toBe(true);
41
+ });
42
+ });
43
+ it('should successfully fetch and return CRM properties', async () => {
44
+ mockFetchCrmProperties.mockResolvedValue({
45
+ firstname: 'Test value for firstname',
46
+ lastname: 'Test value for lastname',
47
+ });
48
+ const propertyNames = ['firstname', 'lastname'];
49
+ const { result } = renderHook(() => useCrmProperties(propertyNames));
50
+ await waitFor(() => {
51
+ expect(result.current.properties).toEqual({
52
+ firstname: 'Test value for firstname',
53
+ lastname: 'Test value for lastname',
54
+ });
55
+ expect(result.current.error).toBeNull();
56
+ expect(result.current.isLoading).toBe(false);
57
+ });
58
+ });
59
+ it('should handle fetch errors correctly', async () => {
60
+ const errorMessage = 'Failed to fetch CRM properties';
61
+ mockFetchCrmProperties.mockRejectedValue(new Error(errorMessage));
62
+ const propertyNames = ['firstname'];
63
+ const { result } = renderHook(() => useCrmProperties(propertyNames));
64
+ await waitFor(() => {
65
+ expect(result.current.error).toBeInstanceOf(Error);
66
+ expect(result.current.error?.message).toBe(errorMessage);
67
+ expect(result.current.properties).toEqual({});
68
+ expect(result.current.isLoading).toBe(false);
69
+ });
70
+ });
71
+ it('should update properties when propertiesUpdatedCallback is called', async () => {
72
+ // Capture the callback so we can simulate an external update to CRM properties
73
+ let capturedCallback;
74
+ mockFetchCrmProperties.mockImplementation((_propertyNames, propertiesUpdatedCallback) => {
75
+ capturedCallback = propertiesUpdatedCallback;
76
+ return { firstname: 'Initial', lastname: 'Initial' };
77
+ });
78
+ const propertyNames = ['firstname', 'lastname'];
79
+ const { result } = renderHook(() => useCrmProperties(propertyNames));
80
+ await waitFor(() => {
81
+ expect(result.current.properties).toEqual({
82
+ firstname: 'Initial',
83
+ lastname: 'Initial',
84
+ });
85
+ expect(result.current.isLoading).toBe(false);
86
+ });
87
+ const updatedProperties = { firstname: 'Updated', lastname: 'Updated' };
88
+ await waitFor(() => {
89
+ if (capturedCallback) {
90
+ capturedCallback(updatedProperties);
91
+ expect(result.current.properties).toEqual(updatedProperties);
92
+ }
93
+ });
94
+ });
95
+ });
@@ -733,7 +733,7 @@ export declare const LineChart: "LineChart" & {
733
733
  *
734
734
  * **Links:**
735
735
  * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/tabs Documentation}
736
- * - {@link https://github.com/HubSpot/ui-extensions-examples/tree/main/tabs-example Tabs Example}
736
+ * - {@link https://github.com/hubspotdev/uie-tabbed-product-carousel Tabs Example}
737
737
  */
738
738
  export declare const Tabs: "Tabs" & {
739
739
  readonly type?: "Tabs" | undefined;
@@ -752,7 +752,7 @@ export declare const Tabs: "Tabs" & {
752
752
  *
753
753
  * **Links:**
754
754
  * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/tabs Documentation}
755
- * - {@link https://github.com/HubSpot/ui-extensions-examples/tree/main/tabs-example Tabs Example}
755
+ * - {@link https://github.com/hubspotdev/uie-tabbed-product-carousel Tabs Example}
756
756
  */
757
757
  export declare const Tab: "Tab" & {
758
758
  readonly type?: "Tab" | undefined;
@@ -493,7 +493,7 @@ export const LineChart = createRemoteReactComponent('LineChart');
493
493
  *
494
494
  * **Links:**
495
495
  * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/tabs Documentation}
496
- * - {@link https://github.com/HubSpot/ui-extensions-examples/tree/main/tabs-example Tabs Example}
496
+ * - {@link https://github.com/hubspotdev/uie-tabbed-product-carousel Tabs Example}
497
497
  */
498
498
  export const Tabs = createRemoteReactComponent('Tabs');
499
499
  /**
@@ -508,7 +508,7 @@ export const Tabs = createRemoteReactComponent('Tabs');
508
508
  *
509
509
  * **Links:**
510
510
  * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/tabs Documentation}
511
- * - {@link https://github.com/HubSpot/ui-extensions-examples/tree/main/tabs-example Tabs Example}
511
+ * - {@link https://github.com/hubspotdev/uie-tabbed-product-carousel Tabs Example}
512
512
  */
513
513
  export const Tab = createRemoteReactComponent('Tab');
514
514
  /**
@@ -0,0 +1,4 @@
1
+ export type CrmPropertiesResponse = {
2
+ [key: string]: string;
3
+ };
4
+ export declare const fetchCrmProperties: (propertyNames: string[], propertiesUpdatedCallback: (properties: Record<string, string>) => void) => Promise<Record<string, string>>;
@@ -0,0 +1,32 @@
1
+ // Type guard for CrmPropertiesResponse
2
+ function isCrmPropertiesResponse(data) {
3
+ if (
4
+ // Confirm the data is a defined object
5
+ data === null ||
6
+ typeof data !== 'object' ||
7
+ // Confirm all keys and values are strings
8
+ !Object.keys(data).every((key) => typeof key === 'string' && typeof data[key] === 'string')) {
9
+ return false;
10
+ }
11
+ return true;
12
+ }
13
+ export const fetchCrmProperties = async (propertyNames, propertiesUpdatedCallback) => {
14
+ try {
15
+ // eslint-disable-next-line hubspot-dev/no-confusing-browser-globals
16
+ const response = await self.fetchCrmProperties(propertyNames, propertiesUpdatedCallback);
17
+ if (!response.ok) {
18
+ throw new Error(`Failed to fetch CRM properties: ${response.statusText}`);
19
+ }
20
+ const data = await response.json();
21
+ if (!isCrmPropertiesResponse(data)) {
22
+ throw new Error('Invalid response format');
23
+ }
24
+ return data;
25
+ }
26
+ catch (error) {
27
+ if (error instanceof Error) {
28
+ throw error;
29
+ }
30
+ throw new Error('Failed to fetch CRM properties: Unknown error');
31
+ }
32
+ };
@@ -1,9 +1,11 @@
1
- import { CrmHostActions } from 'types';
1
+ export interface CrmPropertiesState {
2
+ properties: Record<string, string>;
3
+ error: Error | null;
4
+ isLoading: boolean;
5
+ }
2
6
  /**
3
7
  * A hook for using and managing CRM properties.
4
8
  *
5
9
  * @experimental This hook is experimental and might change or be removed in future versions.
6
10
  */
7
- export declare function useCrmProperties(actions: CrmHostActions, properties: string[], callback: (properties: Record<string, string>) => void, onUpdate?: (properties: Record<string, string>) => void | Promise<void> | undefined | null): {
8
- onPropertyChanged: import("types").refreshObjectPropertiesAction;
9
- };
11
+ export declare function useCrmProperties(propertyNames: string[]): CrmPropertiesState;
@@ -1,22 +1,49 @@
1
- import { useEffect } from 'react';
2
- import { createPropertyManager } from '../crm/propertyManager';
1
+ import { useEffect, useState } from 'react';
3
2
  import { logger } from '../../logger';
3
+ import { fetchCrmProperties } from '../crm/fetchCrmProperties';
4
4
  /**
5
5
  * A hook for using and managing CRM properties.
6
6
  *
7
7
  * @experimental This hook is experimental and might change or be removed in future versions.
8
8
  */
9
- export function useCrmProperties(actions, properties, callback, onUpdate) {
9
+ export function useCrmProperties(propertyNames) {
10
+ const [properties, setProperties] = useState({});
11
+ const [isLoading, setIsLoading] = useState(true);
12
+ const [error, setError] = useState(null);
13
+ // Log experimental warning once on mount
10
14
  useEffect(() => {
11
15
  logger.warn('useCrmProperties is an experimental hook and might change or be removed in the future.');
12
16
  }, []);
17
+ const propertiesUpdatedCallback = (newProperties) => {
18
+ setProperties(newProperties);
19
+ };
20
+ // Fetch the properties
13
21
  useEffect(() => {
14
- const propertyManager = createPropertyManager(actions);
15
- return propertyManager.initializePropertyMonitoring(properties, (props) => callback(props), (props) => callback(props));
16
- }, [actions, properties, callback, onUpdate]);
17
- // Return the refresh function
18
- const propertyManager = createPropertyManager(actions);
22
+ (async () => {
23
+ try {
24
+ const propertyData = await fetchCrmProperties(propertyNames, propertiesUpdatedCallback);
25
+ setProperties(propertyData);
26
+ setError(null);
27
+ }
28
+ catch (err) {
29
+ const errorData = err instanceof Error
30
+ ? err
31
+ : new Error('Failed to fetch CRM properties');
32
+ setError(errorData);
33
+ setProperties({});
34
+ }
35
+ finally {
36
+ setIsLoading(false);
37
+ }
38
+ })().catch((err) => {
39
+ setError(err);
40
+ setProperties({});
41
+ setIsLoading(false);
42
+ });
43
+ }, [propertyNames]);
19
44
  return {
20
- onPropertyChanged: propertyManager.refreshProperties,
45
+ properties,
46
+ error,
47
+ isLoading,
21
48
  };
22
49
  }
@@ -54,4 +54,16 @@ declare const SettingsView: "SettingsView" & {
54
54
  readonly props?: experimentalTypes.SettingsViewProps | undefined;
55
55
  readonly children?: true | undefined;
56
56
  } & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"SettingsView", experimentalTypes.SettingsViewProps, true>>;
57
- export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, };
57
+ /**
58
+ * The `ExpandableText` component renders a text that can be expanded or collapsed based on a maximum height.
59
+ *
60
+ * **Links:**
61
+ *
62
+ * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/expandable-text ExpandableText Docs}
63
+ */
64
+ declare const ExpandableText: "ExpandableText" & {
65
+ readonly type?: "ExpandableText" | undefined;
66
+ readonly props?: experimentalTypes.ExpandableTextProps | undefined;
67
+ readonly children?: true | undefined;
68
+ } & import("@remote-ui/react").ReactComponentTypeFromRemoteComponentType<import("@remote-ui/types").RemoteComponentType<"ExpandableText", experimentalTypes.ExpandableTextProps, true>>;
69
+ export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, };
@@ -19,4 +19,12 @@ const Grid = createRemoteReactComponent('Grid');
19
19
  /** @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. */
20
20
  const GridItem = createRemoteReactComponent('GridItem');
21
21
  const SettingsView = createRemoteReactComponent('SettingsView');
22
- export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, };
22
+ /**
23
+ * The `ExpandableText` component renders a text that can be expanded or collapsed based on a maximum height.
24
+ *
25
+ * **Links:**
26
+ *
27
+ * - {@link https://developers.hubspot.com/docs/reference/ui-components/standard-components/expandable-text ExpandableText Docs}
28
+ */
29
+ const ExpandableText = createRemoteReactComponent('ExpandableText');
30
+ export { Iframe, MediaObject, Inline, Stack2, Center, SimpleGrid, GridItem, Grid, SettingsView, ExpandableText, };
@@ -111,4 +111,15 @@ export interface SettingsViewProps {
111
111
  */
112
112
  onCancel?: ReactionsHandler<ExtensionEvent>;
113
113
  }
114
+ /**
115
+ * @ignore
116
+ * @experimental do not use in production
117
+ */
118
+ export interface ExpandableTextProps {
119
+ children: ReactNode;
120
+ maxHeight?: number;
121
+ expandButtonText?: string;
122
+ collapseButtonText?: string;
123
+ expanded?: boolean;
124
+ }
114
125
  export {};