@hubspot/ui-extensions 0.12.3 → 0.13.0

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 (84) hide show
  1. package/dist/__tests__/crm/hooks/useAssociations.spec.js +28 -0
  2. package/dist/__tests__/crm/utils/fetchAssociations.spec.js +2 -1
  3. package/dist/__tests__/hooks/useDebounce.spec.js +123 -0
  4. package/dist/__tests__/hooks/utils/useFetchLifecycle.spec.js +324 -0
  5. package/dist/__tests__/internal/hook-utils.spec.d.ts +1 -0
  6. package/dist/__tests__/internal/hook-utils.spec.js +17 -0
  7. package/dist/__tests__/test-d/extension-points.test-d.js +1 -0
  8. package/dist/crm/hooks/useAssociations.d.ts +4 -12
  9. package/dist/crm/hooks/useAssociations.js +46 -138
  10. package/dist/crm/hooks/useCrmProperties.js +29 -125
  11. package/dist/crm/utils/fetchAssociations.d.ts +0 -8
  12. package/dist/crm/utils/fetchAssociations.js +0 -10
  13. package/dist/hooks/useDebounce.d.ts +19 -0
  14. package/dist/hooks/useDebounce.js +32 -0
  15. package/dist/hooks/utils/useFetchLifecycle.d.ts +35 -0
  16. package/dist/hooks/utils/useFetchLifecycle.js +103 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/internal/hook-utils.d.ts +13 -6
  20. package/dist/internal/hook-utils.js +24 -10
  21. package/dist/pages/components/index.d.ts +1 -0
  22. package/dist/pages/components/index.js +1 -0
  23. package/dist/{shared/types/pages → pages}/components/page-routes.d.ts +6 -39
  24. package/dist/pages/components/page-routes.js +62 -0
  25. package/dist/pages/create-page-router.d.ts +33 -0
  26. package/dist/pages/create-page-router.js +121 -0
  27. package/dist/pages/create-page-router.test.d.ts +1 -0
  28. package/dist/pages/create-page-router.test.js +296 -0
  29. package/dist/pages/hooks.d.ts +7 -0
  30. package/dist/pages/hooks.js +14 -0
  31. package/dist/pages/index.d.ts +6 -0
  32. package/dist/pages/index.js +4 -0
  33. package/dist/pages/internal/app-page-route-context.d.ts +16 -0
  34. package/dist/pages/internal/app-page-route-context.js +12 -0
  35. package/dist/pages/internal/convert-page-routes-react-elements.d.ts +9 -0
  36. package/dist/pages/internal/convert-page-routes-react-elements.js +138 -0
  37. package/dist/pages/internal/page-router-internal-types.d.ts +40 -0
  38. package/dist/pages/internal/page-router-internal-types.js +5 -0
  39. package/dist/pages/internal/trie-router.d.ts +31 -0
  40. package/dist/pages/internal/trie-router.js +141 -0
  41. package/dist/pages/internal/trie-router.test.d.ts +1 -0
  42. package/dist/pages/internal/trie-router.test.js +263 -0
  43. package/dist/pages/internal/useAppPageLocation.d.ts +1 -0
  44. package/dist/pages/internal/useAppPageLocation.js +13 -0
  45. package/dist/pages/types.d.ts +28 -0
  46. package/dist/pages/types.js +1 -0
  47. package/dist/shared/remoteComponents.d.ts +24 -0
  48. package/dist/shared/remoteComponents.js +28 -0
  49. package/dist/shared/types/actions.d.ts +12 -2
  50. package/dist/shared/types/components/button.d.ts +2 -2
  51. package/dist/shared/types/components/image.d.ts +2 -2
  52. package/dist/shared/types/components/inputs.d.ts +7 -1
  53. package/dist/shared/types/components/link.d.ts +2 -2
  54. package/dist/shared/types/context.d.ts +5 -0
  55. package/dist/shared/types/crm.d.ts +4 -0
  56. package/dist/shared/types/extension-points.d.ts +9 -3
  57. package/dist/shared/types/extension-points.js +1 -0
  58. package/dist/shared/types/pages/components/index.d.ts +3 -1
  59. package/dist/shared/types/pages/components/page-breadcrumbs.d.ts +34 -0
  60. package/dist/shared/types/pages/components/page-breadcrumbs.js +1 -0
  61. package/dist/shared/types/pages/components/page-header.d.ts +82 -0
  62. package/dist/shared/types/pages/components/page-header.js +1 -0
  63. package/dist/shared/types/pages/components/page-link.d.ts +4 -2
  64. package/dist/shared/types/pages/components/page-title.d.ts +12 -0
  65. package/dist/shared/types/pages/components/page-title.js +1 -0
  66. package/dist/shared/types/shared.d.ts +8 -0
  67. package/dist/shared/types/worker-globals.d.ts +3 -12
  68. package/dist/testing/__tests__/createRenderer.spec.js +1 -1
  69. package/dist/testing/internal/mocks/index.d.ts +14 -6
  70. package/dist/testing/internal/mocks/index.js +19 -5
  71. package/dist/testing/internal/mocks/mock-app-page-location.d.ts +7 -0
  72. package/dist/testing/internal/mocks/mock-app-page-location.js +35 -0
  73. package/dist/testing/internal/mocks/mock-extension-point-api.js +21 -1
  74. package/dist/testing/internal/mocks/mock-hooks.js +12 -7
  75. package/dist/testing/render.js +7 -6
  76. package/dist/testing/types.d.ts +19 -3
  77. package/dist/utils/pagination.d.ts +17 -0
  78. package/dist/utils/pagination.js +10 -0
  79. package/package.json +2 -2
  80. package/dist/experimental/pages/index.d.ts +0 -4
  81. package/dist/experimental/pages/index.js +0 -3
  82. package/dist/shared/types/pages/app-pages-types.d.ts +0 -75
  83. /package/dist/{shared/types/pages/app-pages-types.js → __tests__/hooks/useDebounce.spec.d.ts} +0 -0
  84. /package/dist/{shared/types/pages/components/page-routes.js → __tests__/hooks/utils/useFetchLifecycle.spec.d.ts} +0 -0
