@webstudio-is/react-sdk 0.50.0 → 0.51.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 (41) hide show
  1. package/lib/app/custom-components/image.js +2 -4
  2. package/lib/app/custom-components/shared/remix-link.js +7 -23
  3. package/lib/cjs/app/custom-components/image.cjs +1 -3
  4. package/lib/cjs/app/custom-components/shared/remix-link.cjs +6 -22
  5. package/lib/cjs/components/__generated__/link-block.props.cjs +5 -5
  6. package/lib/cjs/components/__generated__/link.props.cjs +6 -6
  7. package/lib/cjs/components/__generated__/rich-text-link.props.cjs +5 -5
  8. package/lib/cjs/components/link-block.ws.cjs +6 -2
  9. package/lib/cjs/components/link.cjs +12 -5
  10. package/lib/cjs/components/link.ws.cjs +8 -1
  11. package/lib/cjs/components/rich-text-link.ws.cjs +5 -2
  12. package/lib/cjs/context.cjs +2 -1
  13. package/lib/cjs/props.cjs +43 -2
  14. package/lib/cjs/tree/create-elements-tree.cjs +8 -1
  15. package/lib/cjs/tree/root.cjs +1 -0
  16. package/lib/components/__generated__/link-block.props.js +5 -5
  17. package/lib/components/__generated__/link.props.js +6 -6
  18. package/lib/components/__generated__/rich-text-link.props.js +5 -5
  19. package/lib/components/link-block.ws.js +6 -2
  20. package/lib/components/link.js +12 -5
  21. package/lib/components/link.ws.js +8 -1
  22. package/lib/components/rich-text-link.ws.js +6 -3
  23. package/lib/context.js +2 -1
  24. package/lib/props.js +43 -2
  25. package/lib/tree/create-elements-tree.js +8 -1
  26. package/lib/tree/root.js +1 -0
  27. package/package.json +9 -9
  28. package/src/app/custom-components/image.tsx +4 -7
  29. package/src/app/custom-components/shared/remix-link.tsx +12 -48
  30. package/src/components/__generated__/link-block.props.ts +5 -5
  31. package/src/components/__generated__/link.props.ts +6 -6
  32. package/src/components/__generated__/rich-text-link.props.ts +5 -5
  33. package/src/components/link-block.ws.tsx +6 -2
  34. package/src/components/link.tsx +12 -6
  35. package/src/components/link.ws.tsx +8 -1
  36. package/src/components/rich-text-link.ws.tsx +6 -3
  37. package/src/context.tsx +3 -1
  38. package/src/props.test.ts +95 -0
  39. package/src/props.ts +59 -3
  40. package/src/tree/create-elements-tree.tsx +6 -2
  41. package/src/tree/root.ts +2 -0
@@ -5,14 +5,12 @@ import {
5
5
  } from "react";
6
6
  import { Image as WebstudioImage, loaders } from "@webstudio-is/image";
7
7
  import { Image as SdkImage } from "../../components/image";
8
- import { usePropAsset } from "../../props";
9
- import { idAttribute } from "../../tree/webstudio-component";
8
+ import { usePropAsset, getInstanceIdFromComponentProps } from "../../props";
10
9
  import { getParams } from "../params";
11
10
  const defaultTag = "img";
