@webstudio-is/react-sdk 0.56.0 → 0.58.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 (90) hide show
  1. package/lib/app/custom-components/image.js +6 -6
  2. package/lib/app/custom-components/shared/remix-link.js +7 -3
  3. package/lib/app/params.js +4 -2
  4. package/lib/cjs/app/custom-components/image.js +6 -6
  5. package/lib/cjs/app/custom-components/shared/remix-link.js +7 -3
  6. package/lib/cjs/app/params.js +4 -2
  7. package/lib/cjs/components/link.js +17 -8
  8. package/lib/cjs/css/css.js +8 -5
  9. package/lib/cjs/css/global-rules.js +3 -3
  10. package/lib/cjs/css/style-rules.js +10 -3
  11. package/lib/cjs/props.js +35 -12
  12. package/lib/cjs/pubsub/raf-queue.js +3 -6
  13. package/lib/cjs/tree/root.js +1 -1
  14. package/lib/components/link.js +17 -8
  15. package/lib/css/css.js +8 -5
  16. package/lib/css/global-rules.js +4 -7
  17. package/lib/css/style-rules.js +10 -3
  18. package/lib/props.js +35 -12
  19. package/lib/pubsub/raf-queue.js +3 -6
  20. package/lib/tree/root.js +1 -1
  21. package/lib/types/app/custom-components/image.d.ts +2 -2
  22. package/lib/types/app/custom-components/index.d.ts +8 -8
  23. package/lib/types/app/custom-components/link-block.d.ts +2 -2
  24. package/lib/types/app/custom-components/link.d.ts +2 -2
  25. package/lib/types/app/custom-components/rich-text-link.d.ts +2 -2
  26. package/lib/types/app/custom-components/shared/remix-link.d.ts +3 -3
  27. package/lib/types/app/params.d.ts +2 -2
  28. package/lib/types/components/blockquote.d.ts +1 -1
  29. package/lib/types/components/blockquote.stories.d.ts +2 -2
  30. package/lib/types/components/body.d.ts +1 -1
  31. package/lib/types/components/body.stories.d.ts +1 -1
  32. package/lib/types/components/bold.d.ts +1 -1
  33. package/lib/types/components/bold.stories.d.ts +2 -2
  34. package/lib/types/components/box.d.ts +1 -1
  35. package/lib/types/components/box.stories.d.ts +4 -4
  36. package/lib/types/components/button.d.ts +1 -1
  37. package/lib/types/components/button.stories.d.ts +4 -4
  38. package/lib/types/components/code.d.ts +1 -1
  39. package/lib/types/components/code.stories.d.ts +4 -4
  40. package/lib/types/components/components-utils.d.ts +7 -7
  41. package/lib/types/components/form.d.ts +1 -1
  42. package/lib/types/components/form.stories.d.ts +2 -2
  43. package/lib/types/components/heading.d.ts +1 -1
  44. package/lib/types/components/heading.stories.d.ts +4 -4
  45. package/lib/types/components/image.d.ts +1 -1
  46. package/lib/types/components/image.stories.d.ts +2 -2
  47. package/lib/types/components/input.d.ts +1 -1
  48. package/lib/types/components/input.stories.d.ts +2 -2
  49. package/lib/types/components/italic.d.ts +1 -1
  50. package/lib/types/components/italic.stories.d.ts +2 -2
  51. package/lib/types/components/link-block.stories.d.ts +4 -4
  52. package/lib/types/components/link.d.ts +1 -1
  53. package/lib/types/components/link.stories.d.ts +4 -4
  54. package/lib/types/components/list-item.d.ts +1 -1
  55. package/lib/types/components/list-item.stories.d.ts +2 -2
  56. package/lib/types/components/list.d.ts +1 -1
  57. package/lib/types/components/list.stories.d.ts +4 -4
  58. package/lib/types/components/paragraph.d.ts +1 -1
  59. package/lib/types/components/paragraph.stories.d.ts +2 -2
  60. package/lib/types/components/rich-text-link.stories.d.ts +4 -4
  61. package/lib/types/components/separator.d.ts +1 -1
  62. package/lib/types/components/separator.stories.d.ts +2 -2
  63. package/lib/types/components/span.d.ts +1 -1
  64. package/lib/types/components/span.stories.d.ts +2 -2
  65. package/lib/types/components/subscript.d.ts +1 -1
  66. package/lib/types/components/subscript.stories.d.ts +2 -2
  67. package/lib/types/components/superscript.d.ts +1 -1
  68. package/lib/types/components/superscript.stories.d.ts +2 -2
  69. package/lib/types/components/text-block.d.ts +1 -1
  70. package/lib/types/components/text-block.stories.d.ts +2 -2
  71. package/lib/types/css/css.d.ts +1 -3
  72. package/lib/types/css/global-rules.d.ts +2 -3
  73. package/lib/types/css/style-rules.d.ts +1 -0
  74. package/lib/types/props.d.ts +73 -12
  75. package/lib/types/tree/webstudio-component.d.ts +2 -2
  76. package/package.json +21 -21
  77. package/src/app/custom-components/image.tsx +6 -7
  78. package/src/app/custom-components/shared/remix-link.tsx +7 -3
  79. package/src/app/params.ts +6 -4
  80. package/src/components/link.tsx +21 -7
  81. package/src/css/css.ts +9 -7
  82. package/src/css/global-rules.ts +6 -10
  83. package/src/css/style-rules.test.ts +22 -0
  84. package/src/css/style-rules.ts +15 -4
  85. package/src/props.test.ts +85 -20
  86. package/src/props.ts +57 -15
  87. package/src/pubsub/raf-queue.ts +3 -9
  88. package/src/tree/create-elements-tree.tsx +3 -7
  89. package/src/tree/root.ts +1 -1
  90. package/src/tree/webstudio-component.tsx +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstudio-is/react-sdk",