@@ -0,0 +1,7 @@
1
+ import type { MatchedPageRoute } from './types.ts';
2
+ /**
3
+ * The hook that provides the current page route to the component.
4
+ *
5
+ * @returns The current page route.
6
+ */
7
+ export declare const usePageRoute: () => MatchedPageRoute;
@@ -0,0 +1,14 @@
1
+ import { useContext } from 'react';
2
+ import { AppPageRouteContext } from "./internal/app-page-route-context.js";
3
+ /**
4
+ * The hook that provides the current page route to the component.
5
+ *
6
+ * @returns The current page route.
7
+ */
8
+ export const usePageRoute = () => {
9
+ const matchedPageRoute = useContext(AppPageRouteContext);
10
+ if (!matchedPageRoute) {
11
+ throw new Error('usePageRoute must be used within a <PageRouter> component produced by createPageRouter');
12
+ }
13
+ return matchedPageRoute;
14
+ };
@@ -0,0 +1,6 @@
1
+ export * from './components/index.ts';
2
+ export * from './create-page-router.tsx';
3
+ export * from './hooks.ts';
4
+ export type * from './types.ts';
5
+ export type * from '../shared/types/pages/components/index.ts';
6
+ export { PageBreadcrumbs, PageHeader, PageLink, PageTitle, } from '../shared/remoteComponents.tsx';
@@ -0,0 +1,4 @@
1
+ export * from "./components/index.js";
2
+ export * from "./create-page-router.js";
3
+ export * from "./hooks.js";
4
+ export { PageBreadcrumbs, PageHeader, PageLink, PageTitle, } from "../shared/remoteComponents.js";
@@ -0,0 +1,16 @@
1
+ import type { MatchedPageRoute } from '../types.ts';
2
+ export declare const AppPageRouteContext: import("react").Context<MatchedPageRoute | null>;
3
+ interface AppPageRouteProviderProps {
4
+ children: React.ReactNode;
5
+ pageRoute: MatchedPageRoute;
6
+ }
7
+ /**
8
+ * The provider that provides the current page route to the component.
9
+ * This component is used internally by the produced page router components.
10
+ *
11
+ * @param children - The children to render.
12
+ * @param pageRoute - The current page route.
13
+ * @returns The provider.
14
+ */
15
+ export declare const AppPageRouteProvider: ({ children, pageRoute, }: AppPageRouteProviderProps) => import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext } from 'react';
3
+ export const AppPageRouteContext = createContext(null);
4
+ /**
5
+ * The provider that provides the current page route to the component.
6
+ * This component is used internally by the produced page router components.
7
+ *
8
+ * @param children - The children to render.
9
+ * @param pageRoute - The current page route.
10
+ * @returns The provider.
11
+ */
12
+ export const AppPageRouteProvider = ({ children, pageRoute, }) => (_jsx(AppPageRouteContext.Provider, { value: pageRoute, children: children }));
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type RoutesNode } from './page-router-internal-types.ts';
3
+ /**
4
+ * Converts a react node to a routes node by recursively converting the react node.
5
+ *
6
+ * @param reactNode - The react node to convert.
7
+ * @returns The converted routes node.
8
+ */
9
+ export declare const convertReactPageRoutesElement: (reactNode: ReactNode) => RoutesNode;
@@ -0,0 +1,138 @@
1
+ import { Fragment, isValidElement, Children as ReactChildren, } from 'react';
2
+ import { PageRoutes, } from "../components/page-routes.js";
3
+ import { RouteNodeType, } from "./page-router-internal-types.js";
4
+ const isReactPageRoutesElement = (reactRouteNode) => {
5
+ return isValidElement(reactRouteNode) && reactRouteNode.type === PageRoutes;
6
+ };
7
+ const isReactRouteElement = (reactNode) => isValidElement(reactNode) && reactNode.type === PageRoutes.Route;
8
+ const isReactIndexRouteElement = (reactNode) => isValidElement(reactNode) && reactNode.type === PageRoutes.IndexRoute;
9
+ const isReactAnyRouteElement = (reactNode) => isValidElement(reactNode) && reactNode.type === PageRoutes.AnyRoute;
10
+ const isReactFragmentElement = (reactRouteNode) => isValidElement(reactRouteNode) && reactRouteNode.type === Fragment;
11
+ /**
12
+ * Describes a React node by producing a string representation of the React node.
13
+ *
14
+ * @param reactNode - The react node to describe.
15
+ * @returns The description of the react node.
16
+ */
17
+ const describeReactNode = (reactNode) => {
18
+ if (reactNode == null) {
19
+ return String(reactNode);
20
+ }
21
+ if (typeof reactNode !== 'object') {
22
+ return JSON.stringify(reactNode);
23
+ }
24
+ if (isValidElement(reactNode)) {
25
+ const elementType = reactNode.type;
26
+ const name = typeof elementType === 'string'
27
+ ? elementType
28
+ : typeof elementType === 'function'
29
+ ? elementType.displayName ||
30
+ elementType.name ||
31
+ 'Unknown'
32
+ : 'Unknown';
33
+ return `<${name} />`;
34
+ }
35
+ return JSON.stringify(reactNode);
36
+ };
37
+ class InvalidRoutesReactNodeError extends Error {
38
+ constructor(reactNode) {
39
+ super(`Invalid React node for page routes: ${describeReactNode(reactNode)}`);
40
+ }
41
+ }
42
+ const addRoute = (parentNode, childNode) => {
43
+ if (!parentNode.children) {
44
+ parentNode.children = [];
45
+ }
46
+ parentNode.children.push(childNode);
47
+ };
48
+ /**
49
+ * Converts a routes element to a routes node by recursively converting the routes children.
50
+ *
51
+ * @param reactRoutesElement - The routes element to convert.
52
+ * @returns The converted routes node.
53
+ */
54
+ function convertReactRoutesElement(reactRoutesElement) {
55
+ const { path, layoutComponent, children } = reactRoutesElement.props;
56
+ const routesNode = {
57
+ type: RouteNodeType.Routes,
58
+ path,
59
+ layoutComponent,
60
+ children: null,
61
+ };
62
+ convertChildrenReactRouteElements(routesNode, children);
63
+ return routesNode;
64
+ }
65
+ /**
66
+ * Converts the children of a routes element to a routes node by recursively converting the children.
67
+ *
68
+ * @param parentNode - The parent node to add the children to.
69
+ * @param children - The children to convert.
70
+ * @returns void.
71
+ */
72
+ function convertChildrenReactRouteElements(parentNode, children) {
73
+ ReactChildren.forEach(children, (child) => {
74
+ if (child == null || typeof child === 'boolean') {
75
+ return;
76
+ }
77
+ if (isReactPageRoutesElement(child)) {
78
+ const routesNode = convertReactRoutesElement(child);
79
+ addRoute(parentNode, routesNode);
80
+ }
81
+ else if (isReactRouteElement(child)) {
82
+ const { path, component, id } = child.props;
83
+ const routeNode = {
84
+ type: RouteNodeType.Path,
85
+ path,
86
+ component,
87
+ id,
88
+ };
89
+ addRoute(parentNode, routeNode);
90
+ }
91
+ else if (isReactIndexRouteElement(child)) {
92
+ const { component, id } = child.props;
93
+ const routeNode = {
94
+ type: RouteNodeType.Path,
95
+ path: '/',
96
+ component,
97
+ id,
98
+ };
99
+ addRoute(parentNode, routeNode);
100
+ }
101
+ else if (isReactAnyRouteElement(child)) {
102
+ const { component, id } = child.props;
103
+ const routeNode = {
104
+ type: RouteNodeType.Path,
105
+ path: '*',
106
+ component,
107
+ id,
108
+ };
109
+ addRoute(parentNode, routeNode);
110
+ }
111
+ else if (isReactFragmentElement(child)) {
112
+ convertChildrenReactRouteElements(parentNode, child.props.children);
113
+ }
114
+ else {
115
+ throw new InvalidRoutesReactNodeError(child);
116
+ }
117
+ });
118
+ }
119
+ /**
120
+ * Converts a react node to a routes node by recursively converting the react node.
121
+ *
122
+ * @param reactNode - The react node to convert.
123
+ * @returns The converted routes node.
124
+ */
125
+ export const convertReactPageRoutesElement = (reactNode) => {
126
+ if (isReactPageRoutesElement(reactNode)) {
127
+ return convertReactRoutesElement(reactNode);
128
+ }
129
+ if (isReactFragmentElement(reactNode)) {
130
+ const rootNode = {
131
+ type: RouteNodeType.Routes,
132
+ children: null,
133
+ };
134
+ convertChildrenReactRouteElements(rootNode, reactNode.props.children);
135
+ return rootNode;
136
+ }
137
+ throw new InvalidRoutesReactNodeError(reactNode);
138
+ };
@@ -0,0 +1,40 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import type { EmptyProps } from '../../shared/types/shared.ts';
3
+ import type { PageRoutesLayoutComponent } from '../components/page-routes.ts';
4
+ export interface PageRoutesLayoutProps {
5
+ children: ReactNode;
6
+ }
7
+ export declare enum RouteNodeType {
8
+ Routes = "routes",
9
+ Path = "path"
10
+ }
11
+ /**
12
+ * An intermediate representation of the page routes before being added to the trie router.
13
+ */
14
+ export interface RoutesNode {
15
+ type: RouteNodeType.Routes;
16
+ path?: string;
17
+ layoutComponent?: PageRoutesLayoutComponent;
18
+ children: RouteNode[] | null;
19
+ }
20
+ /**
21
+ * An intermediate representation of a route before being added to the trie router.
22
+ */
23
+ export interface RoutePathNode {
24
+ type: RouteNodeType.Path;
25
+ path: string;
26
+ component: ComponentType<EmptyProps>;
27
+ id?: string;
28
+ }
29
+ /**
30
+ * An intermediate representation of a route descriptor node that is used to populate an app page router.
31
+ */
32
+ export type RouteNode = RoutesNode | RoutePathNode;
33
+ /**
34
+ * The data for a page route.
35
+ */
36
+ export interface PageRouteData {
37
+ component: ComponentType<EmptyProps>;
38
+ layouts: PageRoutesLayoutComponent[];
39
+ id?: string;
40
+ }
@@ -0,0 +1,5 @@
1
+ export var RouteNodeType;
2
+ (function (RouteNodeType) {
3
+ RouteNodeType["Routes"] = "routes";
4
+ RouteNodeType["Path"] = "path";
5
+ })(RouteNodeType || (RouteNodeType = {}));
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Trie-based URL router. Routes are split into path segments and are stored in a prefix tree for
3
+ * O(number-of-path-segments) lookup. When multiple route types match, priority is: static > param > wildcard.
4
+ */
5
+ export interface MatchedRoute<TRouteData> {
6
+ data: TRouteData;
7
+ params: Record<string, string>;
8
+ }
9
+ /**
10
+ * The TrieRouter interface defines the methods for matching and adding routes to the router.
11
+ */
12
+ export interface TrieRouter<TRouteData> {
13
+ /**
14
+ * Matches a path to a route and returns the route data and parameters
15
+ */
16
+ matchPath: (path: string) => MatchedRoute<TRouteData> | null;
17
+ /**
18
+ * Adds a route to the router
19
+ */
20
+ addRoute: (path: string, data: TRouteData) => void;
21
+ }
22
+ /**
23
+ * Creates a new TrieRouter instance. After creation, routes can be added to the router using the addRoute method.
24
+ * Routes can be matched using the matchPath method.
25
+ *
26
+ * The order that routes are added does not matter. The router will always match the longest possible route.
27
+ *
28
+ * @param TRouteData - The type of the route data that can be associated with each added route
29
+ * @returns A TrieRouter instance
30
+ */
31
+ export declare const createTrieRouter: <TRouteData = unknown>() => TrieRouter<TRouteData>;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Trie-based URL router. Routes are split into path segments and are stored in a prefix tree for
3
+ * O(number-of-path-segments) lookup. When multiple route types match, priority is: static > param > wildcard.
4
+ */
5
+ const WILDCARD = '*';
6
+ const normalizePath = (path) => `/${path.replace(/\/+/g, '/').replace(/\/$/, '').replace(/^\//, '')}`;
7
+ const splitSegments = (normalizedPath) => normalizedPath === '/' ? [] : normalizedPath.slice(1).split('/');
8
+ const isParamSegment = (segment) => segment.startsWith(':');
9
+ const getParamNameForSegment = (segment) => segment.slice(1);
10
+ const safeDecodeURIComponent = (value) => {
11
+ try {
12
+ return decodeURIComponent(value);
13
+ }
14
+ catch {
15
+ return value;
16
+ }
17
+ };
18
+ /**
19
+ * Recursively searches the trie for an existing route that would conflict
20
+ * with the given segments. Returns the conflicting pattern, or null.
21
+ */
22
+ const findConflictingPattern = (node, segmentIndex, segments) => {
23
+ if (segmentIndex === segments.length) {
24
+ return node.pattern ?? null;
25
+ }
26
+ const segment = segments[segmentIndex];
27
+ if (isParamSegment(segment)) {
28
+ const { paramChildren } = node;
29
+ if (paramChildren) {
30
+ for (const paramChild of Object.values(paramChildren)) {
31
+ const result = findConflictingPattern(paramChild, segmentIndex + 1, segments);
32
+ if (result)
33
+ return result;
34
+ }
35
+ }
36
+ }
37
+ else if (segment === WILDCARD) {
38
+ return node.children?.[WILDCARD]?.pattern ?? null;
39
+ }
40
+ else {
41
+ const childNode = node.children?.[segment];
42
+ if (childNode) {
43
+ const result = findConflictingPattern(childNode, segmentIndex + 1, segments);
44
+ if (result)
45
+ return result;
46
+ }
47
+ }
48
+ return null;
49
+ };
50
+ /**
51
+ * Recursively walks the trie to find a route matching the given segments.
52
+ * Tries static children, then param children, then wildcard (priority order).
53
+ */
54
+ const findMatchingRoute = (node, params, segments, segmentIndex) => {
55
+ if (segmentIndex === segments.length) {
56
+ // Only terminal nodes have data assigned
57
+ if ('data' in node) {
58
+ return {
59
+ data: node.data,
60
+ params,
61
+ };
62
+ }
63
+ return null;
64
+ }
65
+ const segment = segments[segmentIndex];
66
+ // Try matches in priority order: static > param > wildcard
67
+ const childNode = node.children?.[segment];
68
+ if (childNode) {
69
+ const result = findMatchingRoute(childNode, params, segments, segmentIndex + 1);
70
+ if (result)
71
+ return result;
72
+ }
73
+ const { paramChildren } = node;
74
+ if (paramChildren) {
75
+ for (const [paramName, paramChild] of Object.entries(paramChildren)) {
76
+ const result = findMatchingRoute(paramChild, { ...params, [paramName]: safeDecodeURIComponent(segment) }, segments, segmentIndex + 1);
77
+ if (result)
78
+ return result;
79
+ }
80
+ }
81
+ const wildcardNode = node.children?.[WILDCARD];
82
+ if (wildcardNode && 'data' in wildcardNode) {
83
+ return {
84
+ data: wildcardNode.data,
85
+ params: {
86
+ ...params,
87
+ [WILDCARD]: segments.slice(segmentIndex).join('/'),
88
+ },
89
+ };
90
+ }
91
+ return null;
92
+ };
93
+ /**
94
+ * Creates a new TrieRouter instance. After creation, routes can be added to the router using the addRoute method.
95
+ * Routes can be matched using the matchPath method.
96
+ *
97
+ * The order that routes are added does not matter. The router will always match the longest possible route.
98
+ *
99
+ * @param TRouteData - The type of the route data that can be associated with each added route
100
+ * @returns A TrieRouter instance
101
+ */
102
+ export const createTrieRouter = () => {
103
+ const rootNode = {};
104
+ return {
105
+ matchPath: (path) => {
106
+ const normalizedPath = normalizePath(path);
107
+ const segments = splitSegments(normalizedPath);
108
+ return findMatchingRoute(rootNode, {}, segments, 0);
109
+ },
110
+ addRoute: (path, data) => {
111
+ const normalizedPath = normalizePath(path);
112
+ const segments = splitSegments(normalizedPath);
113
+ // Validate the route before we start adding it to the trie to avoid adding invalid routes
114
+ for (let i = 0; i < segments.length; i++) {
115
+ const segment = segments[i];
116
+ if (segment === WILDCARD && i !== segments.length - 1) {
117
+ throw new Error(`Wildcard must be the last segment in route: ${normalizedPath}`);
118
+ }
119
+ if (isParamSegment(segment) && getParamNameForSegment(segment) === '') {
120
+ throw new Error(`Invalid route "${normalizedPath}": param segment at position ${i} has an empty name`);
121
+ }
122
+ }
123
+ const conflictingPattern = findConflictingPattern(rootNode, 0, segments);
124
+ if (conflictingPattern) {
125
+ throw new Error(`Route conflict: "${normalizedPath}" conflicts with existing route "${conflictingPattern}"`);
126
+ }
127
+ // Add the route to the trie
128
+ let current = rootNode;
129
+ for (const segment of segments) {
130
+ if (isParamSegment(segment)) {
131
+ current = (current.paramChildren ??= {})[getParamNameForSegment(segment)] ??= {};
132
+ }
133
+ else {
134
+ current = (current.children ??= {})[segment] ??= {};
135
+ }
136
+ }
137
+ current.data = data;
138
+ current.pattern = normalizedPath;
139
+ },
140
+ };
141
+ };
@@ -0,0 +1 @@
1
+ export {};