12
11
  const Image = forwardRef(
13
12
  (props, ref) => {
14
- const componentId = props[idAttribute];
15
- const asset = usePropAsset(componentId, "src");
13
+ const asset = usePropAsset(getInstanceIdFromComponentProps(props), "src");
16
14
  const params = getParams();
17
15
  const loader = useMemo(() => {
18
16
  if (asset === void 0) {
@@ -1,30 +1,14 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { Link } from "@remix-run/react";
2
+ import { Link as RemixLink } from "@remix-run/react";
3
3
  import { forwardRef } from "react";
4
- const isAbsoluteUrl = (href) => {
5
- try {
6
- new URL(href);
7
- return true;
8
- } catch {
9
- return false;
10
- }
11
- };
12
- const isAbsoluteUrlRemix = (href) => /^[a-z+]+:\/\//i.test(href) || href.startsWith("//");
4
+ import { usePropUrl, getInstanceIdFromComponentProps } from "../../../props";
13
5
  const wrapLinkComponent = (BaseLink) => {
14
- const Component = forwardRef(({ href = "", ...props }, ref) => {
15
- const isAbsolute = isAbsoluteUrl(href);
16
- const willRemixTryToTreatAsAbsoluteAndCrash = isAbsolute === false && isAbsoluteUrlRemix(href);
17
- if (isAbsolute || willRemixTryToTreatAsAbsoluteAndCrash) {
18
- return /* @__PURE__ */ jsx(
19
- BaseLink,
20
- {
21
- ...props,
22
- href: willRemixTryToTreatAsAbsoluteAndCrash ? "" : href,
23
- ref
24
- }
25
- );
6
+ const Component = forwardRef((props, ref) => {
7
+ const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
8
+ if (typeof href === "string" || href === void 0) {
9
+ return /* @__PURE__ */ jsx(BaseLink, { ...props, ref });
26
10
  }
27
- return /* @__PURE__ */ jsx(Link, { ...props, to: href, ref });
11
+ return /* @__PURE__ */ jsx(RemixLink, { ...props, to: href.path, ref });
28
12
  });
29
13
  Component.displayName = BaseLink.displayName;
30
14
  return Component;
@@ -26,13 +26,11 @@ var import_react = require("react");
26
26
  var import_image = require("@webstudio-is/image");
27
27
  var import_image2 = require("../../components/image");
28
28
  var import_props = require("../../props");
29
- var import_webstudio_component = require("../../tree/webstudio-component");
30
29
  var import_params = require("../params");
31
30
  const defaultTag = "img";
32
31
  const Image = (0, import_react.forwardRef)(
33
32
  (props, ref) => {
34
- const componentId = props[import_webstudio_component.idAttribute];
35
- const asset = (0, import_props.usePropAsset)(componentId, "src");
33
+ const asset = (0, import_props.usePropAsset)((0, import_props.getInstanceIdFromComponentProps)(props), "src");
36
34
  const params = (0, import_params.getParams)();
37
35
  const loader = (0, import_react.useMemo)(() => {
38
36
  if (asset === void 0) {
@@ -24,30 +24,14 @@ module.exports = __toCommonJS(remix_link_exports);
24
24
  var import_jsx_runtime = require("react/jsx-runtime");
25
25
  var import_react = require("@remix-run/react");
26
26
  var import_react2 = require("react");
27
- const isAbsoluteUrl = (href) => {
28
- try {
29
- new URL(href);
30
- return true;
31
- } catch {
32
- return false;
33
- }
34
- };
35
- const isAbsoluteUrlRemix = (href) => /^[a-z+]+:\/\//i.test(href) || href.startsWith("//");
27
+ var import_props = require("../../../props");
36
28
  const wrapLinkComponent = (BaseLink) => {
37
- const Component = (0, import_react2.forwardRef)(({ href = "", ...props }, ref) => {
38
- const isAbsolute = isAbsoluteUrl(href);
39
- const willRemixTryToTreatAsAbsoluteAndCrash = isAbsolute === false && isAbsoluteUrlRemix(href);
40
- if (isAbsolute || willRemixTryToTreatAsAbsoluteAndCrash) {
41
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
42
- BaseLink,
43
- {
44
- ...props,
45
- href: willRemixTryToTreatAsAbsoluteAndCrash ? "" : href,
46
- ref
47
- }
48
- );
29
+ const Component = (0, import_react2.forwardRef)((props, ref) => {
30
+ const href = (0, import_props.usePropUrl)((0, import_props.getInstanceIdFromComponentProps)(props), "href");
31
+ if (typeof href === "string" || href === void 0) {
32
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BaseLink, { ...props, ref });
49
33
  }
50
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Link, { ...props, to: href, ref });
34
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Link, { ...props, to: href.path, ref });
51
35
  });
52
36
  Component.displayName = BaseLink.displayName;
53
37
  return Component;
@@ -25,18 +25,18 @@ const props = {
25
25
  slot: { required: false, control: "text", type: "string" },
26
26
  style: { required: false, control: "text", type: "string" },
27
27
  title: { required: false, control: "text", type: "string" },
28
- download: { required: false, control: "text", type: "string" },
29
28
  href: { required: false, control: "text", type: "string" },
30
- hrefLang: { required: false, control: "text", type: "string" },
31
- media: { required: false, control: "text", type: "string" },
32
- ping: { required: false, control: "text", type: "string" },
33
- rel: { required: false, control: "text", type: "string" },
34
29
  target: {
35
30
  required: false,
36
31
  control: "select",
37
32
  type: "string",
38
33
  options: ["_self", "_blank", "_parent", "_top"]
39
34
  },
35
+ download: { required: false, control: "text", type: "string" },
36
+ hrefLang: { required: false, control: "text", type: "string" },
37
+ media: { required: false, control: "text", type: "string" },
38
+ ping: { required: false, control: "text", type: "string" },
39
+ rel: { required: false, control: "text", type: "string" },
40
40
  type: { required: false, control: "text", type: "string" },
41
41
  referrerPolicy: {
42
42
  required: false,
@@ -25,18 +25,18 @@ const props = {
25
25
  slot: { required: false, control: "text", type: "string" },
26
26
  style: { required: false, control: "text", type: "string" },
27
27
  title: { required: false, control: "text", type: "string" },
28
- download: { required: false, control: "text", type: "string" },
29
- href: { required: false, control: "text", type: "string", defaultValue: "" },
30
- hrefLang: { required: false, control: "text", type: "string" },
31
- media: { required: false, control: "text", type: "string" },
32
- ping: { required: false, control: "text", type: "string" },
33
- rel: { required: false, control: "text", type: "string" },
28
+ href: { required: false, control: "text", type: "string" },
34
29
  target: {
35
30
  required: false,
36
31
  control: "select",
37
32
  type: "string",
38
33
  options: ["_self", "_blank", "_parent", "_top"]
39
34
  },
35
+ download: { required: false, control: "text", type: "string" },
36
+ hrefLang: { required: false, control: "text", type: "string" },
37
+ media: { required: false, control: "text", type: "string" },
38
+ ping: { required: false, control: "text", type: "string" },
39
+ rel: { required: false, control: "text", type: "string" },
40
40
  type: { required: false, control: "text", type: "string" },
41
41
  referrerPolicy: {
42
42
  required: false,
@@ -25,18 +25,18 @@ const props = {
25
25
  slot: { required: false, control: "text", type: "string" },
26
26
  style: { required: false, control: "text", type: "string" },
27
27
  title: { required: false, control: "text", type: "string" },
28
- download: { required: false, control: "text", type: "string" },
29
28
  href: { required: false, control: "text", type: "string" },
30
- hrefLang: { required: false, control: "text", type: "string" },
31
- media: { required: false, control: "text", type: "string" },
32
- ping: { required: false, control: "text", type: "string" },
33
- rel: { required: false, control: "text", type: "string" },
34
29
  target: {
35
30
  required: false,
36
31
  control: "select",
37
32
  type: "string",
38
33
  options: ["_self", "_blank", "_parent", "_top"]
39
34
  },
35
+ download: { required: false, control: "text", type: "string" },
36
+ hrefLang: { required: false, control: "text", type: "string" },
37
+ media: { required: false, control: "text", type: "string" },
38
+ ping: { required: false, control: "text", type: "string" },
39
+ rel: { required: false, control: "text", type: "string" },
40
40
  type: { required: false, control: "text", type: "string" },
41
41
  referrerPolicy: {
42
42
  required: false,
@@ -24,6 +24,7 @@ __export(link_block_ws_exports, {
24
24
  module.exports = __toCommonJS(link_block_ws_exports);
25
25
  var import_icons = require("@webstudio-is/icons");
26
26
  var import_link_block = require("./__generated__/link-block.props");
27
+ var import_link = require("./link.ws");
27
28
  const presetStyle = {
28
29
  boxSizing: {
29
30
  type: "keyword",
@@ -42,6 +43,9 @@ const meta = {
42
43
  presetStyle
43
44
  };
44
45
  const propsMeta = {
45
- props: import_link_block.props,
46
- initialProps: ["href", "target", "prefetch"]
46
+ props: {
47
+ ...import_link_block.props,
48
+ href: import_link.propsMeta.props.href
49
+ },
50
+ initialProps: import_link.propsMeta.initialProps
47
51
  };
@@ -23,9 +23,16 @@ __export(link_exports, {
23
23
  module.exports = __toCommonJS(link_exports);
24
24
  var import_jsx_runtime = require("react/jsx-runtime");
25
25
  var import_react = require("react");
26
- const Link = (0, import_react.forwardRef)(
27
- ({ href = "", ...props }, ref) => {
28
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { ...props, href, ref });
29
- }
30
- );
26
+ var import_props = require("../props");
27
+ const Link = (0, import_react.forwardRef)((props, ref) => {
28
+ const href = (0, import_props.usePropUrl)((0, import_props.getInstanceIdFromComponentProps)(props), "href");
29
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
30
+ "a",
31
+ {
32
+ ...props,
33
+ href: typeof href === "string" ? href : href?.path,
34
+ ref
35
+ }
36
+ );
37
+ });
31
38
  Link.displayName = "Link";
@@ -44,6 +44,13 @@ const meta = {
44
44
  children: ["Link text you can edit"]
45
45
  };
46
46
  const propsMeta = {
47
- props: import_link.props,
47
+ props: {
48
+ ...import_link.props,
49
+ href: {
50
+ type: "string",
51
+ control: "url",
52
+ required: false
53
+ }
54
+ },
48
55
  initialProps: ["href", "target", "prefetch"]
49
56
  };
@@ -31,6 +31,9 @@ const meta = {
31
31
  children: []
32
32
  };
33
33
  const propsMeta = {
34
- ...import_link.propsMeta,
35
- props: import_rich_text_link.props
34
+ initialProps: import_link.propsMeta.initialProps,
35
+ props: {
36
+ ...import_rich_text_link.props,
37
+ href: import_link.propsMeta.props.href
38
+ }
36
39
  };
@@ -25,5 +25,6 @@ var import_nanostores = require("nanostores");
25
25
  var import_react = require("react");
26
26
  const ReactSdkContext = (0, import_react.createContext)({
27
27
  propsByInstanceIdStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map()),
28
- assetsStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map())
28
+ assetsStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map()),
29
+ pagesStore: (0, import_nanostores.atom)(/* @__PURE__ */ new Map())
29
30
  });
package/lib/cjs/props.cjs CHANGED
@@ -18,15 +18,19 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var props_exports = {};
20
20
  __export(props_exports, {
21
+ getInstanceIdFromComponentProps: () => getInstanceIdFromComponentProps,
21
22
  getPropsByInstanceId: () => getPropsByInstanceId,
23
+ resolveUrlProp: () => resolveUrlProp,
22
24
  useInstanceProps: () => useInstanceProps,
23
- usePropAsset: () => usePropAsset
25
+ usePropAsset: () => usePropAsset,
26
+ usePropUrl: () => usePropUrl
24
27
  });
25
28
  module.exports = __toCommonJS(props_exports);
26
29
  var import_react = require("react");
27
30
  var import_nanostores = require("nanostores");
28
31
  var import_react2 = require("@nanostores/react");
29
32
  var import_context = require("./context");
33
+ var import_webstudio_component = require("./tree/webstudio-component");
30
34
  const getPropsByInstanceId = (props) => {
31
35
  const propsByInstanceId = /* @__PURE__ */ new Map();
32
36
  for (const prop of props.values()) {
@@ -61,7 +65,7 @@ const usePropAsset = (instanceId, name) => {
61
65
  (propsByInstanceId, assets) => {
62
66
  const instanceProps = propsByInstanceId.get(instanceId);
63
67
  if (instanceProps === void 0) {
64
- return void 0;
68
+ return;
65
69
  }
66
70
  for (const prop of instanceProps) {
67
71
  if (prop.type === "asset" && prop.name === name) {
@@ -75,3 +79,40 @@ const usePropAsset = (instanceId, name) => {
75
79
  const asset = (0, import_react2.useStore)(assetStore);
76
80
  return asset;
77
81
  };
82
+ const resolveUrlProp = (instanceId, name, propsByInstanceId, pages) => {
83
+ const instanceProps = propsByInstanceId.get(instanceId);
84
+ if (instanceProps === void 0) {
85
+ return;
86
+ }
87
+ for (const prop of instanceProps) {
88
+ if (prop.name !== name) {
89
+ continue;
90
+ }
91
+ if (prop.type === "page") {
92
+ return pages.get(prop.value);
93
+ }
94
+ if (prop.type === "string") {
95
+ for (const page of pages.values()) {
96
+ if (page.path === prop.value) {
97
+ return page;
98
+ }
99
+ }
100
+ return prop.value;
101
+ }
102
+ return;
103
+ }
104
+ };
105
+ const usePropUrl = (instanceId, name) => {
106
+ const { propsByInstanceIdStore, pagesStore } = (0, import_react.useContext)(import_context.ReactSdkContext);
107
+ const pageStore = (0, import_react.useMemo)(
108
+ () => (0, import_nanostores.computed)(
109
+ [propsByInstanceIdStore, pagesStore],
110
+ (propsByInstanceId, pages) => resolveUrlProp(instanceId, name, propsByInstanceId, pages)
111
+ ),
112
+ [propsByInstanceIdStore, pagesStore, instanceId, name]
113
+ );
114
+ return (0, import_react2.useStore)(pageStore);
115
+ };
116
+ const getInstanceIdFromComponentProps = (props) => {
117
+ return props[import_webstudio_component.idAttribute];
118
+ };
@@ -31,6 +31,7 @@ const createElementsTree = ({
31
31
  instance,
32
32
  propsByInstanceIdStore,
33
33
  assetsStore,
34
+ pagesStore,
34
35
  Component,
35
36
  getComponent
36
37
  }) => {
@@ -55,7 +56,13 @@ const createElementsTree = ({
55
56
  ],
56
57
  getComponent
57
58
  });
58
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_context.ReactSdkContext.Provider, { value: { propsByInstanceIdStore, assetsStore }, children: root });
59
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
60
+ import_context.ReactSdkContext.Provider,
61
+ {
62
+ value: { propsByInstanceIdStore, assetsStore, pagesStore },
63
+ children: root
64
+ }
65
+ );
59
66
  };
60
67
  const createInstanceChildrenElements = ({
61
68
  instanceSelector,
@@ -75,6 +75,7 @@ const InstanceRoot = ({
75
75
  (0, import_props.getPropsByInstanceId)(new Map(data.build.props))
76
76
  ),
77
77
  assetsStore: (0, import_nanostores.atom)(new Map(data.assets.map((asset) => [asset.id, asset]))),
78
+ pagesStore: (0, import_nanostores.atom)(new Map(data.pages.map((page) => [page.id, page]))),
78
79
  Component: Component ?? import_webstudio_component.WebstudioComponent,
79
80
  getComponent
80
81
  });
@@ -2,18 +2,18 @@ const props = {
2
2
  slot: { required: false, control: "text", type: "string" },
3
3
  style: { required: false, control: "text", type: "string" },
4
4
  title: { required: false, control: "text", type: "string" },
5
- download: { required: false, control: "text", type: "string" },
6
5
  href: { required: false, control: "text", type: "string" },
7
- hrefLang: { required: false, control: "text", type: "string" },
8
- media: { required: false, control: "text", type: "string" },
9
- ping: { required: false, control: "text", type: "string" },
10
- rel: { required: false, control: "text", type: "string" },
11
6
  target: {
12
7
  required: false,
13
8
  control: "select",
14
9
  type: "string",
15
10
  options: ["_self", "_blank", "_parent", "_top"]
16
11
  },
12
+ download: { required: false, control: "text", type: "string" },
13
+ hrefLang: { required: false, control: "text", type: "string" },
14
+ media: { required: false, control: "text", type: "string" },
15
+ ping: { required: false, control: "text", type: "string" },
16
+ rel: { required: false, control: "text", type: "string" },
17
17
  type: { required: false, control: "text", type: "string" },
18
18
  referrerPolicy: {
19
19
  required: false,
@@ -2,18 +2,18 @@ const props = {
2
2
  slot: { required: false, control: "text", type: "string" },
3
3
  style: { required: false, control: "text", type: "string" },
4
4
  title: { required: false, control: "text", type: "string" },
5
- download: { required: false, control: "text", type: "string" },
6
- href: { required: false, control: "text", type: "string", defaultValue: "" },
7
- hrefLang: { required: false, control: "text", type: "string" },
8
- media: { required: false, control: "text", type: "string" },
9
- ping: { required: false, control: "text", type: "string" },
10
- rel: { required: false, control: "text", type: "string" },
5
+ href: { required: false, control: "text", type: "string" },
11
6
  target: {
12
7
  required: false,
13
8
  control: "select",
14
9
  type: "string",
15
10
  options: ["_self", "_blank", "_parent", "_top"]
16
11
  },
12
+ download: { required: false, control: "text", type: "string" },
13
+ hrefLang: { required: false, control: "text", type: "string" },
14
+ media: { required: false, control: "text", type: "string" },
15
+ ping: { required: false, control: "text", type: "string" },
16
+ rel: { required: false, control: "text", type: "string" },
17
17
  type: { required: false, control: "text", type: "string" },
18
18
  referrerPolicy: {
19
19
  required: false,
@@ -2,18 +2,18 @@ const props = {
2
2
  slot: { required: false, control: "text", type: "string" },
3
3
  style: { required: false, control: "text", type: "string" },
4
4
  title: { required: false, control: "text", type: "string" },
5
- download: { required: false, control: "text", type: "string" },
6
5
  href: { required: false, control: "text", type: "string" },
7
- hrefLang: { required: false, control: "text", type: "string" },
8
- media: { required: false, control: "text", type: "string" },
9
- ping: { required: false, control: "text", type: "string" },
10
- rel: { required: false, control: "text", type: "string" },
11
6
  target: {
12
7
  required: false,
13
8
  control: "select",
14
9
  type: "string",
15
10
  options: ["_self", "_blank", "_parent", "_top"]
16
11
  },
12
+ download: { required: false, control: "text", type: "string" },
13
+ hrefLang: { required: false, control: "text", type: "string" },
14
+ media: { required: false, control: "text", type: "string" },
15
+ ping: { required: false, control: "text", type: "string" },
16
+ rel: { required: false, control: "text", type: "string" },
17
17
  type: { required: false, control: "text", type: "string" },
18
18
  referrerPolicy: {
19
19
  required: false,
@@ -1,5 +1,6 @@
1
1
  import { BoxLinkIcon } from "@webstudio-is/icons";
2
2
  import { props } from "./__generated__/link-block.props";
3
+ import { propsMeta as linkPropsMeta } from "./link.ws";
3
4
  const presetStyle = {
4
5
  boxSizing: {
5
6
  type: "keyword",
@@ -18,8 +19,11 @@ const meta = {
18
19
  presetStyle
19
20
  };
20
21
  const propsMeta = {
21
- props,
22
- initialProps: ["href", "target", "prefetch"]
22
+ props: {
23
+ ...props,
24
+ href: linkPropsMeta.props.href
25
+ },
26
+ initialProps: linkPropsMeta.initialProps
23
27
  };
24
28
  export {
25
29
  meta,
@@ -1,10 +1,17 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
- const Link = forwardRef(
4
- ({ href = "", ...props }, ref) => {
5
- return /* @__PURE__ */ jsx("a", { ...props, href, ref });
6
- }
7
- );
3
+ import { usePropUrl, getInstanceIdFromComponentProps } from "../props";
4
+ const Link = forwardRef((props, ref) => {
5
+ const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
6
+ return /* @__PURE__ */ jsx(
7
+ "a",
8
+ {
9
+ ...props,
10
+ href: typeof href === "string" ? href : href?.path,
11
+ ref
12
+ }
13
+ );
14
+ });
8
15
  Link.displayName = "Link";
9
16
  export {
10
17
  Link
@@ -20,7 +20,14 @@ const meta = {
20
20
  children: ["Link text you can edit"]
21
21
  };
22
22
  const propsMeta = {
23
- props,
23
+ props: {
24
+ ...props,
25
+ href: {
26
+ type: "string",
27
+ control: "url",
28
+ required: false
29
+ }
30
+ },
24
31
  initialProps: ["href", "target", "prefetch"]
25
32
  };
26
33
  export {
@@ -1,5 +1,5 @@
1
1
  import { props } from "./__generated__/rich-text-link.props";
2
- import { meta as linkMeta, propsMeta as propsLinkMeta } from "./link.ws";
2
+ import { meta as linkMeta, propsMeta as linkPropsMeta } from "./link.ws";
3
3
  const { category, ...linkMetaRest } = linkMeta;
4
4
  const meta = {
5
5
  ...linkMetaRest,
@@ -7,8 +7,11 @@ const meta = {
7
7
  children: []
8
8
  };
9
9
  const propsMeta = {
10
- ...propsLinkMeta,
11
- props
10
+ initialProps: linkPropsMeta.initialProps,
11
+ props: {
12
+ ...props,
13
+ href: linkPropsMeta.props.href
14
+ }
12
15
  };
13
16
  export {
14
17
  meta,
package/lib/context.js CHANGED
@@ -2,7 +2,8 @@ import { atom } from "nanostores";
2
2
  import { createContext } from "react";
3
3
  const ReactSdkContext = createContext({
4
4
  propsByInstanceIdStore: atom(/* @__PURE__ */ new Map()),
5
- assetsStore: atom(/* @__PURE__ */ new Map())
5
+ assetsStore: atom(/* @__PURE__ */ new Map()),
6
+ pagesStore: atom(/* @__PURE__ */ new Map())
6
7
  });
7
8
  export {
8
9
  ReactSdkContext
package/lib/props.js 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 { ReactSdkContext } from "./context";
5
+ import { idAttribute } from "./tree/webstudio-component";
5
6
  const getPropsByInstanceId = (props) => {
6
7
  const propsByInstanceId = /* @__PURE__ */ new Map();
7
8
  for (const prop of props.values()) {
@@ -36,7 +37,7 @@ const usePropAsset = (instanceId, name) => {
36
37
  (propsByInstanceId, assets) => {
37
38
  const instanceProps = propsByInstanceId.get(instanceId);
38
39
  if (instanceProps === void 0) {
39
- return void 0;
40
+ return;
40
41
  }
41
42
  for (const prop of instanceProps) {
42
43
  if (prop.type === "asset" && prop.name === name) {
@@ -50,8 +51,48 @@ const usePropAsset = (instanceId, name) => {
50
51
  const asset = useStore(assetStore);
51
52
  return asset;
52
53
  };
54
+ const resolveUrlProp = (instanceId, name, propsByInstanceId, pages) => {
55
+ const instanceProps = propsByInstanceId.get(instanceId);
56
+ if (instanceProps === void 0) {
57
+ return;
58
+ }
59
+ for (const prop of instanceProps) {
60
+ if (prop.name !== name) {
61
+ continue;
62
+ }
63
+ if (prop.type === "page") {
64
+ return pages.get(prop.value);
65
+ }
66
+ if (prop.type === "string") {
67
+ for (const page of pages.values()) {
68
+ if (page.path === prop.value) {
69
+ return page;
70
+ }
71
+ }
72
+ return prop.value;
73
+ }
74
+ return;
75
+ }
76
+ };
77
+ const usePropUrl = (instanceId, name) => {
78
+ const { propsByInstanceIdStore, pagesStore } = useContext(ReactSdkContext);
79
+ const pageStore = useMemo(
80
+ () => computed(
81
+ [propsByInstanceIdStore, pagesStore],
82
+ (propsByInstanceId, pages) => resolveUrlProp(instanceId, name, propsByInstanceId, pages)
83
+ ),
84
+ [propsByInstanceIdStore, pagesStore, instanceId, name]
85
+ );
86
+ return useStore(pageStore);
87
+ };
88
+ const getInstanceIdFromComponentProps = (props) => {
89
+ return props[idAttribute];
90
+ };
53
91
  export {
92
+ getInstanceIdFromComponentProps,
54
93
  getPropsByInstanceId,
94
+ resolveUrlProp,
55
95
  useInstanceProps,
56
- usePropAsset
96
+ usePropAsset,
97
+ usePropUrl
57
98
  };
@@ -8,6 +8,7 @@ const createElementsTree = ({
8
8
  instance,
9
9
  propsByInstanceIdStore,
10
10
  assetsStore,
11
+ pagesStore,
11
12
  Component,
12
13
  getComponent
13
14
  }) => {
@@ -32,7 +33,13 @@ const createElementsTree = ({
32
33
  ],
33
34
  getComponent
34
35
  });
35
- return /* @__PURE__ */ jsx(ReactSdkContext.Provider, { value: { propsByInstanceIdStore, assetsStore }, children: root });
36
+ return /* @__PURE__ */ jsx(
37
+ ReactSdkContext.Provider,
38
+ {
39
+ value: { propsByInstanceIdStore, assetsStore, pagesStore },
40
+ children: root
41
+ }
42
+ );
36
43
  };
37
44
  const createInstanceChildrenElements = ({
38
45
  instanceSelector,
package/lib/tree/root.js CHANGED
@@ -52,6 +52,7 @@ const InstanceRoot = ({
52
52
  getPropsByInstanceId(new Map(data.build.props))
53
53
  ),
54
54
  assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))),
55
+ pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))),
55
56
  Component: Component ?? WebstudioComponent,
56
57
  getComponent
57
58
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstudio-is/react-sdk",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Webstudio JavaScript / TypeScript API",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -40,14 +40,14 @@
40
40
  "mitt": "^3.0.0",
41
41
  "nanostores": "^0.7.1",
42
42
  "warn-once": "^0.1.1",
43
- "@webstudio-is/asset-uploader": "^0.50.0",
44
- "@webstudio-is/css-data": "^0.50.0",
45
- "@webstudio-is/generate-arg-types": "^0.50.0",
46
- "@webstudio-is/css-vars": "^0.50.0",
47
- "@webstudio-is/icons": "^0.50.0",
48
- "@webstudio-is/image": "^0.50.0",
49
- "@webstudio-is/prisma-client": "^0.50.0",
50
- "@webstudio-is/project-build": "^0.50.0"
43
+ "@webstudio-is/asset-uploader": "^0.51.0",
44
+ "@webstudio-is/css-data": "^0.51.0",
45
+ "@webstudio-is/css-vars": "^0.51.0",
46
+ "@webstudio-is/generate-arg-types": "^0.51.0",
47
+ "@webstudio-is/icons": "^0.51.0",
48
+ "@webstudio-is/image": "^0.51.0",
49
+ "@webstudio-is/prisma-client": "^0.51.0",
50
+ "@webstudio-is/project-build": "^0.51.0"
51
51
  },
52
52
  "exports": {
53
53
  ".": {
@@ -1,24 +1,21 @@
1
1
  import {
2
2
  forwardRef,
3
3
  useMemo,
4
- type ComponentProps,
4
+ type ComponentPropsWithoutRef,
5
5
  type ElementRef,
6
6
  } from "react";
7
7
  import { Image as WebstudioImage, loaders } from "@webstudio-is/image";
8
8
  import { Image as SdkImage } from "../../components/image";
9
- import { usePropAsset } from "../../props";
10
- import { idAttribute } from "../../tree/webstudio-component";
9
+ import { usePropAsset, getInstanceIdFromComponentProps } from "../../props";
11
10
  import { getParams } from "../params";
12
11
 
13
12
  const defaultTag = "img";
14
13
 
15
- type Props = ComponentProps<typeof WebstudioImage> & { [idAttribute]: string };
14
+ type Props = ComponentPropsWithoutRef<typeof WebstudioImage>;
16
15
 
17
16
  export const Image = forwardRef<ElementRef<typeof defaultTag>, Props>(
18
17
  (props, ref) => {
19
- const componentId = props[idAttribute] as string;
20
-
21
- const asset = usePropAsset(componentId, "src");
18
+ const asset = usePropAsset(getInstanceIdFromComponentProps(props), "src");
22
19
  const params = getParams();
23
20
 
24
21
  const loader = useMemo(() => {
@@ -1,59 +1,23 @@
1
- import { Link } from "@remix-run/react";
2
- import type {
3
- ElementRef,
4
- ComponentProps,
5
- RefAttributes,
6
- ForwardRefExoticComponent,
7
- } from "react";
1
+ import { Link as RemixLink } from "@remix-run/react";
2
+ import type { ComponentPropsWithoutRef } from "react";
8
3
  import { forwardRef } from "react";
9
- import type { Link as BaseLink } from "../../../components/link";
4
+ import type { Link } from "../../../components/link";
5
+ import { usePropUrl, getInstanceIdFromComponentProps } from "../../../props";
10
6
 
11
- const isAbsoluteUrl = (href: string) => {
12
- try {
13
- new URL(href);
14
- return true;
15
- } catch {
16
- return false;
17
- }
18
- };
19
-
20
- // Remix's check for absolute URL copied from here:
21
- // https://github.com/remix-run/react-router/blob/react-router-dom%406.8.0/packages/react-router-dom/index.tsx#L423-L424
22
- const isAbsoluteUrlRemix = (href: string) =>
23
- /^[a-z+]+:\/\//i.test(href) || href.startsWith("//");
24
-
25
- type Props = ComponentProps<typeof BaseLink>;
26
-
27
- type Ref = ElementRef<"a">;
28
-
29
- export const wrapLinkComponent = (
30
- BaseLink: ForwardRefExoticComponent<Props & RefAttributes<Ref>>
31
- ) => {
32
- // We're not actually wrapping BaseLink (no way to wrap with Remix's Link),
33
- // but this is still useful because we're making sure that props/ref types are compatible
34
- const Component = forwardRef<Ref, Props>(({ href = "", ...props }, ref) => {
35
- const isAbsolute = isAbsoluteUrl(href);
7
+ type LinkComponent = typeof Link;
8
+ type LinkProps = ComponentPropsWithoutRef<LinkComponent>;
36
9
 
37
- // This is a workaround for a bug in Remix: https://github.com/remix-run/remix/issues/5440
38
- // It has a buggy absolute URL detection, which gives false positives on value like "//" or "http://"
39
- // and causes entire app to crash
40
- const willRemixTryToTreatAsAbsoluteAndCrash =
41
- isAbsolute === false && isAbsoluteUrlRemix(href);
10
+ export const wrapLinkComponent = (BaseLink: LinkComponent) => {
11
+ const Component: LinkComponent = forwardRef((props: LinkProps, ref) => {
12
+ const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
42
13
 
43
- if (isAbsolute || willRemixTryToTreatAsAbsoluteAndCrash) {
44
- return (
45
- <BaseLink
46
- {...props}
47
- href={willRemixTryToTreatAsAbsoluteAndCrash ? "" : href}
48
- ref={ref}
49
- />
50
- );
14
+ if (typeof href === "string" || href === undefined) {
15
+ return <BaseLink {...props} ref={ref} />;
51
16
  }
52
17
 
53
- return <Link {...props} to={href} ref={ref} />;
18
+ return <RemixLink {...props} to={href.path} ref={ref} />;
54
19
  });
55
20
 
56
- // This is the only part that we use from BaseLink at runtime
57
21
  Component.displayName = BaseLink.displayName;
58
22
 
59
23
  return Component;
@@ -4,18 +4,18 @@ export const props: Record<string, PropMeta> = {
4
4
  slot: { required: false, control: "text", type: "string" },
5
5
  style: { required: false, control: "text", type: "string" },
6
6
  title: { required: false, control: "text", type: "string" },
7
- download: { required: false, control: "text", type: "string" },
8
7
  href: { required: false, control: "text", type: "string" },
9
- hrefLang: { required: false, control: "text", type: "string" },
10
- media: { required: false, control: "text", type: "string" },
11
- ping: { required: false, control: "text", type: "string" },
12
- rel: { required: false, control: "text", type: "string" },
13
8
  target: {
14
9
  required: false,
15
10
  control: "select",
16
11
  type: "string",
17
12
  options: ["_self", "_blank", "_parent", "_top"],
18
13
  },
14
+ download: { required: false, control: "text", type: "string" },
15
+ hrefLang: { required: false, control: "text", type: "string" },
16
+ media: { required: false, control: "text", type: "string" },
17
+ ping: { required: false, control: "text", type: "string" },
18
+ rel: { required: false, control: "text", type: "string" },
19
19
  type: { required: false, control: "text", type: "string" },
20
20
  referrerPolicy: {
21
21
  required: false,
@@ -4,18 +4,18 @@ export const props: Record<string, PropMeta> = {
4
4
  slot: { required: false, control: "text", type: "string" },
5
5
  style: { required: false, control: "text", type: "string" },
6
6
  title: { required: false, control: "text", type: "string" },
7
- download: { required: false, control: "text", type: "string" },
8
- href: { required: false, control: "text", type: "string", defaultValue: "" },
9
- hrefLang: { required: false, control: "text", type: "string" },
10
- media: { required: false, control: "text", type: "string" },
11
- ping: { required: false, control: "text", type: "string" },
12
- rel: { required: false, control: "text", type: "string" },
7
+ href: { required: false, control: "text", type: "string" },
13
8
  target: {
14
9
  required: false,
15
10
  control: "select",
16
11
  type: "string",
17
12
  options: ["_self", "_blank", "_parent", "_top"],
18
13
  },
14
+ download: { required: false, control: "text", type: "string" },
15
+ hrefLang: { required: false, control: "text", type: "string" },
16
+ media: { required: false, control: "text", type: "string" },
17
+ ping: { required: false, control: "text", type: "string" },
18
+ rel: { required: false, control: "text", type: "string" },
19
19
  type: { required: false, control: "text", type: "string" },
20
20
  referrerPolicy: {
21
21
  required: false,
@@ -4,18 +4,18 @@ export const props: Record<string, PropMeta> = {
4
4
  slot: { required: false, control: "text", type: "string" },
5
5
  style: { required: false, control: "text", type: "string" },
6
6
  title: { required: false, control: "text", type: "string" },
7
- download: { required: false, control: "text", type: "string" },
8
7
  href: { required: false, control: "text", type: "string" },
9
- hrefLang: { required: false, control: "text", type: "string" },
10
- media: { required: false, control: "text", type: "string" },
11
- ping: { required: false, control: "text", type: "string" },
12
- rel: { required: false, control: "text", type: "string" },
13
8
  target: {
14
9
  required: false,
15
10
  control: "select",
16
11
  type: "string",
17
12
  options: ["_self", "_blank", "_parent", "_top"],
18
13
  },
14
+ download: { required: false, control: "text", type: "string" },
15
+ hrefLang: { required: false, control: "text", type: "string" },
16
+ media: { required: false, control: "text", type: "string" },
17
+ ping: { required: false, control: "text", type: "string" },
18
+ rel: { required: false, control: "text", type: "string" },
19
19
  type: { required: false, control: "text", type: "string" },
20
20
  referrerPolicy: {
21
21
  required: false,
@@ -1,6 +1,7 @@
1
1
  import { BoxLinkIcon } from "@webstudio-is/icons";
2
2
  import type { WsComponentMeta, WsComponentPropsMeta } from "./component-meta";
3
3
  import { props } from "./__generated__/link-block.props";
4
+ import { propsMeta as linkPropsMeta } from "./link.ws";
4
5
 
5
6
  const presetStyle = {
6
7
  boxSizing: {
@@ -22,6 +23,9 @@ export const meta: WsComponentMeta = {
22
23
  };
23
24
 
24
25
  export const propsMeta: WsComponentPropsMeta = {
25
- props,
26
- initialProps: ["href", "target", "prefetch"],
26
+ props: {
27
+ ...props,
28
+ href: linkPropsMeta.props.href,
29
+ },
30
+ initialProps: linkPropsMeta.initialProps,
27
31
  };
@@ -1,4 +1,5 @@
1
- import { forwardRef, type ElementRef, type ComponentProps } from "react";
1
+ import { forwardRef, type ComponentProps } from "react";
2
+ import { usePropUrl, getInstanceIdFromComponentProps } from "../props";
2
3
 
3
4
  // @todo props that come from remix link, shouldn't be here at all
4
5
  // - prefetch should be only on remix component and it already is
@@ -10,10 +11,15 @@ type Props = Omit<ComponentProps<"a">, "href" | "target"> & {
10
11
  prefetch?: "none" | "intent" | "render";
11
12
  };
12
13
 
13
- export const Link = forwardRef<ElementRef<"a">, Props>(
14
- ({ href = "", ...props }, ref) => {
15
- return <a {...props} href={href} ref={ref} />;
16
- }
17
- );
14
+ export const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
15
+ const href = usePropUrl(getInstanceIdFromComponentProps(props), "href");
16
+ return (
17
+ <a
18
+ {...props}
19
+ href={typeof href === "string" ? href : href?.path}
20
+ ref={ref}
21
+ />
22
+ );
23
+ });
18
24
 
19
25
  Link.displayName = "Link";
@@ -24,6 +24,13 @@ export const meta: WsComponentMeta = {
24
24
  };
25
25
 
26
26
  export const propsMeta: WsComponentPropsMeta = {
27
- props,
27
+ props: {
28
+ ...props,
29
+ href: {
30
+ type: "string",
31
+ control: "url",
32
+ required: false,
33
+ },
34
+ },
28
35
  initialProps: ["href", "target", "prefetch"],
29
36
  };
@@ -1,6 +1,6 @@
1
1
  import type { WsComponentMeta, WsComponentPropsMeta } from "./component-meta";
2
2
  import { props } from "./__generated__/rich-text-link.props";
3
- import { meta as linkMeta, propsMeta as propsLinkMeta } from "./link.ws";
3
+ import { meta as linkMeta, propsMeta as linkPropsMeta } from "./link.ws";
4
4
 
5
5
  const { category, ...linkMetaRest } = linkMeta;
6
6
 
@@ -11,6 +11,9 @@ export const meta: WsComponentMeta = {
11
11
  };
12
12
 
13
13
  export const propsMeta: WsComponentPropsMeta = {
14
- ...propsLinkMeta,
15
- props,
14
+ initialProps: linkPropsMeta.initialProps,
15
+ props: {
16
+ ...props,
17
+ href: linkPropsMeta.props.href,
18
+ },
16
19
  };
package/src/context.tsx CHANGED
@@ -1,11 +1,13 @@
1
1
  import { type ReadableAtom, atom } from "nanostores";
2
2
  import { createContext } from "react";
3
- import type { Assets, PropsByInstanceId } from "./props";
3
+ import type { Assets, Pages, PropsByInstanceId } from "./props";
4
4
 
5
5
  export const ReactSdkContext = createContext<{
6
6
  propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
7
7
  assetsStore: ReadableAtom<Assets>;
8
+ pagesStore: ReadableAtom<Pages>;
8
9
  }>({
9
10
  propsByInstanceIdStore: atom(new Map()),
10
11
  assetsStore: atom(new Map()),
12
+ pagesStore: atom(new Map()),
11
13
  });
@@ -0,0 +1,95 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import { resolveUrlProp, type Pages, type PropsByInstanceId } from "./props";
3
+ import type { Page, Prop } from "@webstudio-is/project-build";
4
+
5
+ const unique = () => Math.random().toString();
6
+
7
+ describe("resolveUrlProp", () => {
8
+ const instanceId = unique();
9
+
10
+ const page1: Page = {
11
+ id: unique(),
12
+ path: `/${unique()}`,
13
+ name: "",
14
+ title: "",
15
+ meta: {},
16
+ rootInstanceId: "0",
17
+ };
18
+
19
+ const page2: Page = {
20
+ id: unique(),
21
+ path: `/${unique()}`,
22
+ name: "",
23
+ title: "",
24
+ meta: {},
25
+ rootInstanceId: "0",
26
+ };
27
+
28
+ const pageByIdProp: Prop = {
29
+ type: "page",
30
+ id: unique(),
31
+ instanceId,
32
+ name: unique(),
33
+ value: page1.id,
34
+ };
35
+
36
+ const pageByPathProp: Prop = {
37
+ type: "string",
38
+ id: unique(),
39
+ instanceId,
40
+ name: unique(),
41
+ value: page2.path,
42
+ };
43
+
44
+ const arbitraryUrlProp: Prop = {
45
+ type: "string",
46
+ id: unique(),
47
+ instanceId,
48
+ name: unique(),
49
+ value: unique(),
50
+ };
51
+
52
+ const propsByInstanceId: PropsByInstanceId = new Map([
53
+ [instanceId, [pageByIdProp, pageByPathProp, arbitraryUrlProp]],
54
+ ]);
55
+
56
+ const pages: Pages = new Map([
57
+ [page1.id, page1],
58
+ [page2.id, page2],
59
+ ]);
60
+
61
+ test("if instanceId is unknown returns undefined", () => {
62
+ expect(
63
+ resolveUrlProp("unknown", pageByIdProp.name, propsByInstanceId, pages)
64
+ ).toBeUndefined();
65
+ });
66
+
67
+ test("if prop name is unknown returns undefined", () => {
68
+ expect(
69
+ resolveUrlProp(instanceId, "unknown", propsByInstanceId, pages)
70
+ ).toBeUndefined();
71
+ });
72
+
73
+ test("page by id", () => {
74
+ expect(
75
+ resolveUrlProp(instanceId, pageByIdProp.name, propsByInstanceId, pages)
76
+ ).toBe(page1);
77
+ });
78
+
79
+ test("page by path", () => {
80
+ expect(
81
+ resolveUrlProp(instanceId, pageByPathProp.name, propsByInstanceId, pages)
82
+ ).toBe(page2);
83
+ });
84
+
85
+ test("arbitrary url", () => {
86
+ expect(
87
+ resolveUrlProp(
88
+ instanceId,
89
+ arbitraryUrlProp.name,
90
+ propsByInstanceId,
91
+ pages
92
+ )
93
+ ).toBe(arbitraryUrlProp.value);
94
+ });
95
+ });
package/src/props.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import { useContext, useMemo } from "react";
2
2
  import { computed } from "nanostores";
3
3
  import { useStore } from "@nanostores/react";
4
- import type { Instance, Prop, Props } from "@webstudio-is/project-build";
4
+ import type { Instance, Page, Prop, Props } from "@webstudio-is/project-build";
5
5
  import type { Asset } from "@webstudio-is/asset-uploader";
6
6
  import { ReactSdkContext } from "./context";
7
+ import { idAttribute } from "./tree/webstudio-component";
7
8
 
8
9
  export type PropsByInstanceId = Map<Instance["id"], Prop[]>;
9
10
 
10
11
  export type Assets = Map<Asset["id"], Asset>;
12
+ export type Pages = Map<Page["id"], Page>;
11
13
 
12
14
  export const getPropsByInstanceId = (props: Props) => {
13
15
  const propsByInstanceId: PropsByInstanceId = new Map();
@@ -39,7 +41,7 @@ export const useInstanceProps = (instanceId: Instance["id"]) => {
39
41
  return instancePropsObject;
40
42
  };
41
43
 
42
- // this utility is be used for image component in both builder and preview
44
+ // this utility is used for image component in both builder and preview
43
45
  // so need to optimize rerenders with computed
44
46
  export const usePropAsset = (instanceId: Instance["id"], name: string) => {
45
47
  const { propsByInstanceIdStore, assetsStore } = useContext(ReactSdkContext);
@@ -49,7 +51,7 @@ export const usePropAsset = (instanceId: Instance["id"], name: string) => {
49
51
  (propsByInstanceId, assets) => {
50
52
  const instanceProps = propsByInstanceId.get(instanceId);
51
53
  if (instanceProps === undefined) {
52
- return undefined;
54
+ return;
53
55
  }
54
56
  for (const prop of instanceProps) {
55
57
  if (prop.type === "asset" && prop.name === name) {
@@ -63,3 +65,57 @@ export const usePropAsset = (instanceId: Instance["id"], name: string) => {
63
65
  const asset = useStore(assetStore);
64
66
  return asset;
65
67
  };
68
+
69
+ export const resolveUrlProp = (
70
+ instanceId: Instance["id"],
71
+ name: string,
72
+ propsByInstanceId: PropsByInstanceId,
73
+ pages: Pages
74
+ ): Page | string | undefined => {
75
+ const instanceProps = propsByInstanceId.get(instanceId);
76
+ if (instanceProps === undefined) {
77
+ return;
78
+ }
79
+ for (const prop of instanceProps) {
80
+ if (prop.name !== name) {
81
+ continue;
82
+ }
83
+
84
+ if (prop.type === "page") {
85
+ return pages.get(prop.value);
86
+ }
87
+
88
+ if (prop.type === "string") {
89
+ for (const page of pages.values()) {
90
+ if (page.path === prop.value) {
91
+ return page;
92
+ }
93
+ }
94
+ return prop.value;
95
+ }
96
+
97
+ return;
98
+ }
99
+ };
100
+
101
+ // this utility is used for link component in both builder and preview
102
+ // so need to optimize rerenders with computed
103
+ export const usePropUrl = (instanceId: Instance["id"], name: string) => {
104
+ const { propsByInstanceIdStore, pagesStore } = useContext(ReactSdkContext);
105
+ const pageStore = useMemo(
106
+ () =>
107
+ computed(
108
+ [propsByInstanceIdStore, pagesStore],
109
+ (propsByInstanceId, pages) =>
110
+ resolveUrlProp(instanceId, name, propsByInstanceId, pages)
111
+ ),
112
+ [propsByInstanceIdStore, pagesStore, instanceId, name]
113
+ );
114
+ return useStore(pageStore);
115
+ };
116
+
117
+ export const getInstanceIdFromComponentProps = (
118
+ props: Record<string, unknown>
119
+ ) => {
120
+ return props[idAttribute] as string;
121
+ };
@@ -4,7 +4,7 @@ import { Scripts, ScrollRestoration } from "@remix-run/react";
4
4
  import type { Instance } from "@webstudio-is/project-build";
5
5
  import type { GetComponent } from "../components/components-utils";
6
6
  import { ReactSdkContext } from "../context";
7
- import type { Assets, PropsByInstanceId } from "../props";
7
+ import type { Assets, Pages, PropsByInstanceId } from "../props";
8
8
  import type { WebstudioComponent } from "./webstudio-component";
9
9
  import { SessionStoragePolyfill } from "./session-storage-polyfill";
10
10
 
@@ -15,6 +15,7 @@ export const createElementsTree = ({
15
15
  instance,
16
16
  propsByInstanceIdStore,
17
17
  assetsStore,
18
+ pagesStore,
18
19
  Component,
19
20
  getComponent,
20
21
  }: {
@@ -22,6 +23,7 @@ export const createElementsTree = ({
22
23
  instance: Instance;
23
24
  propsByInstanceIdStore: ReadableAtom<PropsByInstanceId>;
24
25
  assetsStore: ReadableAtom<Assets>;
26
+ pagesStore: ReadableAtom<Pages>;
25
27
  Component: (props: ComponentProps<typeof WebstudioComponent>) => JSX.Element;
26
28
  getComponent: GetComponent;
27
29
  }) => {
@@ -47,7 +49,9 @@ export const createElementsTree = ({
47
49
  getComponent,
48
50
  });
49
51
  return (
50
- <ReactSdkContext.Provider value={{ propsByInstanceIdStore, assetsStore }}>
52
+ <ReactSdkContext.Provider
53
+ value={{ propsByInstanceIdStore, assetsStore, pagesStore }}
54
+ >
51
55
  {root}
52
56
  </ReactSdkContext.Provider>
53
57
  );
package/src/tree/root.ts CHANGED
@@ -18,6 +18,7 @@ import type { GetComponent } from "../components/components-utils";
18
18
 
19
19
  export type Data = {
20
20
  page: Page;
21
+ pages: Array<Page>;
21
22
  build: Build;
22
23
  assets: Array<Asset>;
23
24
  params?: Params;
@@ -86,6 +87,7 @@ export const InstanceRoot = ({
86
87
  getPropsByInstanceId(new Map(data.build.props))
87
88
  ),
88
89
  assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))),
90
+ pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))),
89
91
  Component: Component ?? WebstudioComponent,
90
92
  getComponent,
91
93
  });