3
- "version": "0.56.0",
3
+ "version": "0.58.0",
4
4
  "description": "Webstudio JavaScript / TypeScript API",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -13,23 +13,23 @@
13
13
  "@remix-run/server-runtime": "1.15.0",
14
14
  "@storybook/react": "^6.5.16",
15
15
  "@types/node": "^18.11.18",
16
- "@types/react": "^17.0.24",
17
- "@types/react-dom": "^17.0.9",
16
+ "@types/react": "^18.0.35",
17
+ "@types/react-dom": "^18.0.11",
18
18
  "jest": "^29.3.1",
19
- "react": "^17.0.2",
20
- "react-dom": "^17.0.2",
19
+ "react": "^18.2.0",
20
+ "react-dom": "^18.2.0",
21
21
  "typescript": "5.0.3",
22
22
  "zod": "^3.19.1",
23
- "@webstudio-is/jest-config": "^1.0.2",
23
+ "@webstudio-is/jest-config": "^1.0.5",
24
24
  "@webstudio-is/scripts": "^0.0.0",
25
25
  "@webstudio-is/storybook-config": "^0.0.0",
26
- "@webstudio-is/tsconfig": "^1.0.3"
26
+ "@webstudio-is/tsconfig": "^1.0.5"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@remix-run/react": "1.9.0",
30
30
  "@remix-run/server-runtime": "1.9.0",
31
- "react": "^17.0.2",
32
- "react-dom": "^17.0.2",
31
+ "react": "^18.2.0",
32
+ "react-dom": "^18.2.0",
33
33
  "zod": "^3.19.1"
34
34
  },
