@hubspot/ui-extensions 0.12.2 → 0.12.4
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.
- package/dist/__tests__/crm/utils/fetchAssociations.spec.js +2 -1
- package/dist/__tests__/crm/utils/fetchCrmProperties.spec.js +1 -1
- package/dist/crm/hooks/useAssociations.d.ts +3 -11
- package/dist/crm/hooks/useAssociations.js +3 -2
- package/dist/crm/hooks/useCrmProperties.d.ts +1 -1
- package/dist/crm/hooks/useCrmProperties.js +2 -2
- package/dist/crm/utils/fetchAssociations.d.ts +1 -9
- package/dist/crm/utils/fetchAssociations.js +0 -11
- package/dist/crm/utils/fetchCrmProperties.js +0 -1
- package/dist/experimental/pages/components/index.d.ts +1 -0
- package/dist/experimental/pages/components/index.js +1 -0
- package/dist/experimental/pages/components/page-routes.d.ts +83 -0
- package/dist/experimental/pages/components/page-routes.js +66 -0
- package/dist/experimental/pages/create-page-router.d.ts +35 -0
- package/dist/experimental/pages/create-page-router.js +123 -0
- package/dist/experimental/pages/create-page-router.test.d.ts +1 -0
- package/dist/experimental/pages/create-page-router.test.js +296 -0
- package/dist/experimental/pages/hooks.d.ts +8 -0
- package/dist/experimental/pages/hooks.js +15 -0
- package/dist/experimental/pages/index.d.ts +6 -0
- package/dist/experimental/pages/index.js +4 -0
- package/dist/experimental/pages/internal/app-page-route-context.d.ts +16 -0
- package/dist/experimental/pages/internal/app-page-route-context.js +12 -0
- package/dist/experimental/pages/internal/convert-page-routes-react-elements.d.ts +9 -0
- package/dist/experimental/pages/internal/convert-page-routes-react-elements.js +138 -0
- package/dist/experimental/pages/internal/page-router-internal-types.d.ts +40 -0
- package/dist/experimental/pages/internal/page-router-internal-types.js +5 -0
- package/dist/experimental/pages/internal/trie-router.d.ts +31 -0
- package/dist/experimental/pages/internal/trie-router.js +141 -0
- package/dist/experimental/pages/internal/trie-router.test.d.ts +1 -0
- package/dist/experimental/pages/internal/trie-router.test.js +263 -0
- package/dist/experimental/pages/internal/useAppPageLocation.d.ts +1 -0
- package/dist/experimental/pages/internal/useAppPageLocation.js +13 -0
- package/dist/experimental/pages/types.d.ts +29 -0
- package/dist/experimental/pages/types.js +1 -0
- package/dist/hs-internal/__tests__/createRemoteComponentInternal.spec.js +4 -4
- package/dist/hubspot.js +0 -1
- package/dist/internal/global-utils.d.ts +1 -1
- package/dist/internal/hook-utils.d.ts +7 -6
- package/dist/internal/hook-utils.js +8 -9
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/shared/remoteComponents.d.ts +28 -2
- package/dist/shared/remoteComponents.js +32 -0
- package/dist/shared/types/components/accordion.d.ts +2 -2
- package/dist/shared/types/components/alert.d.ts +2 -2
- package/dist/shared/types/components/app-home-header-actions.d.ts +3 -3
- package/dist/shared/types/components/button-row.d.ts +2 -2
- package/dist/shared/types/components/button.d.ts +5 -5
- package/dist/shared/types/components/card.d.ts +2 -2
- package/dist/shared/types/components/chart.d.ts +1 -1
- package/dist/shared/types/components/description-list.d.ts +2 -2
- package/dist/shared/types/components/divider.d.ts +1 -1
- package/dist/shared/types/components/dropdown.d.ts +2 -3
- package/dist/shared/types/components/empty-state.d.ts +2 -2
- package/dist/shared/types/components/error-state.d.ts +2 -2
- package/dist/shared/types/components/form.d.ts +3 -3
- package/dist/shared/types/components/heading.d.ts +2 -2
- package/dist/shared/types/components/icon.d.ts +3 -3
- package/dist/shared/types/components/iframe.d.ts +1 -1
- package/dist/shared/types/components/illustration.d.ts +1 -1
- package/dist/shared/types/components/image.d.ts +4 -4
- package/dist/shared/types/components/inputs.d.ts +8 -2
- package/dist/shared/types/components/layouts.d.ts +2 -2
- package/dist/shared/types/components/link.d.ts +5 -5
- package/dist/shared/types/components/list.d.ts +2 -2
- package/dist/shared/types/components/loading-spinner.d.ts +1 -1
- package/dist/shared/types/components/modal.d.ts +2 -2
- package/dist/shared/types/components/panel.d.ts +2 -2
- package/dist/shared/types/components/progress-bar.d.ts +1 -1
- package/dist/shared/types/components/score.d.ts +2 -2
- package/dist/shared/types/components/selects.d.ts +1 -1
- package/dist/shared/types/components/spacer.d.ts +1 -1
- package/dist/shared/types/components/statistics.d.ts +2 -2
- package/dist/shared/types/components/status-tag.d.ts +2 -2
- package/dist/shared/types/components/step-indicator.d.ts +1 -1
- package/dist/shared/types/components/table.d.ts +2 -2
- package/dist/shared/types/components/tabs.d.ts +2 -2
- package/dist/shared/types/components/tag.d.ts +3 -3
- package/dist/shared/types/components/text.d.ts +2 -2
- package/dist/shared/types/components/tile.d.ts +2 -2
- package/dist/shared/types/components/toggle.d.ts +1 -1
- package/dist/shared/types/components/toggleInputs.d.ts +1 -1
- package/dist/shared/types/components/tooltip.d.ts +2 -2
- package/dist/shared/types/crm.d.ts +2 -2
- package/dist/shared/types/experimental.d.ts +1 -1
- package/dist/shared/types/extend.d.ts +2 -2
- package/dist/shared/types/extension-points.d.ts +4 -5
- package/dist/shared/types/pages/app-pages-types.d.ts +75 -0
- package/dist/shared/types/pages/app-pages-types.js +1 -0
- package/dist/shared/types/pages/components/index.d.ts +4 -0
- package/dist/shared/types/pages/components/index.js +1 -0
- package/dist/shared/types/pages/components/page-breadcrumbs.d.ts +34 -0
- package/dist/shared/types/pages/components/page-breadcrumbs.js +1 -0
- package/dist/shared/types/pages/components/page-header.d.ts +82 -0
- package/dist/shared/types/pages/components/page-header.js +1 -0
- package/dist/shared/types/pages/components/page-link.d.ts +25 -0
- package/dist/shared/types/pages/components/page-link.js +1 -0
- package/dist/shared/types/pages/components/page-routes.d.ts +115 -0
- package/dist/shared/types/pages/components/page-routes.js +1 -0
- package/dist/shared/types/pages/components/page-title.d.ts +12 -0
- package/dist/shared/types/pages/components/page-title.js +1 -0
- package/dist/shared/types/pages/index.d.ts +1 -0
- package/dist/shared/types/pages/index.js +1 -0
- package/dist/shared/types/pages.d.ts +1 -0
- package/dist/shared/types/pages.js +1 -0
- package/dist/shared/types/shared.d.ts +15 -1
- package/dist/shared/types/worker-globals.d.ts +8 -3
- package/dist/shared/utils/remote-component-registry.d.ts +1 -1
- package/dist/testing/__tests__/isMatch.spec.js +1 -1
- package/dist/testing/__tests__/mocks.runServerlessFunction.spec.js +1 -1
- package/dist/testing/__tests__/mocks.useAssociations.spec.js +2 -2
- package/dist/testing/__tests__/mocks.useCrmProperties.spec.js +2 -2
- package/dist/testing/__tests__/mocks.useExtensionActions.spec.js +1 -1
- package/dist/testing/__tests__/mocks.useExtensionApi.spec.js +1 -1
- package/dist/testing/__tests__/mocks.useExtensionContext.spec.js +1 -1
- package/dist/testing/internal/convert.d.ts +1 -1
- package/dist/testing/internal/convert.js +4 -5
- package/dist/testing/internal/debug.d.ts +1 -1
- package/dist/testing/internal/element.d.ts +1 -1
- package/dist/testing/internal/errors.d.ts +2 -2
- package/dist/testing/internal/fragment.d.ts +1 -1
- package/dist/testing/internal/match.d.ts +2 -2
- package/dist/testing/internal/mocks/index.d.ts +15 -7
- package/dist/testing/internal/mocks/index.js +20 -6
- package/dist/testing/internal/mocks/mock-app-page-location.d.ts +7 -0
- package/dist/testing/internal/mocks/mock-app-page-location.js +35 -0
- package/dist/testing/internal/mocks/mock-extension-point-api.d.ts +2 -1
- package/dist/testing/internal/mocks/mock-extension-point-api.js +1 -1
- package/dist/testing/internal/mocks/mock-hooks.d.ts +1 -1
- package/dist/testing/internal/mocks/mock-hooks.js +12 -7
- package/dist/testing/internal/text.d.ts +2 -2
- package/dist/testing/internal/type-utils-internal.d.ts +2 -2
- package/dist/testing/internal/types-internal.d.ts +6 -6
- package/dist/testing/render.d.ts +2 -2
- package/dist/testing/render.js +12 -12
- package/dist/testing/type-utils.d.ts +1 -1
- package/dist/testing/type-utils.js +1 -1
- package/dist/testing/types.d.ts +27 -11
- package/dist/testing/utils.d.ts +1 -1
- package/dist/testing/utils.js +1 -1
- package/dist/utils/pagination.d.ts +17 -0
- package/dist/utils/pagination.js +10 -0
- package/package.json +31 -23
|
@@ -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 {};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { createTrieRouter } from "./trie-router.js";
|
|
2
|
+
describe('addRoute — conflict detection', () => {
|
|
3
|
+
it('throws on duplicate exact route', () => {
|
|
4
|
+
const router = createTrieRouter();
|
|
5
|
+
router.addRoute('/foo', 'first');
|
|
6
|
+
expect(() => router.addRoute('/foo', 'second')).toThrowError('Route conflict: "/foo" conflicts with existing route "/foo"');
|
|
7
|
+
});
|
|
8
|
+
it('throws on duplicate root route', () => {
|
|
9
|
+
const router = createTrieRouter();
|
|
10
|
+
router.addRoute('/', 'first');
|
|
11
|
+
expect(() => router.addRoute('/', 'second')).toThrowError('Route conflict: "/" conflicts with existing route "/"');
|
|
12
|
+
});
|
|
13
|
+
it('throws when two param-only routes conflict', () => {
|
|
14
|
+
const router = createTrieRouter();
|
|
15
|
+
router.addRoute('/:foo', 'first');
|
|
16
|
+
expect(() => router.addRoute('/:bar', 'second')).toThrowError('Route conflict: "/:bar" conflicts with existing route "/:foo"');
|
|
17
|
+
});
|
|
18
|
+
it('throws when param routes with matching suffix conflict', () => {
|
|
19
|
+
const router = createTrieRouter();
|
|
20
|
+
router.addRoute('/:foo/suffix', 'first');
|
|
21
|
+
expect(() => router.addRoute('/:bar/suffix', 'second')).toThrowError('Route conflict: "/:bar/suffix" conflicts with existing route "/:foo/suffix"');
|
|
22
|
+
});
|
|
23
|
+
it('allows param routes with different sub-paths', () => {
|
|
24
|
+
const router = createTrieRouter();
|
|
25
|
+
router.addRoute('/:foo', 'terminal');
|
|
26
|
+
router.addRoute('/:bar/baz', 'with-baz');
|
|
27
|
+
expect(router.matchPath('/hello')).toEqual({
|
|
28
|
+
data: 'terminal',
|
|
29
|
+
params: { foo: 'hello' },
|
|
30
|
+
});
|
|
31
|
+
expect(router.matchPath('/hello/baz')).toEqual({
|
|
32
|
+
data: 'with-baz',
|
|
33
|
+
params: { bar: 'hello' },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('throws when wildcard is not the last segment', () => {
|
|
37
|
+
const router = createTrieRouter();
|
|
38
|
+
expect(() => router.addRoute('/foo/*/bar', 'data')).toThrowError('Wildcard must be the last segment in route: /foo/*/bar');
|
|
39
|
+
});
|
|
40
|
+
it('throws when a param segment has an empty name', () => {
|
|
41
|
+
const router = createTrieRouter();
|
|
42
|
+
expect(() => router.addRoute('/:', 'data')).toThrowError('Invalid route "/:": param segment at position 0 has an empty name');
|
|
43
|
+
});
|
|
44
|
+
it('throws for mid-path wildcard without corrupting the trie', () => {
|
|
45
|
+
const router = createTrieRouter();
|
|
46
|
+
expect(() => router.addRoute('/foo/*/bar', 'data')).toThrowError('Wildcard must be the last segment in route: /foo/*/bar');
|
|
47
|
+
router.addRoute('/foo/bar', 'data');
|
|
48
|
+
expect(router.matchPath('/foo/bar')).toEqual({
|
|
49
|
+
data: 'data',
|
|
50
|
+
params: {},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it('throws on duplicate wildcard route', () => {
|
|
54
|
+
const router = createTrieRouter();
|
|
55
|
+
router.addRoute('/foo/*', 'first');
|
|
56
|
+
expect(() => router.addRoute('/foo/*', 'second')).toThrowError('Route conflict: "/foo/*" conflicts with existing route "/foo/*"');
|
|
57
|
+
});
|
|
58
|
+
it("throws when empty string and '/' are both registered as root routes", () => {
|
|
59
|
+
const router = createTrieRouter();
|
|
60
|
+
router.addRoute('', 'first');
|
|
61
|
+
expect(() => router.addRoute('/', 'second')).toThrowError('Route conflict: "/" conflicts with existing route "/"');
|
|
62
|
+
});
|
|
63
|
+
it('allows different sub-paths under the same param name', () => {
|
|
64
|
+
const router = createTrieRouter();
|
|
65
|
+
router.addRoute('/:id/foo', 'first');
|
|
66
|
+
router.addRoute('/:id/bar', 'second');
|
|
67
|
+
expect(router.matchPath('/123/foo')).toEqual({
|
|
68
|
+
data: 'first',
|
|
69
|
+
params: { id: '123' },
|
|
70
|
+
});
|
|
71
|
+
expect(router.matchPath('/123/bar')).toEqual({
|
|
72
|
+
data: 'second',
|
|
73
|
+
params: { id: '123' },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('matchPath — static routes', () => {
|
|
78
|
+
it('matches root path', () => {
|
|
79
|
+
const router = createTrieRouter();
|
|
80
|
+
router.addRoute('/', 'root');
|
|
81
|
+
expect(router.matchPath('/')).toEqual({
|
|
82
|
+
data: 'root',
|
|
83
|
+
params: {},
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
it('matches single segment', () => {
|
|
87
|
+
const router = createTrieRouter();
|
|
88
|
+
router.addRoute('/foo', 'foo-page');
|
|
89
|
+
expect(router.matchPath('/foo')).toEqual({
|
|
90
|
+
data: 'foo-page',
|
|
91
|
+
params: {},
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it('matches multi-segment path', () => {
|
|
95
|
+
const router = createTrieRouter();
|
|
96
|
+
router.addRoute('/foo/bar', 'foo-bar');
|
|
97
|
+
expect(router.matchPath('/foo/bar')).toEqual({
|
|
98
|
+
data: 'foo-bar',
|
|
99
|
+
params: {},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('returns null for unregistered path', () => {
|
|
103
|
+
const router = createTrieRouter();
|
|
104
|
+
router.addRoute('/foo', 'foo-page');
|
|
105
|
+
expect(router.matchPath('/bar')).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
it('returns null when path is a prefix of a registered route', () => {
|
|
108
|
+
const router = createTrieRouter();
|
|
109
|
+
router.addRoute('/foo/bar', 'foo-bar');
|
|
110
|
+
expect(router.matchPath('/foo')).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
it('returns null when path extends beyond a registered route', () => {
|
|
113
|
+
const router = createTrieRouter();
|
|
114
|
+
router.addRoute('/foo', 'foo-page');
|
|
115
|
+
expect(router.matchPath('/foo/bar')).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('matchPath — param routes', () => {
|
|
119
|
+
it('matches param route and extracts param', () => {
|
|
120
|
+
const router = createTrieRouter();
|
|
121
|
+
router.addRoute('/:someParam/foo/bar', 'param-route');
|
|
122
|
+
expect(router.matchPath('/hello/foo/bar')).toEqual({
|
|
123
|
+
data: 'param-route',
|
|
124
|
+
params: { someParam: 'hello' },
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
it('matches route with multiple params', () => {
|
|
128
|
+
const router = createTrieRouter();
|
|
129
|
+
router.addRoute('/:a/:b', 'multi-param');
|
|
130
|
+
expect(router.matchPath('/x/y')).toEqual({
|
|
131
|
+
data: 'multi-param',
|
|
132
|
+
params: { a: 'x', b: 'y' },
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
it('decodes URI-encoded param values', () => {
|
|
136
|
+
const router = createTrieRouter();
|
|
137
|
+
router.addRoute('/:query', 'search');
|
|
138
|
+
expect(router.matchPath('/hello%20world')).toEqual({
|
|
139
|
+
data: 'search',
|
|
140
|
+
params: { query: 'hello world' },
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('matches different param names with different sub-paths', () => {
|
|
144
|
+
const router = createTrieRouter();
|
|
145
|
+
router.addRoute('/:param1/foo', 'foo-route');
|
|
146
|
+
router.addRoute('/:param2/bar', 'bar-route');
|
|
147
|
+
expect(router.matchPath('/x/foo')).toEqual({
|
|
148
|
+
data: 'foo-route',
|
|
149
|
+
params: { param1: 'x' },
|
|
150
|
+
});
|
|
151
|
+
expect(router.matchPath('/x/bar')).toEqual({
|
|
152
|
+
data: 'bar-route',
|
|
153
|
+
params: { param2: 'x' },
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('matchPath — wildcard routes', () => {
|
|
158
|
+
it('matches wildcard and captures remaining segments', () => {
|
|
159
|
+
const router = createTrieRouter();
|
|
160
|
+
router.addRoute('/foo/*', 'wildcard');
|
|
161
|
+
expect(router.matchPath('/foo/a/b/c')).toEqual({
|
|
162
|
+
data: 'wildcard',
|
|
163
|
+
params: { '*': 'a/b/c' },
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
it('matches root wildcard', () => {
|
|
167
|
+
const router = createTrieRouter();
|
|
168
|
+
router.addRoute('/*', 'catch-all');
|
|
169
|
+
expect(router.matchPath('/anything/here')).toEqual({
|
|
170
|
+
data: 'catch-all',
|
|
171
|
+
params: { '*': 'anything/here' },
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
it('matches wildcard with single remaining segment', () => {
|
|
175
|
+
const router = createTrieRouter();
|
|
176
|
+
router.addRoute('/foo/*', 'wildcard');
|
|
177
|
+
expect(router.matchPath('/foo/bar')).toEqual({
|
|
178
|
+
data: 'wildcard',
|
|
179
|
+
params: { '*': 'bar' },
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
it('returns null when path matches wildcard prefix but has no remaining segment', () => {
|
|
183
|
+
const router = createTrieRouter();
|
|
184
|
+
router.addRoute('/foo/*', 'wildcard');
|
|
185
|
+
expect(router.matchPath('/foo')).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe('matchPath — undefined data', () => {
|
|
189
|
+
it('matches static route with undefined data', () => {
|
|
190
|
+
const router = createTrieRouter();
|
|
191
|
+
router.addRoute('/foo', undefined);
|
|
192
|
+
expect(router.matchPath('/foo')).toEqual({
|
|
193
|
+
data: undefined,
|
|
194
|
+
params: {},
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
it('matches param route with undefined data', () => {
|
|
198
|
+
const router = createTrieRouter();
|
|
199
|
+
router.addRoute('/:id', undefined);
|
|
200
|
+
expect(router.matchPath('/123')).toEqual({
|
|
201
|
+
data: undefined,
|
|
202
|
+
params: { id: '123' },
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
it('matches wildcard route with undefined data', () => {
|
|
206
|
+
const router = createTrieRouter();
|
|
207
|
+
router.addRoute('/foo/*', undefined);
|
|
208
|
+
expect(router.matchPath('/foo/bar/baz')).toEqual({
|
|
209
|
+
data: undefined,
|
|
210
|
+
params: { '*': 'bar/baz' },
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
it('returns null for unregistered path when other routes have undefined data', () => {
|
|
214
|
+
const router = createTrieRouter();
|
|
215
|
+
router.addRoute('/foo', undefined);
|
|
216
|
+
expect(router.matchPath('/bar')).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe('matchPath — priority', () => {
|
|
220
|
+
it('static wins over param', () => {
|
|
221
|
+
const router = createTrieRouter();
|
|
222
|
+
router.addRoute('/:param/bar', 'param-route');
|
|
223
|
+
router.addRoute('/foo/bar', 'static-route');
|
|
224
|
+
expect(router.matchPath('/foo/bar')).toEqual({
|
|
225
|
+
data: 'static-route',
|
|
226
|
+
params: {},
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
it('static wins over wildcard', () => {
|
|
230
|
+
const router = createTrieRouter();
|
|
231
|
+
router.addRoute('/foo/*', 'wildcard-route');
|
|
232
|
+
router.addRoute('/foo/bar', 'static-route');
|
|
233
|
+
expect(router.matchPath('/foo/bar')).toEqual({
|
|
234
|
+
data: 'static-route',
|
|
235
|
+
params: {},
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
it('param wins over wildcard', () => {
|
|
239
|
+
const router = createTrieRouter();
|
|
240
|
+
router.addRoute('/*', 'wildcard-route');
|
|
241
|
+
router.addRoute('/:id', 'param-route');
|
|
242
|
+
expect(router.matchPath('/hello')).toEqual({
|
|
243
|
+
data: 'param-route',
|
|
244
|
+
params: { id: 'hello' },
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
it('falls back to wildcard when static does not match deeper', () => {
|
|
248
|
+
const router = createTrieRouter();
|
|
249
|
+
router.addRoute('/foo/bar/baz', 'deep-static');
|
|
250
|
+
router.addRoute('/foo/*', 'wildcard-route');
|
|
251
|
+
expect(router.matchPath('/foo/qux')).toEqual({
|
|
252
|
+
data: 'wildcard-route',
|
|
253
|
+
params: { '*': 'qux' },
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe('matchPath — invalid URI-encoded param', () => {
|
|
258
|
+
it('does not throw on malformed URI-encoded param', () => {
|
|
259
|
+
const router = createTrieRouter();
|
|
260
|
+
router.addRoute('/:id', 'route');
|
|
261
|
+
expect(() => router.matchPath('/%ZZ')).not.toThrow();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useAppPageLocation: () => import("../../../index.ts").AppPageLocation;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getWorkerGlobals } from "../../../internal/global-utils.js";
|
|
2
|
+
import { useMocksContext } from "../../../internal/hook-utils.js";
|
|
3
|
+
export const useAppPageLocation = () => {
|
|
4
|
+
const mocksContext = useMocksContext();
|
|
5
|
+
if (!mocksContext) {
|
|
6
|
+
// If no mocks context is provided, call the original hook implementation provided by
|
|
7
|
+
// the worker globals.
|
|
8
|
+
return getWorkerGlobals().hsWorkerAPI.useAppPageLocation();
|
|
9
|
+
}
|
|
10
|
+
// Otherwise, call the mock implementation provided by the mocks context.
|
|
11
|
+
const { useAppPageLocation } = mocksContext;
|
|
12
|
+
return useAppPageLocation();
|
|
13
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { EmptyProps } from '../../shared/types/shared.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Represents the current page route that has been matched by the page router.
|
|
5
|
+
*/
|
|
6
|
+
export interface MatchedPageRoute {
|
|
7
|
+
/**
|
|
8
|
+
* The id of the route that has been matched.
|
|
9
|
+
*/
|
|
10
|
+
routeId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The path of the route that has been matched.
|
|
13
|
+
*/
|
|
14
|
+
path: string;
|
|
15
|
+
/**
|
|
16
|
+
* The parameters of the route that has been matched.
|
|
17
|
+
*/
|
|
18
|
+
params: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The props type for a PageRouter component.
|
|
22
|
+
*/
|
|
23
|
+
export type PageRouterProps = EmptyProps;
|
|
24
|
+
/**
|
|
25
|
+
* The component type for the page router that handles rendering matched routes.
|
|
26
|
+
*
|
|
27
|
+
* @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.
|
|
28
|
+
*/
|
|
29
|
+
export type PageRouterComponent = ComponentType<PageRouterProps>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { createRenderer } from "../../testing/index.js";
|
|
4
|
-
import { createRemoteComponentInternal } from "../index.js";
|
|
5
|
-
import { __hubSpotComponentRegistry } from "../../shared/remoteComponents.js";
|
|
6
4
|
import { Button, Text } from "../../index.js";
|
|
5
|
+
import { __hubSpotComponentRegistry } from "../../shared/remoteComponents.js";
|
|
6
|
+
import { createRenderer } from "../../testing/index.js";
|
|
7
7
|
import { InvalidComponentsError } from "../../testing/internal/errors.js";
|
|
8
|
-
import {
|
|
8
|
+
import { createRemoteComponentInternal } from "../index.js";
|
|
9
9
|
// Create custom components once at the top level
|
|
10
10
|
const CustomButton = createRemoteComponentInternal('TestCustomButton');
|
|
11
11
|
const CustomCard = createRemoteComponentInternal('TestCustomCard', {
|
package/dist/hubspot.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RendererMocksContext } from '../testing/internal/mocks/index.ts';
|
|
2
|
+
import type { RendererMocks } from '../testing/types.ts';
|
|
2
3
|
type AnyFunction = (...args: any[]) => any;
|
|
3
4
|
/**
|
|
4
5
|
* Creates a mock-aware hook function that can be used to mock the original hook function.
|
|
@@ -8,19 +9,19 @@ type AnyFunction = (...args: any[]) => any;
|
|
|
8
9
|
* @param originalHookFunction The original hook function to call if no mock is found
|
|
9
10
|
* @returns The mocked hook function or the original hook function if no mock is found
|
|
10
11
|
*/
|
|
11
|
-
export declare const createMockAwareHook: <THookName extends keyof
|
|
12
|
+
export declare const createMockAwareHook: <THookName extends keyof RendererMocks, THookFunction extends AnyFunction>(hookName: THookName, originalHookFunction: THookFunction) => THookFunction;
|
|
12
13
|
/**
|
|
13
14
|
* A hook that provides access to the Mocks context.
|
|
14
|
-
* Returns the mocks
|
|
15
|
+
* Returns the mocks context value if inside a MocksContextProvider, otherwise returns null.
|
|
15
16
|
*
|
|
16
|
-
* @returns The mocks
|
|
17
|
+
* @returns The mocks context value or null if not in a test environment.
|
|
17
18
|
*/
|
|
18
|
-
export declare function useMocksContext():
|
|
19
|
+
export declare function useMocksContext(): RendererMocksContext | null;
|
|
19
20
|
/**
|
|
20
21
|
* A React component that provides the Mocks context that can be used to provide mocks to the mock-aware hook functions.
|
|
21
22
|
*
|
|
22
23
|
* @param children The children to render.
|
|
23
24
|
* @returns The children wrapped in the Mocks context provider.
|
|
24
25
|
*/
|
|
25
|
-
export declare const MocksContextProvider: import("react").Provider<
|
|
26
|
+
export declare const MocksContextProvider: import("react").Provider<RendererMocksContext | null>;
|
|
26
27
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
|
-
const
|
|
2
|
+
const ReactRenderMocksContext = createContext(null);
|
|
3
3
|
/**
|
|
4
4
|
* Creates a mock-aware hook function that can be used to mock the original hook function.
|
|
5
5
|
* The mock-aware hook function will return the mocked hook function if a mock is found, otherwise it will return the original hook function.
|
|
@@ -10,29 +10,28 @@ const MocksContext = createContext(null);
|
|
|
10
10
|
*/
|
|
11
11
|
export const createMockAwareHook = (hookName, originalHookFunction) => {
|
|
12
12
|
const useWrapper = (...args) => {
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
13
|
+
const mocksContext = useMocksContext();
|
|
14
|
+
if (!mocksContext) {
|
|
15
15
|
// If no mocks are provided, call the original hook function
|
|
16
16
|
return originalHookFunction(...args);
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
const { mocks } = mocksContext;
|
|
19
19
|
const mockHook = mocks[hookName];
|
|
20
20
|
if (!mockHook) {
|
|
21
21
|
throw new Error(`Illegal State: Mock for hook ${hookName} not found.`);
|
|
22
22
|
}
|
|
23
|
-
// Call the mocked hook function with the same arguments as the original hook function and return the result
|
|
24
23
|
return mockHook(...args);
|
|
25
24
|
};
|
|
26
25
|
return useWrapper;
|
|
27
26
|
};
|
|
28
27
|
/**
|
|
29
28
|
* A hook that provides access to the Mocks context.
|
|
30
|
-
* Returns the mocks
|
|
29
|
+
* Returns the mocks context value if inside a MocksContextProvider, otherwise returns null.
|
|
31
30
|
*
|
|
32
|
-
* @returns The mocks
|
|
31
|
+
* @returns The mocks context value or null if not in a test environment.
|
|
33
32
|
*/
|
|
34
33
|
export function useMocksContext() {
|
|
35
|
-
return useContext(
|
|
34
|
+
return useContext(ReactRenderMocksContext);
|
|
36
35
|
}
|
|
37
36
|
/**
|
|
38
37
|
* A React component that provides the Mocks context that can be used to provide mocks to the mock-aware hook functions.
|
|
@@ -40,4 +39,4 @@ export function useMocksContext() {
|
|
|
40
39
|
* @param children The children to render.
|
|
41
40
|
* @returns The children wrapped in the Mocks context provider.
|
|
42
41
|
*/
|
|
43
|
-
export const MocksContextProvider =
|
|
42
|
+
export const MocksContextProvider = ReactRenderMocksContext.Provider;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PageBreadcrumbs } from '../shared/remoteComponents.tsx';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PageBreadcrumbs } from "../shared/remoteComponents.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type * as experimentalTypes from './types/experimental.ts';
|
|
2
1
|
import type * as componentTypes from './types/components/index.ts';
|
|
3
|
-
import * as crmTypes from './types/crm.ts';
|
|
2
|
+
import type * as crmTypes from './types/crm.ts';
|
|
3
|
+
import type * as experimentalTypes from './types/experimental.ts';
|
|
4
|
+
import type * as pageComponentTypes from './types/pages/components/index.ts';
|
|
4
5
|
/**
|
|
5
6
|
* Represents a registry of HubSpot-provided React components that should only be used **internally** by the UI extension SDK.
|
|
6
7
|
*
|
|
@@ -625,6 +626,31 @@ export declare const PrimaryHeaderActionButton: import("./types/shared.ts").HubS
|
|
|
625
626
|
*
|
|
626
627
|
*/
|
|
627
628
|
export declare const SecondaryHeaderActionButton: import("./types/shared.ts").HubSpotReactComponent<componentTypes.HeaderActionButtonProps>;
|
|
629
|
+
/**
|
|
630
|
+
* The `PageHeader` component renders the actions within the header of the page. It accepts `PrimaryAction` and `SecondaryActions` as children.
|
|
631
|
+
*
|
|
632
|
+
* **Links:**
|
|
633
|
+
* - {@link https://developers.hubspot.com/docs/reference/ui-components/app-page-components/page-header Docs}
|
|
634
|
+
*/
|
|
635
|
+
export declare const PageHeader: import("./types/shared.ts").HubSpotReactComponent<import("./types/shared.ts").UnknownComponentProps> & {
|
|
636
|
+
PrimaryAction: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageHeaderPrimaryActionProps>;
|
|
637
|
+
SecondaryActions: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageHeaderSecondaryActionsProps>;
|
|
638
|
+
Link: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageHeaderLinkProps>;
|
|
639
|
+
PageLink: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageHeaderPageLinkProps>;
|
|
640
|
+
};
|
|
641
|
+
/**
|
|
642
|
+
* The 'PageBreadcrumbs' component renders a list of links to show the user's current location within the app and allow them to navigate back to previous pages.
|
|
643
|
+
*
|
|
644
|
+
* **Links:**
|
|
645
|
+
*
|
|
646
|
+
* - {@link https://developers.hubspot.com/docs/reference/ui-components/app-page-components/page-breadcrumbs Docs}
|
|
647
|
+
*/
|
|
648
|
+
export declare const PageBreadcrumbs: import("./types/shared.ts").HubSpotReactComponent<import("./types/shared.ts").UnknownComponentProps> & {
|
|
649
|
+
PageLink: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageBreadcrumbsPageLinkProps>;
|
|
650
|
+
Current: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageBreadcrumbsCurrentProps>;
|
|
651
|
+
};
|
|
652
|
+
export declare const PageLink: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageLinkProps>;
|
|
653
|
+
export declare const PageTitle: import("./types/shared.ts").HubSpotReactComponent<pageComponentTypes.PageTitleProps>;
|
|
628
654
|
/**
|
|
629
655
|
* @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.
|
|
630
656
|
*/
|
|
@@ -646,6 +646,38 @@ export const SecondaryHeaderActionButton = createAndRegisterRemoteReactComponent
|
|
|
646
646
|
fragmentProps: ['overlay'],
|
|
647
647
|
});
|
|
648
648
|
////////////////////////////////////////////////////////
|
|
649
|
+
// APP PAGE COMPONENTS
|
|
650
|
+
////////////////////////////////////////////////////////
|
|
651
|
+
/**
|
|
652
|
+
* The `PageHeader` component renders the actions within the header of the page. It accepts `PrimaryAction` and `SecondaryActions` as children.
|
|
653
|
+
*
|
|
654
|
+
* **Links:**
|
|
655
|
+
* - {@link https://developers.hubspot.com/docs/reference/ui-components/app-page-components/page-header Docs}
|
|
656
|
+
*/
|
|
657
|
+
export const PageHeader = createAndRegisterRemoteCompoundReactComponent('PageHeader', {
|
|
658
|
+
compoundComponentProperties: {
|
|
659
|
+
PrimaryAction: createAndRegisterRemoteReactComponent('PageHeaderPrimaryAction'),
|
|
660
|
+
SecondaryActions: createAndRegisterRemoteReactComponent('PageHeaderSecondaryActions'),
|
|
661
|
+
Link: createAndRegisterRemoteReactComponent('PageHeaderLink'),
|
|
662
|
+
PageLink: createAndRegisterRemoteReactComponent('PageHeaderPageLink'),
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
/**
|
|
666
|
+
* The 'PageBreadcrumbs' component renders a list of links to show the user's current location within the app and allow them to navigate back to previous pages.
|
|
667
|
+
*
|
|
668
|
+
* **Links:**
|
|
669
|
+
*
|
|
670
|
+
* - {@link https://developers.hubspot.com/docs/reference/ui-components/app-page-components/page-breadcrumbs Docs}
|
|
671
|
+
*/
|
|
672
|
+
export const PageBreadcrumbs = createAndRegisterRemoteCompoundReactComponent('PageBreadcrumbs', {
|
|
673
|
+
compoundComponentProperties: {
|
|
674
|
+
PageLink: createAndRegisterRemoteReactComponent('PageBreadcrumbsPageLink'),
|
|
675
|
+
Current: createAndRegisterRemoteReactComponent('PageBreadcrumbsCurrent'),
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
export const PageLink = createAndRegisterRemoteReactComponent('PageLink');
|
|
679
|
+
export const PageTitle = createAndRegisterRemoteReactComponent('PageTitle');
|
|
680
|
+
////////////////////////////////////////////////////////
|
|
649
681
|
// EXPERIMENTAL COMPONENTS
|
|
650
682
|
////////////////////////////////////////////////////////
|
|
651
683
|
/**
|