@hubspot/ui-extensions 0.8.52 → 0.8.53

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,58 @@
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);
28
+ expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames);
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)).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)).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)).rejects.toThrow('Invalid response format');
57
+ });
58
+ });
@@ -0,0 +1,71 @@
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
+ });
@@ -0,0 +1,4 @@
1
+ export type CrmPropertiesResponse = {
2
+ [key: string]: string;
3
+ };
4
+ export declare const fetchCrmProperties: (propertyNames: string[]) => 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) => {
14
+ try {
15
+ // eslint-disable-next-line hubspot-dev/no-confusing-browser-globals
16
+ const response = await self.fetchCrmProperties(propertyNames);
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,46 @@
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
+ // Fetch the properties
13
18
  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);
19
+ (async () => {
20
+ try {
21
+ const propertyData = await fetchCrmProperties(propertyNames);
22
+ setProperties(propertyData);
23
+ setError(null);
24
+ }
25
+ catch (err) {
26
+ const errorData = err instanceof Error
27
+ ? err
28
+ : new Error('Failed to fetch CRM properties');
29
+ setError(errorData);
30
+ setProperties({});
31
+ }
32
+ finally {
33
+ setIsLoading(false);
34
+ }
35
+ })().catch((err) => {
36
+ setError(err);
37
+ setProperties({});
38
+ setIsLoading(false);
39
+ });
40
+ }, [propertyNames]);
19
41
  return {
20
- onPropertyChanged: propertyManager.refreshProperties,
42
+ properties,
43
+ error,
44
+ isLoading,
21
45
  };
22
46
  }
@@ -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 {};