35
35
  "dependencies": {
@@ -38,16 +38,16 @@
38
38
  "html-tags": "^3.2.0",
39
39
  "nanoevents": "^7.0.1",
40
40
  "nanostores": "^0.7.1",
41
- "@webstudio-is/asset-uploader": "^0.56.0",
42
- "@webstudio-is/css-data": "^0.56.0",
43
- "@webstudio-is/css-engine": "^0.56.0",
44
- "@webstudio-is/css-vars": "^0.56.0",
45
- "@webstudio-is/fonts": "^0.56.0",
46
- "@webstudio-is/generate-arg-types": "^0.56.0",
47
- "@webstudio-is/icons": "^0.56.0",
48
- "@webstudio-is/image": "^0.56.0",
49
- "@webstudio-is/prisma-client": "^0.56.0",
50
- "@webstudio-is/project-build": "^0.56.0"
41
+ "@webstudio-is/asset-uploader": "^0.58.0",
42
+ "@webstudio-is/css-data": "^0.58.0",
43
+ "@webstudio-is/css-engine": "^0.58.0",
44
+ "@webstudio-is/css-vars": "^0.58.0",
45
+ "@webstudio-is/fonts": "^0.58.0",
46
+ "@webstudio-is/generate-arg-types": "^0.58.0",
47
+ "@webstudio-is/icons": "^0.58.0",
48
+ "@webstudio-is/image": "^0.58.0",
49
+ "@webstudio-is/prisma-client": "^0.58.0",
50
+ "@webstudio-is/project-build": "^0.58.0"
51
51
  },
52
52
  "exports": {
53
53
  ".": {
@@ -69,8 +69,8 @@
69
69
  "dev": "build-package --watch",
70
70
  "build": "build-package",
71
71
  "build:args": "generate-arg-types './src/components/*.tsx !./src/**/*.stories.tsx !./src/**/*.ws.tsx' && prettier --write \"**/*.props.ts\"",
72
- "dts": "tsc --emitDeclarationOnly --declaration --declarationDir lib/types",
73
- "typecheck": "tsc --noEmit",
72
+ "dts": "tsc --declarationDir lib/types",
73
+ "typecheck": "tsc --noEmit --emitDeclarationOnly false",
74
74
  "test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests",
75
75
  "lint": "eslint ./src --ext .ts,.tsx --max-warnings 0",
76
76
  "checks": "pnpm typecheck && pnpm lint && pnpm test",
@@ -23,16 +23,15 @@ export const Image = forwardRef<ElementRef<typeof defaultTag>, Props>(
23
23
  return null;
24
24
  }
25
25
  if (asset.location === "REMOTE") {
26
- return loaders.cloudflareImageLoader(params);
26
+ return loaders.cloudflareImageLoader({
27
+ resizeOrigin: params.resizeOrigin,
28
+ cdnUrl: params.assetBaseUrl,
29
+ });
27
30
  }
28
- return loaders.localImageLoader(params);
31
+ return loaders.localImageLoader({ publicPath: params.assetBaseUrl });
29
32
  }, [asset, params]);
30
33
 
31
- let src = props.src;
32
-
33
- if (asset != null) {
34
- src = asset.path;
35
- }
34
+ const src = asset?.name ?? props.src;
36
35
 
37
36
  if (asset == null || loader == null) {
38
37
  return <SdkImage key={src} {...props} src={src} ref={ref} />;
@@ -11,11 +11,15 @@ export const wrapLinkComponent = (BaseLink: LinkComponent) => {
11
11
  const Component: LinkComponent = forwardRef((props: LinkProps, ref) => {
12
12
  const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
13
13
 
14
- if (typeof href === "string" || href === undefined) {
15
- return <BaseLink {...props} ref={ref} />;
14
+ if (href?.type === "page") {
15
+ let to = href.page.path;
16
+ if (href.hash !== undefined) {
17
+ to += `#${href.hash}`;
18
+ }
19
+ return <RemixLink {...props} to={to} ref={ref} />;
16
20
  }
17
21
 
18
- return <RemixLink {...props} to={href.path} ref={ref} />;
22
+ return <BaseLink {...props} ref={ref} />;
19
23
  });
20
24
 
21
25
  Component.displayName = BaseLink.displayName;
package/src/app/params.ts CHANGED
@@ -1,14 +1,16 @@
1
1
  export type Params = {
2
2
  resizeOrigin?: string;
3
- publicPath?: string;
3
+ assetBaseUrl: string;
4
4
  };
5
5
 
6
- let params: Params | null = {};
6
+ let params: undefined | Params;
7
7
 
8
- const emptyParams: Params = {};
8
+ const emptyParams: Params = {
9
+ assetBaseUrl: "/",
10
+ };
9
11
 
10
12
  export const getParams = (): Params => params ?? emptyParams;
11
13
 
12
- export const setParams = (newParams: Params | null) => {
14
+ export const setParams = (newParams: undefined | Params) => {
13
15
  params = newParams;
14
16
  };
@@ -1,5 +1,6 @@
1
1
  import { forwardRef, type ComponentProps } from "react";
2
2
  import { usePropUrl, getInstanceIdFromComponentProps } from "../props";
3
+ import { getParams } from "../app/params";
3
4
 
4
5
  export const defaultTag = "a";
5
6
 
@@ -15,13 +16,26 @@ type Props = Omit<ComponentProps<"a">, "href" | "target"> & {
15
16
 
16
17
  export const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
17
18
  const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
18
- return (
19
- <a
20
- {...props}
21
- href={typeof href === "string" ? href : href?.path}
22
- ref={ref}
23
- />
24
- );
19
+
20
+ const { assetBaseUrl } = getParams();
21
+
22
+ let url = "#";
23
+
24
+ switch (href?.type) {
25
+ case "page":
26
+ url = href.page.path;
27
+ if (href.hash !== undefined) {
28
+ url += `#${href.hash}`;
29
+ }
30
+ break;
31
+ case "asset":
32
+ url = `${assetBaseUrl}${href.asset.name}`;
33
+ break;
34
+ case "string":
35
+ url = href.url;
36
+ }
37
+
38
+ return <a {...props} href={url} ref={ref} />;
25
39
  });
26
40
 
27
41
  Link.displayName = "Link";
package/src/css/css.ts CHANGED
@@ -15,7 +15,7 @@ type Data = {
15
15
  };
16
16
 
17
17
  type CssOptions = {
18
- publicPath?: string;
18
+ assetBaseUrl: string;
19
19
  };
20
20
 
21
21
  export const createImageValueTransformer =
@@ -28,9 +28,8 @@ export const createImageValueTransformer =
28
28
  }
29
29
 
30
30
  // @todo reuse image loaders and generate image-set
31
- const { publicPath = "/" } = options;
32
- const url =
33
- asset.location === "REMOTE" ? asset.path : `${publicPath}${asset.name}`;
31
+ const { assetBaseUrl } = options;
32
+ const url = `${assetBaseUrl}${asset.name}`;
34
33
 
35
34
  return {
36
35
  type: "image",
@@ -53,7 +52,10 @@ export const generateCssText = (data: Data, options: CssOptions) => {
53
52
 
54
53
  const engine = createCssEngine({ name: "ssr" });
55
54
 
56
- addGlobalRules(engine, { assets });
55
+ addGlobalRules(engine, {
56
+ assets,
57
+ assetBaseUrl: options.assetBaseUrl,
58
+ });
57
59
 
58
60
  for (const breakpoint of breakpoints.values()) {
59
61
  engine.addMediaRule(breakpoint.id, breakpoint);
@@ -75,9 +77,9 @@ export const generateCssText = (data: Data, options: CssOptions) => {
75
77
  }
76
78
 
77
79
  const styleRules = getStyleRules(styles, styleSourceSelections);
78
- for (const { breakpointId, instanceId, style } of styleRules) {
80
+ for (const { breakpointId, instanceId, state, style } of styleRules) {
79
81
  engine.addStyleRule(
80
- `[${idAttribute}="${instanceId}"]`,
82
+ `[${idAttribute}="${instanceId}"]${state ?? ""}`,
81
83
  {
82
84
  breakpoint: breakpointId,
83
85
  style,
@@ -1,28 +1,24 @@
1
1
  import type { CssEngine } from "@webstudio-is/css-engine";
2
2
  import type { Assets, FontAsset } from "@webstudio-is/asset-uploader";
3
- import {
4
- type FontFormat,
5
- FONT_FORMATS,
6
- getFontFaces,
7
- } from "@webstudio-is/fonts";
3
+ import { getFontFaces } from "@webstudio-is/fonts";
8
4
 
9
5
  export const addGlobalRules = (
10
6
  engine: CssEngine,
11
- { assets }: { assets: Assets }
7
+ { assets, assetBaseUrl }: { assets: Assets; assetBaseUrl: string }
12
8
  ) => {
13
9
  // @todo we need to figure out all global resets while keeping
14
10
  // the engine aware of all of them.
15
11
  // Ideally, the user is somehow aware and in control of the reset
16
12
  engine.addPlaintextRule("html {margin: 0; height: 100%}");
17
13
 
18
- const fontAssets: Array<FontAsset> = [];
14
+ const fontAssets: FontAsset[] = [];
19
15
  for (const asset of assets.values()) {
20
- if (asset && FONT_FORMATS.has(asset.format as FontFormat)) {
21
- fontAssets.push(asset as FontAsset);
16
+ if (asset?.type === "font") {
17
+ fontAssets.push(asset);
22
18
  }
23
19
  }
24
20
 
25
- const fontFaces = getFontFaces(fontAssets);
21
+ const fontFaces = getFontFaces(fontAssets, { assetBaseUrl });
26
22
  for (const fontFace of fontFaces) {
27
23
  engine.addFontFaceRule(fontFace);
28
24
  }
@@ -55,6 +55,13 @@ test("compute styles from different style sources", () => {
55
55
  property: "color",
56
56
  value: { type: "keyword", value: "blue" },
57
57
  }),
58
+ createStyleDeclPair({
59
+ breakpointId: "a",
60
+ styleSourceId: "styleSource6",
61
+ state: ":hover",
62
+ property: "color",
63
+ value: { type: "keyword", value: "blue" },
64
+ }),
58
65
  ]);
59
66
  const styleSourceSelections: StyleSourceSelections = new Map([
60
67
  [
@@ -85,6 +92,7 @@ test("compute styles from different style sources", () => {
85
92
  {
86
93
  "breakpointId": "a",
87
94
  "instanceId": "instance1",
95
+ "state": undefined,
88
96
  "style": {
89
97
  "width": {
90
98
  "type": "unit",
@@ -96,6 +104,7 @@ test("compute styles from different style sources", () => {
96
104
  {
97
105
  "breakpointId": "a",
98
106
  "instanceId": "instance2",
107
+ "state": undefined,
99
108
  "style": {
100
109
  "color": {
101
110
  "type": "keyword",
@@ -110,6 +119,7 @@ test("compute styles from different style sources", () => {
110
119
  {
111
120
  "breakpointId": "b",
112
121
  "instanceId": "instance2",
122
+ "state": undefined,
113
123
  "style": {
114
124
  "color": {
115
125
  "type": "keyword",
@@ -120,6 +130,18 @@ test("compute styles from different style sources", () => {
120
130
  {
121
131
  "breakpointId": "a",
122
132
  "instanceId": "instance3",
133
+ "state": undefined,
134
+ "style": {
135
+ "color": {
136
+ "type": "keyword",
137
+ "value": "blue",
138
+ },
139
+ },
140
+ },
141
+ {
142
+ "breakpointId": "a",
143
+ "instanceId": "instance3",
144
+ "state": ":hover",
123
145
  "style": {
124
146
  "color": {
125
147
  "type": "keyword",
@@ -10,6 +10,7 @@ import type {
10
10
  type StyleRule = {
11
11
  instanceId: string;
12
12
  breakpointId: string;
13
+ state: undefined | string;
13
14
  style: Style;
14
15
  };
15
16
 
@@ -38,7 +39,10 @@ export const getStyleRules = (
38
39
 
39
40
  const styleRules: StyleRule[] = [];
40
41
  for (const { instanceId, values } of styleSourceSelections.values()) {
41
- const styleRuleByBreakpointId = new Map<Breakpoint["id"], StyleRule>();
42
+ const styleRuleByBreakpointId = new Map<
43
+ `${Breakpoint["id"]}:${string}`,
44
+ StyleRule
45
+ >();
42
46
 
43
47
  for (const styleSourceId of values) {
44
48
  const styleSourceStyles = stylesByStyleSourceId.get(styleSourceId);
@@ -46,15 +50,22 @@ export const getStyleRules = (
46
50
  if (styleSourceStyles === undefined) {
47
51
  continue;
48
52
  }
49
- for (const { breakpointId, property, value } of styleSourceStyles) {
50
- let styleRule = styleRuleByBreakpointId.get(breakpointId);
53
+ for (const {
54
+ breakpointId,
55
+ state,
56
+ property,
57
+ value,
58
+ } of styleSourceStyles) {
59
+ const key = `${breakpointId}:${state ?? ""}` as const;
60
+ let styleRule = styleRuleByBreakpointId.get(key);
51
61
  if (styleRule === undefined) {
52
62
  styleRule = {
53
63
  instanceId,
54
64
  breakpointId,
65
+ state,
55
66
  style: {},
56
67
  };
57
- styleRuleByBreakpointId.set(breakpointId, styleRule);
68
+ styleRuleByBreakpointId.set(key, styleRule);
58
69
  }
59
70
  styleRule.style[property] = value;
60
71
  }
package/src/props.test.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { describe, test, expect } from "@jest/globals";
2
2
  import { resolveUrlProp, type Pages, type PropsByInstanceId } from "./props";
3
3
  import type { Page, Prop } from "@webstudio-is/project-build";
4
+ import type { Asset, Assets } from "@webstudio-is/asset-uploader";
4
5
 
5
6
  const unique = () => Math.random().toString();
6
7
 
7
8
  describe("resolveUrlProp", () => {
8
9
  const instanceId = unique();
10
+ const projectId = unique();
9
11
 
10
12
  const page1: Page = {
11
13
  id: unique(),
@@ -25,6 +27,27 @@ describe("resolveUrlProp", () => {
25
27
  rootInstanceId: "0",
26
28
  };
27
29
 
30
+ const asset1: Asset = {
31
+ id: unique(),
32
+ name: unique(),
33
+ type: "image",
34
+ location: "REMOTE",
35
+ projectId,
36
+ format: "png",
37
+ size: 100000,
38
+ createdAt: new Date().toISOString(),
39
+ description: null,
40
+ meta: { width: 128, height: 180 },
41
+ };
42
+
43
+ const assetProp: Prop = {
44
+ type: "asset",
45
+ id: unique(),
46
+ instanceId,
47
+ name: unique(),
48
+ value: asset1.id,
49
+ };
50
+
28
51
  const pageByIdProp: Prop = {
29
52
  type: "page",
30
53
  id: unique(),
@@ -33,6 +56,22 @@ describe("resolveUrlProp", () => {
33
56
  value: page1.id,
34
57
  };
35
58
 
59
+ const instnaceIdProp: Prop = {
60
+ type: "string",
61
+ id: unique(),
62
+ instanceId: unique(),
63
+ name: "id",
64
+ value: unique(),
65
+ };
66
+
67
+ const pageSectionProp: Prop = {
68
+ type: "page",
69
+ id: unique(),
70
+ instanceId,
71
+ name: unique(),
72
+ value: { pageId: page1.id, instanceId: instnaceIdProp.instanceId },
73
+ };
74
+
36
75
  const pageByPathProp: Prop = {
37
76
  type: "string",
38
77
  id: unique(),
@@ -49,8 +88,18 @@ describe("resolveUrlProp", () => {
49
88
  value: unique(),
50
89
  };
51
90
 
52
- const propsByInstanceId: PropsByInstanceId = new Map([
53
- [instanceId, [pageByIdProp, pageByPathProp, arbitraryUrlProp]],
91
+ const props: PropsByInstanceId = new Map([
92
+ [
93
+ instanceId,
94
+ [
95
+ pageByIdProp,
96
+ pageByPathProp,
97
+ arbitraryUrlProp,
98
+ assetProp,
99
+ pageSectionProp,
100
+ ],
101
+ ],
102
+ [instnaceIdProp.instanceId, [instnaceIdProp]],
54
103
  ]);
55
104
 
56
105
  const pages: Pages = new Map([
@@ -58,38 +107,54 @@ describe("resolveUrlProp", () => {
58
107
  [page2.id, page2],
59
108
  ]);
60
109
 
110
+ const assets: Assets = new Map([[asset1.id, asset1]]);
111
+
112
+ const stores = { props, pages, assets };
113
+
61
114
  test("if instanceId is unknown returns undefined", () => {
62
115
  expect(
63
- resolveUrlProp("unknown", pageByIdProp.name, propsByInstanceId, pages)
116
+ resolveUrlProp("unknown", pageByIdProp.name, stores)
64
117
  ).toBeUndefined();
65
118
  });
66
119
 
67
120
  test("if prop name is unknown returns undefined", () => {
68
- expect(
69
- resolveUrlProp(instanceId, "unknown", propsByInstanceId, pages)
70
- ).toBeUndefined();
121
+ expect(resolveUrlProp(instanceId, "unknown", stores)).toBeUndefined();
122
+ });
123
+
124
+ test("asset by id", () => {
125
+ expect(resolveUrlProp(instanceId, assetProp.name, stores)).toEqual({
126
+ type: "asset",
127
+ asset: asset1,
128
+ });
71
129
  });
72
130
 
73
131
  test("page by id", () => {
74
- expect(
75
- resolveUrlProp(instanceId, pageByIdProp.name, propsByInstanceId, pages)
76
- ).toBe(page1);
132
+ expect(resolveUrlProp(instanceId, pageByIdProp.name, stores)).toEqual({
133
+ type: "page",
134
+ page: page1,
135
+ });
77
136
  });
78
137
 
79
138
  test("page by path", () => {
80
- expect(
81
- resolveUrlProp(instanceId, pageByPathProp.name, propsByInstanceId, pages)
82
- ).toBe(page2);
139
+ expect(resolveUrlProp(instanceId, pageByPathProp.name, stores)).toEqual({
140
+ type: "page",
141
+ page: page2,
142
+ });
143
+ });
144
+
145
+ test("section on a page", () => {
146
+ expect(resolveUrlProp(instanceId, pageSectionProp.name, stores)).toEqual({
147
+ type: "page",
148
+ page: page1,
149
+ instanceId: instnaceIdProp.instanceId,
150
+ hash: instnaceIdProp.value,
151
+ });
83
152
  });
84
153
 
85
154
  test("arbitrary url", () => {
86
- expect(
87
- resolveUrlProp(
88
- instanceId,
89
- arbitraryUrlProp.name,
90
- propsByInstanceId,
91
- pages
92
- )
93
- ).toBe(arbitraryUrlProp.value);
155
+ expect(resolveUrlProp(instanceId, arbitraryUrlProp.name, stores)).toEqual({
156
+ type: "string",
157
+ url: arbitraryUrlProp.value,
158
+ });
94
159
  });
95
160
  });
package/src/props.ts CHANGED
@@ -2,6 +2,7 @@ import { useContext, useMemo } from "react";
2
2
  import { computed } from "nanostores";
3
3
  import { useStore } from "@nanostores/react";
4
4
  import type { Instance, Page, Prop, Props } from "@webstudio-is/project-build";
5
+ import type { Asset, Assets } from "@webstudio-is/asset-uploader";
5
6
  import { ReactSdkContext } from "./context";
6
7
  import { idAttribute } from "./tree/webstudio-component";
7
8
 
@@ -31,7 +32,7 @@ export const useInstanceProps = (instanceId: Instance["id"]) => {
31
32
  const instancePropsObject: Record<Prop["name"], Prop["value"]> = {};
32
33
  if (instanceProps) {
33
34
  for (const prop of instanceProps) {
34
- if (prop.type !== "asset") {
35
+ if (prop.type !== "asset" && prop.type !== "page") {
35
36
  instancePropsObject[prop.name] = prop.value;
36
37
  }
37
38
  }
@@ -67,10 +68,22 @@ export const usePropAsset = (instanceId: Instance["id"], name: string) => {
67
68
  export const resolveUrlProp = (
68
69
  instanceId: Instance["id"],
69
70
  name: string,
70
- propsByInstanceId: PropsByInstanceId,
71
- pages: Pages
72
- ): Page | string | undefined => {
73
- const instanceProps = propsByInstanceId.get(instanceId);
71
+ {
72
+ props,
73
+ pages,
74
+ assets,
75
+ }: { props: PropsByInstanceId; pages: Pages; assets: Assets }
76
+ ):
77
+ | {
78
+ type: "page";
79
+ page: Page;
80
+ instanceId?: Instance["id"];
81
+ hash?: string;
82
+ }
83
+ | { type: "asset"; asset: Asset }
84
+ | { type: "string"; url: string }
85
+ | undefined => {
86
+ const instanceProps = props.get(instanceId);
74
87
  if (instanceProps === undefined) {
75
88
  return;
76
89
  }
@@ -80,16 +93,44 @@ export const resolveUrlProp = (
80
93
  }
81
94
 
82
95
  if (prop.type === "page") {
83
- return pages.get(prop.value);
96
+ if (typeof prop.value === "string") {
97
+ const page = pages.get(prop.value);
98
+ return page && { type: "page", page };
99
+ }
100
+
101
+ const { instanceId, pageId } = prop.value;
102
+
103
+ const page = pages.get(pageId);
104
+
105
+ if (page === undefined) {
106
+ return;
107
+ }
108
+
109
+ const idProp = props.get(instanceId)?.find((prop) => prop.name === "id");
110
+
111
+ return {
112
+ type: "page",
113
+ page,
114
+ instanceId,
115
+ hash:
116
+ idProp === undefined || idProp.type !== "string"
117
+ ? undefined
118
+ : idProp.value,
119
+ };
84
120
  }
85
121
 
86
122
  if (prop.type === "string") {
87
123
  for (const page of pages.values()) {
88
124
  if (page.path === prop.value) {
89
- return page;
125
+ return { type: "page", page };
90
126
  }
91
127
  }
92
- return prop.value;
128
+ return { type: "string", url: prop.value };
129
+ }
130
+
131
+ if (prop.type === "asset") {
132
+ const asset = assets.get(prop.value);
133
+ return asset && { type: "asset", asset };
93
134
  }
94
135
 
95
136
  return;
@@ -99,17 +140,18 @@ export const resolveUrlProp = (
99
140
  // this utility is used for link component in both builder and preview
100
141
  // so need to optimize rerenders with computed
101
142
  export const usePropUrl = (instanceId: Instance["id"], name: string) => {
102
- const { propsByInstanceIdStore, pagesStore } = useContext(ReactSdkContext);
103
- const pageStore = useMemo(
143
+ const { propsByInstanceIdStore, pagesStore, assetsStore } =
144
+ useContext(ReactSdkContext);
145
+ const store = useMemo(
104
146
  () =>
105
147
  computed(
106
- [propsByInstanceIdStore, pagesStore],
107
- (propsByInstanceId, pages) =>
108
- resolveUrlProp(instanceId, name, propsByInstanceId, pages)
148
+ [propsByInstanceIdStore, pagesStore, assetsStore],
149
+ (props, pages, assets) =>
150
+ resolveUrlProp(instanceId, name, { props, pages, assets })
109
151
  ),
110
- [propsByInstanceIdStore, pagesStore, instanceId, name]
152
+ [propsByInstanceIdStore, pagesStore, assetsStore, instanceId, name]
111
153
  );
112
- return useStore(pageStore);
154
+ return useStore(store);
113
155
  };
114
156
 
115
157
  export const getInstanceIdFromComponentProps = (
@@ -1,18 +1,12 @@
1
- import { unstable_batchedUpdates as batchedUpdates } from "react-dom";
2
-
3
1
  type Task = () => void;
4
2
 
5
3
  let handle: ReturnType<typeof requestAnimationFrame> | undefined;
6
4
  let updateQueue: Task[] = [];
7
5
 
8
6
  const processUpdates = (updates: Task[]) => {
9
- // Prior to React v18, updates not called within React event handlers would not be batched
10
- // To ensure all updates are batched into a single React update, we wrap them in a batchedUpdates callback
11
- batchedUpdates(() => {
12
- for (const update of updates) {
13
- update();
14
- }
15
- });
7
+ for (const update of updates) {
8
+ update();
9
+ }
16
10
  };
17
11
 
18
12
  export const batchUpdate = (update: () => void) => {