diff --git a/apps/designer/app/canvas/canvas.tsx b/apps/designer/app/canvas/canvas.tsx index 790e86bef3..c3e9ed851c 100644 --- a/apps/designer/app/canvas/canvas.tsx +++ b/apps/designer/app/canvas/canvas.tsx @@ -4,6 +4,9 @@ import type { Build, CanvasData } from "@webstudio-is/project"; import { createElementsTree, useAllUserProps, + registerComponents, + customComponents, + setParams, type OnChangeChildren, type Tree, } from "@webstudio-is/react-sdk"; @@ -139,6 +142,10 @@ export const Canvas = ({ data }: CanvasProps): JSX.Element | null => { const assets = useAssets(data.assets); useAllUserProps(data.props); usePopulateRootInstance(data.tree); + setParams(data.params ?? null); + + registerComponents(customComponents); + // e.g. toggling preview is still needed in both modes useShortcuts(); const isPreviewMode = useSubscribePreviewMode(); diff --git a/apps/designer/app/canvas/features/text-editor/interop.ts b/apps/designer/app/canvas/features/text-editor/interop.ts index db7fdb779d..9f12c7daf1 100644 --- a/apps/designer/app/canvas/features/text-editor/interop.ts +++ b/apps/designer/app/canvas/features/text-editor/interop.ts @@ -32,22 +32,20 @@ const $writeUpdates = ( const children = node.getChildren(); for (const child of children) { if ($isParagraphNode(child)) { - if (updates.length !== 0) { - updates.push("\n"); - } $writeUpdates(child, updates, refs); } if ($isLineBreakNode(child)) { - if (updates.length !== 0) { - // @todo should we visually distinct line breaks and paragraphs? - updates.push("\n"); - } + updates.push("\n"); } if ($isLinkNode(child)) { const id = refs.get(child.getKey()); const childrenUpdates: ChildrenUpdates = []; $writeUpdates(child, childrenUpdates, refs); - updates.push({ id, component: "Link", children: childrenUpdates }); + updates.push({ + id, + component: "RichTextLink", + children: childrenUpdates, + }); } if ($isTextNode(child)) { // support nesting bold into italic and vice versa @@ -115,7 +113,7 @@ const $writeLexical = ( } // convert instances - if (child.component === "Link" && $isElementNode(parent)) { + if (child.component === "RichTextLink" && $isElementNode(parent)) { const linkNode = $createLinkNode(""); refs.set(linkNode.getKey(), child.id); parent.append(linkNode); diff --git a/apps/designer/app/canvas/features/text-editor/text-editor.tsx b/apps/designer/app/canvas/features/text-editor/text-editor.tsx index a9e23aa0f9..4828188d52 100644 --- a/apps/designer/app/canvas/features/text-editor/text-editor.tsx +++ b/apps/designer/app/canvas/features/text-editor/text-editor.tsx @@ -1,4 +1,14 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useLayoutEffect } from "react"; +import { + KEY_ENTER_COMMAND, + INSERT_LINE_BREAK_COMMAND, + COMMAND_PRIORITY_EDITOR, + RootNode, + ElementNode, + $createLineBreakNode, + $getSelection, + $isRangeSelection, +} from "lexical"; import { LinkNode } from "@lexical/link"; import { LexicalComposer } from "@lexical/react/LexicalComposer"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; @@ -39,6 +49,65 @@ const AutofocusPlugin = () => { return null; }; +const RemoveParagaphsPlugin = () => { + const [editor] = useLexicalComposerContext(); + + // register own commands before RichTextPlugin + // to stop propagation + useLayoutEffect(() => { + const removeCommand = editor.registerCommand( + KEY_ENTER_COMMAND, + (event) => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return false; + } + event?.preventDefault(); + // returns true which stops propagation + return editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false); + }, + COMMAND_PRIORITY_EDITOR + ); + + // merge pasted paragraphs into single one + // and separate lines with line breaks + const removeNodeTransform = editor.registerNodeTransform( + RootNode, + (node) => { + // merge paragraphs into first with line breaks between + if (node.getChildrenSize() > 1) { + const children = node.getChildren(); + let first; + for (let index = 0; index < children.length; index += 1) { + const paragraph = children[index]; + // With default configuration root contains only paragraphs. + // Lexical converts headings to paragraphs on paste for example. + // So he we just check root children which are all paragraphs. + if (paragraph instanceof ElementNode) { + if (index === 0) { + first = paragraph; + } else if (first) { + first.append($createLineBreakNode()); + for (const child of paragraph.getChildren()) { + first.append(child); + } + paragraph.remove(); + } + } + } + } + } + ); + + return () => { + removeCommand(); + removeNodeTransform(); + }; + }, [editor]); + + return null; +}; + const onError = (error: Error) => { throw error; }; @@ -54,12 +123,16 @@ export const TextEditor = ({ contentEditable, onChange, }: TextEditorProps) => { + const [paragraphClassName] = useState(() => nanoid()); const [italicClassName] = useState(() => nanoid()); - // initially instance styles are applied to all nodes - // so not necessary to use layout effect - useEffect(() => { + useLayoutEffect(() => { const engine = createCssEngine(); + // reset paragraph styles and make it work inside + engine.addPlaintextRule(` + .${paragraphClassName} { display: inline; margin: 0; } + `); + /// set italic style for bold italic combination on the same element engine.addPlaintextRule(` .${italicClassName} { font-style: italic; } `); @@ -67,7 +140,7 @@ export const TextEditor = ({ return () => { engine.unmount(); }; - }, [italicClassName]); + }, [paragraphClassName, italicClassName]); // store references separately because lexical nodes // cannot store custom data @@ -76,6 +149,7 @@ export const TextEditor = ({ const initialConfig = { namespace: "WsTextEditor", theme: { + paragraph: paragraphClassName, text: { italic: italicClassName, }, @@ -93,6 +167,7 @@ export const TextEditor = ({ return ( + { - if (instance.component === "Link") { + if ( + instance.component === "Link" || + instance.component === "RichTextLink" + ) { event.preventDefault(); } }, diff --git a/apps/designer/app/canvas/shared/instance.ts b/apps/designer/app/canvas/shared/instance.ts index 2dba11d9d5..0c9bc3ec51 100644 --- a/apps/designer/app/canvas/shared/instance.ts +++ b/apps/designer/app/canvas/shared/instance.ts @@ -4,7 +4,7 @@ import { type Instance, type InstanceProps, type Tree, - components, + getComponentMeta, allUserPropsContainer, getBrowserStyle, useAllUserProps, @@ -79,7 +79,7 @@ export const findInsertLocation = ( path.reverse(); const parentIndex = path.findIndex( - ({ item }) => components[item.component].canAcceptChildren + ({ item }) => getComponentMeta(item.component).canAcceptChildren ); // Just in case selected Instance is not in the tree for some reason. diff --git a/apps/designer/app/canvas/shared/styles.ts b/apps/designer/app/canvas/shared/styles.ts index 475682cb7b..8ee9aebe9c 100644 --- a/apps/designer/app/canvas/shared/styles.ts +++ b/apps/designer/app/canvas/shared/styles.ts @@ -55,10 +55,6 @@ const helperStyles = [ `[${idAttribute}][contenteditable] { box-shadow: 0 0 0 4px rgb(36 150 255 / 20%) }`, - // Text Editor wraps each line into a p, so we need to make sure there is no jump between regular rendering and editing - `[${idAttribute}][contenteditable] p { - margin: 0 - }`, ]; export const useManageDesignModeStyles = () => { diff --git a/apps/designer/app/canvas/shared/use-drag-drop.ts b/apps/designer/app/canvas/shared/use-drag-drop.ts index 86b251ccfd..9ea65efec1 100644 --- a/apps/designer/app/canvas/shared/use-drag-drop.ts +++ b/apps/designer/app/canvas/shared/use-drag-drop.ts @@ -18,7 +18,7 @@ import { } from "@webstudio-is/design-system"; import { type Instance, - components, + getComponentMeta, BaseInstance, toBaseInstance, } from "@webstudio-is/react-sdk"; @@ -116,7 +116,7 @@ export const useDragAndDrop = () => { } const data = path.find( - (instance) => components[instance.component].canAcceptChildren + (instance) => getComponentMeta(instance.component).canAcceptChildren ); if (data === undefined) { @@ -179,7 +179,7 @@ export const useDragAndDrop = () => { } // When trying to drag an inline instance, drag its parent instead - if (components[instance.component].isInlineOnly) { + if (getComponentMeta(instance.component).isInlineOnly) { const nonInlineParent = utils.tree.findClosestNonInlineParent( rootInstance, instance.id diff --git a/apps/designer/app/canvas/shared/use-shortcuts.ts b/apps/designer/app/canvas/shared/use-shortcuts.ts index 272e3651b3..52ba409c46 100644 --- a/apps/designer/app/canvas/shared/use-shortcuts.ts +++ b/apps/designer/app/canvas/shared/use-shortcuts.ts @@ -1,6 +1,6 @@ import { useHotkeys } from "react-hotkeys-hook"; import store from "immerhin"; -import { type Instance, components } from "@webstudio-is/react-sdk"; +import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk"; import { shortcuts, options } from "~/shared/shortcuts"; import { publish, useSubscribe } from "~/shared/pubsub"; import { useSelectedInstance } from "./nano-states"; @@ -120,7 +120,9 @@ export const useShortcuts = () => { "enter", (event) => { if (selectedInstance === undefined) return; - const { isContentEditable } = components[selectedInstance.component]; + const { isContentEditable } = getComponentMeta( + selectedInstance.component + ); if (isContentEditable === false) return; // Prevents inserting a newline when entering text-editing mode event.preventDefault(); diff --git a/apps/designer/app/canvas/shared/use-track-selected-element.ts b/apps/designer/app/canvas/shared/use-track-selected-element.ts index 6efdf1f5c2..51cd52764e 100644 --- a/apps/designer/app/canvas/shared/use-track-selected-element.ts +++ b/apps/designer/app/canvas/shared/use-track-selected-element.ts @@ -1,5 +1,9 @@ import { useCallback, useEffect, useRef } from "react"; -import { type Instance, components } from "@webstudio-is/react-sdk"; +import { + type Instance, + getComponentMeta, + getComponentNames, +} from "@webstudio-is/react-sdk"; import { useSubscribe } from "~/shared/pubsub"; import { useSelectedElement, useSelectedInstance } from "./nano-states"; import { @@ -87,14 +91,15 @@ export const useTrackSelectedElement = () => { if (editingInstanceIdRef.current === id) { return; } + const components = getComponentNames(); // It's the second click in a double click. if (event.detail === 2) { const component = dataset.wsComponent as Instance["component"]; - if (component === undefined || component in components === false) { + if (component === undefined || !components.includes(component)) { return; } - const { isInlineOnly, isContentEditable } = components[component]; + const { isInlineOnly, isContentEditable } = getComponentMeta(component); // When user double clicks on an inline instance, we need to select the parent instance and put it indo text editing mode. // Inline instances are not editable directly, only through parent instance. @@ -102,7 +107,7 @@ export const useTrackSelectedElement = () => { const parent = rootInstance && utils.tree.findClosestNonInlineParent(rootInstance, id); - if (parent && components[parent.component].isContentEditable) { + if (parent && getComponentMeta(parent.component).isContentEditable) { selectInstance(parent.id); setEditingInstanceId(parent.id); } diff --git a/apps/designer/app/dashboard/components/header.tsx b/apps/designer/app/dashboard/components/header.tsx index e49304e337..682a2a04a1 100644 --- a/apps/designer/app/dashboard/components/header.tsx +++ b/apps/designer/app/dashboard/components/header.tsx @@ -9,10 +9,14 @@ import { Text, Flex, } from "@webstudio-is/design-system"; -import { User } from "@webstudio-is/prisma-client"; +import { User as DbUser } from "@webstudio-is/prisma-client"; import { useNavigate } from "react-router-dom"; import { logoutPath } from "~/shared/router-utils"; +type User = Omit & { + createdAt: string; +}; + export const DashboardHeader = ({ user }: { user: User }) => { const navigate = useNavigate(); const userNameFallback = (user?.username || user?.email || "X") diff --git a/apps/designer/app/dashboard/dashboard.tsx b/apps/designer/app/dashboard/dashboard.tsx index e96d6c9c36..f3dab6e6c6 100644 --- a/apps/designer/app/dashboard/dashboard.tsx +++ b/apps/designer/app/dashboard/dashboard.tsx @@ -3,9 +3,10 @@ import { useActionData } from "@remix-run/react"; import { Flex } from "@webstudio-is/design-system"; import interStyles from "~/shared/font-faces/inter.css"; import dashboardStyles from "./dashboard.css"; -import { User } from "@webstudio-is/prisma-client"; +import { User as DbUser } from "@webstudio-is/prisma-client"; import { DashboardHeader } from "./components/header"; import { SelectProjectCard } from "./components/card"; + export const links = () => { return [ { @@ -19,6 +20,10 @@ export const links = () => { ]; }; +type User = Omit & { + createdAt: string; +}; + type DashboardProps = { projects?: Array<{ id: string; title: string }>; user: User; diff --git a/apps/designer/app/designer/features/props-panel/control.tsx b/apps/designer/app/designer/features/props-panel/control.tsx index dab2e52657..7fe07995a4 100644 --- a/apps/designer/app/designer/features/props-panel/control.tsx +++ b/apps/designer/app/designer/features/props-panel/control.tsx @@ -1,5 +1,5 @@ import { - componentsMeta, + getComponentMetaProps, type Instance, type UserProp, } from "@webstudio-is/react-sdk"; @@ -13,7 +13,6 @@ import { Switch, TextField, } from "@webstudio-is/design-system"; -import { isFeatureEnabled } from "~/shared/feature-flags"; import { ValuePickerPopover } from "../style-panel/shared/value-picker-popover"; import { ImageManager } from "~/designer/shared/image-manager"; @@ -183,11 +182,13 @@ export function Control({ userProp, onChangePropValue, }: ControlProps) { - const meta = componentsMeta[component]; - const argType = meta[userProp.prop as keyof typeof meta]; + const meta = getComponentMetaProps(component); - const defaultValue = argType.defaultValue; - const type = argType.type; + const argType = meta[userProp.prop]; + + // argType can be undefined in case of new property created + const defaultValue = argType?.defaultValue ?? ""; + const type = argType?.type ?? "text"; if (type == null) { warnOnce( @@ -208,17 +209,15 @@ export function Control({ return ; } - if (isFeatureEnabled("assets")) { - if (component === "Image" && userProp.prop === "src") { - const asset = userProp.asset ?? null; + if (component === "Image" && userProp.prop === "src") { + const asset = userProp.asset ?? null; - return ( - onChangePropValue(asset.path, asset)} - /> - ); - } + return ( + onChangePropValue(asset.path, asset)} + /> + ); } if (includes(textControlTypes, type)) { @@ -242,12 +241,12 @@ export function Control({ } if ( - argType.type === "radio" || - argType.type === "inline-radio" || - argType.type === "check" || - argType.type === "inline-check" || - argType.type === "multi-select" || - argType.type === "select" + argType?.type === "radio" || + argType?.type === "inline-radio" || + argType?.type === "check" || + argType?.type === "inline-check" || + argType?.type === "multi-select" || + argType?.type === "select" ) { const options = argType.options; diff --git a/apps/designer/app/designer/features/props-panel/props-panel.stories.tsx b/apps/designer/app/designer/features/props-panel/props-panel.stories.tsx index 1502a804b1..8e76e5fc89 100644 --- a/apps/designer/app/designer/features/props-panel/props-panel.stories.tsx +++ b/apps/designer/app/designer/features/props-panel/props-panel.stories.tsx @@ -1,5 +1,5 @@ import { ComponentStoryObj } from "@storybook/react"; -import { componentsMeta, type UserProp } from "@webstudio-is/react-sdk"; +import { getComponentMetaProps, type UserProp } from "@webstudio-is/react-sdk"; import { PropsPanel } from "./props-panel"; export default { @@ -64,7 +64,7 @@ export const DefaultProps: ComponentStoryObj = { }, }; -const meta = componentsMeta["Button"]; +const meta = getComponentMetaProps("Button"); export const AllProps: ComponentStoryObj = { args: { @@ -81,7 +81,7 @@ export const AllProps: ComponentStoryObj = { return { id: prop, prop, - value: value.defaultValue, + value: value?.defaultValue ?? "", } as UserProp; }), }, diff --git a/apps/designer/app/designer/features/props-panel/props-panel.tsx b/apps/designer/app/designer/features/props-panel/props-panel.tsx index 892dcdcd78..a19b01f331 100644 --- a/apps/designer/app/designer/features/props-panel/props-panel.tsx +++ b/apps/designer/app/designer/features/props-panel/props-panel.tsx @@ -1,5 +1,5 @@ import { - componentsMeta, + getComponentMetaProps, type Instance, type UserProp, } from "@webstudio-is/react-sdk"; @@ -121,7 +121,7 @@ const Property = ({ onChangePropValue, onDelete, }: PropertyProps) => { - const meta = componentsMeta[component]; + const meta = getComponentMetaProps(component); const argType = meta[userProp.prop as keyof typeof meta]; const isInvalid = @@ -142,7 +142,8 @@ const Property = ({ const argType = meta[name as keyof typeof meta]; const defaultValue = - argType?.defaultValue ?? argType?.type === "boolean" ? false : ""; + argType?.defaultValue ?? + (argType?.type === "boolean" ? false : ""); onChangePropName(name, defaultValue); } @@ -151,7 +152,7 @@ const Property = ({ const argType = meta[name as keyof typeof meta]; const defaultValue = - argType?.defaultValue ?? argType?.type === "boolean" ? false : ""; + argType?.defaultValue ?? (argType?.type === "boolean" ? false : ""); onChangePropName(name, defaultValue); }} diff --git a/apps/designer/app/designer/features/props-panel/use-props-logic.test.ts b/apps/designer/app/designer/features/props-panel/use-props-logic.test.ts index 2410e67056..059ecd93af 100644 --- a/apps/designer/app/designer/features/props-panel/use-props-logic.test.ts +++ b/apps/designer/app/designer/features/props-panel/use-props-logic.test.ts @@ -1,11 +1,11 @@ import { renderHook, act } from "@testing-library/react-hooks"; -import { components, UserProp } from "@webstudio-is/react-sdk"; +import { type ComponentName, UserProp } from "@webstudio-is/react-sdk"; import { nanoid } from "nanoid"; import type { SelectedInstanceData } from "@webstudio-is/project"; import { usePropsLogic } from "./use-props-logic"; const getSelectedInstanceData = ( - componentName: keyof typeof components, + componentName: ComponentName, props: UserProp[] ): SelectedInstanceData => { return { @@ -350,7 +350,7 @@ describe("usePropsLogic", () => { name: "string", description: "string", location: "REMOTE", - createdAt: new Date("1995-12-17T03:24:00Z"), + createdAt: new Date("1995-12-17T03:24:00Z").toISOString(), meta: { width: 101, height: 202 }, path: "string", status: "uploaded", @@ -361,7 +361,7 @@ describe("usePropsLogic", () => { [ { "asset": { - "createdAt": 1995-12-17T03:24:00.000Z, + "createdAt": "1995-12-17T03:24:00.000Z", "description": "string", "format": "string", "id": "string", diff --git a/apps/designer/app/designer/features/props-panel/use-props-logic.ts b/apps/designer/app/designer/features/props-panel/use-props-logic.ts index 4e2c76794d..9c36ae6976 100644 --- a/apps/designer/app/designer/features/props-panel/use-props-logic.ts +++ b/apps/designer/app/designer/features/props-panel/use-props-logic.ts @@ -4,7 +4,7 @@ import type { UserPropsUpdates, } from "@webstudio-is/react-sdk"; import { type Publish } from "~/shared/pubsub"; -import { componentsMeta } from "@webstudio-is/react-sdk"; +import { getComponentMetaProps } from "@webstudio-is/react-sdk"; import ObjectId from "bson-objectid"; import produce from "immer"; import uniqBy from "lodash/uniqBy"; @@ -37,9 +37,9 @@ const getRequiredProps = ( selectedInstanceData: SelectedInstanceData ): UserProp[] => { const { component } = selectedInstanceData; - const meta = componentsMeta[component]; + const meta = getComponentMetaProps(component); return Object.entries(meta) - .filter(([_, value]) => value.required) + .filter(([_, value]) => value?.required) .map(([prop, _]) => ({ id: ObjectId().toString(), prop, @@ -54,12 +54,11 @@ const getPropsWithDefaultValue = ( selectedInstanceData: SelectedInstanceData ): UserProp[] => { const { component } = selectedInstanceData; - const meta = componentsMeta[component]; + const meta = getComponentMetaProps(component); return Object.entries(meta) - .filter(([_, value]) => value.defaultValue != null) + .filter(([_, value]) => value?.defaultValue != null) .map(([prop, propObj]) => { - const { defaultValue } = propObj; - const value = defaultValue ?? ""; + const value = propObj?.defaultValue ?? ""; return { id: ObjectId().toString(), prop, diff --git a/apps/designer/app/designer/features/sidebar-left/panels/components/component-thumb.tsx b/apps/designer/app/designer/features/sidebar-left/panels/components/component-thumb.tsx index f2544d51b6..2fabc04a7f 100644 --- a/apps/designer/app/designer/features/sidebar-left/panels/components/component-thumb.tsx +++ b/apps/designer/app/designer/features/sidebar-left/panels/components/component-thumb.tsx @@ -1,4 +1,4 @@ -import { type Instance, components } from "@webstudio-is/react-sdk"; +import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk"; import { forwardRef, type ElementRef, type ComponentProps } from "react"; import { Flex, Text, styled } from "@webstudio-is/design-system"; @@ -30,7 +30,7 @@ export const ComponentThumb = forwardRef< ElementRef, ComponentThumbProps >(({ component, ...rest }, ref) => { - const { Icon, label } = components[component]; + const { Icon, label } = getComponentMeta(component); return ( -).filter((component) => components[component].isListed); - type DraggableThumbProps = { onClick: MouseEventHandler; component: Instance["component"]; @@ -65,7 +66,10 @@ type TabContentProps = { publish: Publish; }; -const elementToComponentName = (element: Element) => { +const elementToComponentName = ( + element: Element, + listedComponentNames: ComponentName[] +) => { // If drag doesn't start on the button element directly but on one of its children, // we need to trace back to the button that has the data. const parentWithData = element.closest("[data-drag-component]"); @@ -74,7 +78,7 @@ const elementToComponentName = (element: Element) => { return; } const { dragComponent } = parentWithData.dataset; - return componentNames.find((component) => component === dragComponent); + return listedComponentNames.find((component) => component === dragComponent); }; export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => { @@ -92,9 +96,16 @@ export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => { return { x: (x - canvasRect.x) / scale, y: (y - canvasRect.y) / scale }; }; + const listedComponentNames = getComponentNames().filter( + (name) => getComponentMeta(name).isListed + ); + const useDragHandlers = useDrag({ elementToData(element) { - const componentName = elementToComponentName(element); + const componentName = elementToComponentName( + element, + listedComponentNames + ); if (componentName === undefined) { return false; } @@ -142,7 +153,7 @@ export const TabContent = ({ publish, onSetActiveTab }: TabContentProps) => { css={{ padding: "$spacing$3", overflow: "auto" }} ref={useDragHandlers.rootRef} > - {componentNames.map((component: Instance["component"]) => ( + {listedComponentNames.map((component: Instance["component"]) => ( { switch (panel) { case "designTokens": return isFeatureEnabled("designTokens"); - case "assets": - return isFeatureEnabled("assets"); case "navigator": return clientSettings.navigatorLayout === "docked"; } diff --git a/apps/designer/app/designer/features/style-panel/shared/configs.ts b/apps/designer/app/designer/features/style-panel/shared/configs.ts index 8ff27de91a..1698740f5b 100644 --- a/apps/designer/app/designer/features/style-panel/shared/configs.ts +++ b/apps/designer/app/designer/features/style-panel/shared/configs.ts @@ -32,7 +32,6 @@ import { RowGapIcon, ColumnGapIcon, } from "@webstudio-is/icons"; -import { isFeatureEnabled } from "~/shared/feature-flags"; import { keywordValues, properties } from "@webstudio-is/css-data"; type BaseStyleConfig = { @@ -68,7 +67,7 @@ const getControl = (property: StyleProperty): Control => { return "FontFamilyControl"; } case "backgroundImage": { - return isFeatureEnabled("assets") ? "ImageControl" : "TextControl"; + return "ImageControl"; } case "fontWeight": { return "FontWeightControl"; diff --git a/apps/designer/app/designer/features/workspace/canvas-tools/outline/label.tsx b/apps/designer/app/designer/features/workspace/canvas-tools/outline/label.tsx index bcc4afa305..a301856f64 100644 --- a/apps/designer/app/designer/features/workspace/canvas-tools/outline/label.tsx +++ b/apps/designer/app/designer/features/workspace/canvas-tools/outline/label.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import { styled, type Rect } from "@webstudio-is/design-system"; -import { type Instance, components } from "@webstudio-is/react-sdk"; +import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk"; type LabelPosition = "top" | "inside" | "bottom"; type LabelRefCallback = (element: HTMLElement | null) => void; @@ -74,7 +74,7 @@ type LabelProps = { export const Label = ({ component, instanceRect }: LabelProps) => { const [labelRef, position] = useLabelPosition(instanceRect); - const { Icon, label } = components[component]; + const { Icon, label } = getComponentMeta(component); return ( diff --git a/apps/designer/app/designer/shared/inspector/component-info.tsx b/apps/designer/app/designer/shared/inspector/component-info.tsx index bd5a5b89cd..bee801a1cd 100644 --- a/apps/designer/app/designer/shared/inspector/component-info.tsx +++ b/apps/designer/app/designer/shared/inspector/component-info.tsx @@ -1,4 +1,4 @@ -import { components } from "@webstudio-is/react-sdk"; +import { getComponentMeta } from "@webstudio-is/react-sdk"; import { Flex, Text } from "@webstudio-is/design-system"; import type { SelectedInstanceData } from "@webstudio-is/project"; @@ -15,7 +15,9 @@ export const ComponentInfo = ({ color: "$colors$slate11", fontWeight: "500", }} - >{`Selected: ${components[selectedInstanceData.component].label}`} + >{`Selected: ${ + getComponentMeta(selectedInstanceData.component).label + }`} ); }; diff --git a/apps/designer/app/designer/shared/tree/index.tsx b/apps/designer/app/designer/shared/tree/index.tsx index 9d7cbacfdc..5628c991d4 100644 --- a/apps/designer/app/designer/shared/tree/index.tsx +++ b/apps/designer/app/designer/shared/tree/index.tsx @@ -7,7 +7,7 @@ import { type TreeProps, type TreeItemRenderProps, } from "@webstudio-is/design-system"; -import { components, Instance } from "@webstudio-is/react-sdk"; +import { getComponentMeta, Instance } from "@webstudio-is/react-sdk"; import { utils } from "@webstudio-is/project"; const instanceRelatedProps = { @@ -15,13 +15,13 @@ const instanceRelatedProps = { getItemPath: utils.tree.getInstancePath, getItemPathWithPositions: utils.tree.getInstancePathWithPositions, canLeaveParent(item: Instance) { - return components[item.component].isInlineOnly !== true; + return getComponentMeta(item.component).isInlineOnly !== true; }, canAcceptChild(item: Instance) { - return components[item.component].canAcceptChildren; + return getComponentMeta(item.component).canAcceptChildren; }, getItemChildren(item: Instance) { - const component = components[item.component]; + const component = getComponentMeta(item.component); // We want to avoid calling .filter() unnecessarily, because this is a hot path for performance. // We rely on the fact that only content editable or inline components may have `string` children. @@ -37,7 +37,7 @@ const instanceRelatedProps = { ) as Instance[]; }, renderItem(props: TreeItemRenderProps) { - const { Icon, label } = components[props.itemData.component]; + const { Icon, label } = getComponentMeta(props.itemData.component); return ( }>{label} diff --git a/apps/designer/app/env.server.ts b/apps/designer/app/env.server.ts index 06738d30d0..1c26652878 100644 --- a/apps/designer/app/env.server.ts +++ b/apps/designer/app/env.server.ts @@ -9,6 +9,8 @@ const env = { PUBLISHER_ENDPOINT: process.env.PUBLISHER_ENDPOINT || null, PUBLISHER_HOST: process.env.PUBLISHER_HOST || null, BUILD_REQUIRE_SUBDOMAIN: process.env.BUILD_REQUIRE_SUBDOMAIN === "true", + // Must be set for Vercel deployments + RESIZE_ORIGIN: process.env.RESIZE_ORIGIN, } as const; export default env; diff --git a/apps/designer/app/root.tsx b/apps/designer/app/root.tsx index e7e178baa7..6de81f2444 100644 --- a/apps/designer/app/root.tsx +++ b/apps/designer/app/root.tsx @@ -7,7 +7,6 @@ import { OutletProps } from "react-router-dom"; import { PersistentFetcherProvider } from "./shared/fetcher"; const RootWithErrorBoundary = (props: OutletProps) => ( - // @ts-expect-error 'ErrorBoundary' cannot be used as a JSX component. diff --git a/apps/designer/app/routes/$.tsx b/apps/designer/app/routes/$.tsx index 9b08b63087..7fd42bbd59 100644 --- a/apps/designer/app/routes/$.tsx +++ b/apps/designer/app/routes/$.tsx @@ -1,8 +1,8 @@ -import { - redirect, - json, - type LoaderFunction, - type MetaFunction, +import { redirect, json } from "@remix-run/node"; +import type { + MetaFunction, + LoaderFunction, + ErrorBoundaryComponent, } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { InstanceRoot, Root } from "@webstudio-is/react-sdk"; @@ -20,9 +20,7 @@ import { db } from "@webstudio-is/project/server"; import type { DynamicLinksFunction } from "remix-utils"; import type { CanvasData } from "@webstudio-is/project"; -type Data = - | (CanvasData & { env: Env; mode: BuildMode }) - | { errors: string; env: Env }; +type Data = CanvasData & { env: Env; mode: BuildMode }; export const dynamicLinks: DynamicLinksFunction = ({ data, @@ -42,64 +40,53 @@ export const dynamicLinks: DynamicLinksFunction = ({ export const handle = { dynamicLinks }; export const meta: MetaFunction = ({ data }: { data: Data }) => { - if ("errors" in data) { - return { title: "Error" }; - } const { page } = data; return { title: page.title, ...page.meta }; }; -export const loader: LoaderFunction = async ({ - request, -}): Promise => { - try { - const buildParams = getBuildParams(request); +export const loader: LoaderFunction = async ({ request }): Promise => { + const buildParams = getBuildParams(request); - if (buildParams === undefined) { - return redirect(dashboardPath()); - } - - const { mode, pathname } = buildParams; - - const project = await db.project.loadByParams(buildParams); - - if (project === null) { - throw json("Project not found", { status: 404 }); - } - - const canvasData = await loadCanvasData( - project, - mode === "published" ? "prod" : "dev", - pathname - ); - - if (canvasData === undefined) { - throw json("Page not found", { status: 404 }); - } - - return { ...canvasData, env, mode }; - } catch (error) { - // If a Response is thrown, we're rethrowing it for Remix to handle. - // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders - if (error instanceof Response) { - throw error; - } - - sentryException({ error }); - return { - errors: error instanceof Error ? error.message : String(error), - env, - }; + if (buildParams === undefined) { + throw redirect(dashboardPath()); } + + const { mode, pathname } = buildParams; + + const project = await db.project.loadByParams(buildParams); + + if (project === null) { + throw json("Project not found", { status: 404 }); + } + + const canvasData = await loadCanvasData( + project, + mode === "published" ? "prod" : "dev", + pathname + ); + + if (canvasData === undefined) { + throw json("Page not found", { status: 404 }); + } + + const params: CanvasData["params"] = {}; + + if (env.RESIZE_ORIGIN != null) { + params.resizeOrigin = env.RESIZE_ORIGIN; + } + + return { ...canvasData, env, mode, params }; +}; + +export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => { + sentryException({ error }); + const message = error instanceof Error ? error.message : String(error); + return ; }; const Content = () => { const data = useLoaderData(); - if ("errors" in data) { - return ; - } - const Outlet = data.mode === "edit" ? () => diff --git a/apps/designer/app/routes/dashboard/index.tsx b/apps/designer/app/routes/dashboard/index.tsx index 7e514a5ae8..64e5dbd419 100644 --- a/apps/designer/app/routes/dashboard/index.tsx +++ b/apps/designer/app/routes/dashboard/index.tsx @@ -10,9 +10,13 @@ import { db } from "@webstudio-is/project/server"; import { ensureUserCookie } from "~/shared/session"; import { authenticator } from "~/services/auth.server"; import { zfd } from "zod-form-data"; -import { type Project, User } from "@webstudio-is/prisma-client"; +import { type Project, User as DbUser } from "@webstudio-is/prisma-client"; import { designerPath, loginPath } from "~/shared/router-utils"; +type User = Omit & { + createdAt: string; +}; + export { links }; const schema = zfd.formData({ project: zfd.text(), diff --git a/apps/designer/app/routes/designer/$projectId.tsx b/apps/designer/app/routes/designer/$projectId.tsx index 6f2604c951..52ffcc5cea 100644 --- a/apps/designer/app/routes/designer/$projectId.tsx +++ b/apps/designer/app/routes/designer/$projectId.tsx @@ -1,5 +1,5 @@ import { useLoaderData } from "@remix-run/react"; -import { LoaderFunction } from "@remix-run/node"; +import type { ErrorBoundaryComponent, LoaderFunction } from "@remix-run/node"; import { type DesignerProps, Designer, links } from "~/designer"; import { db } from "@webstudio-is/project/server"; import { ErrorMessage } from "~/shared/error"; @@ -11,45 +11,38 @@ export { links }; export const loader: LoaderFunction = async ({ params, request, -}): Promise => { - try { - if (params.projectId === undefined) { - throw new Error("Project id undefined"); - } - - const url = new URL(request.url); - const pageIdParam = url.searchParams.get("pageId"); - - const project = await db.project.loadById(params.projectId); - - if (project === null) { - throw new Error(`Project "${params.projectId}" not found`); - } - - const devBuild = await db.build.loadByProjectId(project.id, "dev"); - - return { - project, - pages: devBuild.pages, - pageId: pageIdParam || devBuild.pages.homePage.id, - buildOrigin: getBuildOrigin(request), - }; - } catch (error) { - sentryException({ error }); - return { errors: error instanceof Error ? error.message : String(error) }; +}): Promise => { + if (params.projectId === undefined) { + throw new Error("Project id undefined"); } + + const url = new URL(request.url); + const pageIdParam = url.searchParams.get("pageId"); + + const project = await db.project.loadById(params.projectId); + + if (project === null) { + throw new Error(`Project "${params.projectId}" not found`); + } + + const devBuild = await db.build.loadByProjectId(project.id, "dev"); + + return { + project, + pages: devBuild.pages, + pageId: pageIdParam || devBuild.pages.homePage.id, + buildOrigin: getBuildOrigin(request), + }; }; -type Error = { - errors: string; +export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => { + sentryException({ error }); + const message = error instanceof Error ? error.message : String(error); + return ; }; export const DesignerRoute = () => { - const data = useLoaderData(); - - if ("errors" in data) { - return ; - } + const data = useLoaderData(); return ; }; diff --git a/apps/designer/app/routes/index.tsx b/apps/designer/app/routes/index.tsx index 0b4d5e85c8..1c4b439020 100644 --- a/apps/designer/app/routes/index.tsx +++ b/apps/designer/app/routes/index.tsx @@ -3,11 +3,16 @@ // see https://github.com/remix-run/remix/issues/2098#issuecomment-1049262218 . // To solve this, we're re-exporting the $.tsx route API in index.tsx -import type { LoaderFunction, MetaFunction } from "@remix-run/node"; +import type { + ErrorBoundaryComponent, + LoaderFunction, + MetaFunction, +} from "@remix-run/node"; import CatchAllContnet, { loader as catchAllloader, meta as catchAllmeta, handle as catchAllHandle, + ErrorBoundary as CatchAllErrorBoundary, } from "./$"; // We're wrapping functions in order for them to be distinct from the ones in $.tsx. @@ -16,5 +21,8 @@ import CatchAllContnet, { export const meta: MetaFunction = (args) => catchAllmeta(args); export const loader: LoaderFunction = (args) => catchAllloader(args); export const handle = catchAllHandle; +export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => ( + +); const Content = () => ; export default Content; diff --git a/apps/designer/app/shared/feature-flags/flags.ts b/apps/designer/app/shared/feature-flags/flags.ts index 9a1c3089a9..972e842029 100644 --- a/apps/designer/app/shared/feature-flags/flags.ts +++ b/apps/designer/app/shared/feature-flags/flags.ts @@ -1,3 +1,2 @@ -export const assets = false; export const designTokens = false; export const example = false; diff --git a/packages/asset-uploader/package.json b/packages/asset-uploader/package.json index 96fc22d95c..d4304c4b01 100644 --- a/packages/asset-uploader/package.json +++ b/packages/asset-uploader/package.json @@ -1,6 +1,6 @@ { "name": "@webstudio-is/asset-uploader", - "version": "0.2.0", + "version": "0.3.0", "description": "Webstudio Asset Uploader", "author": "Webstudio ", "homepage": "https://webstudio.is", diff --git a/packages/asset-uploader/src/types.ts b/packages/asset-uploader/src/types.ts index f511a227dc..79d01291f9 100644 --- a/packages/asset-uploader/src/types.ts +++ b/packages/asset-uploader/src/types.ts @@ -3,9 +3,10 @@ import type { FontMeta } from "@webstudio-is/fonts/server"; import { Asset as DbAsset } from "@webstudio-is/prisma-client"; import type { ImageMeta } from "./utils/format-asset"; -type BaseAsset = Omit & { +type BaseAsset = Omit & { path: string; status?: "uploaded"; + createdAt: string; }; export type FontAsset = Omit & { diff --git a/packages/asset-uploader/src/utils/format-asset.ts b/packages/asset-uploader/src/utils/format-asset.ts index 1c54e372b2..b89a938ee5 100644 --- a/packages/asset-uploader/src/utils/format-asset.ts +++ b/packages/asset-uploader/src/utils/format-asset.ts @@ -19,6 +19,7 @@ export const formatAsset = (asset: DbAsset): Asset => { if (isFont) { return { ...base, + createdAt: base.createdAt.toISOString(), format: asset.format as FontFormat, meta: FontMeta.parse(JSON.parse(asset.meta)), }; @@ -26,6 +27,7 @@ export const formatAsset = (asset: DbAsset): Asset => { return { ...base, + createdAt: base.createdAt.toISOString(), meta: ImageMeta.parse(JSON.parse(asset.meta)), }; }; diff --git a/packages/css-data/package.json b/packages/css-data/package.json index 3d8ec09d58..dc27d904c7 100644 --- a/packages/css-data/package.json +++ b/packages/css-data/package.json @@ -1,6 +1,6 @@ { "name": "@webstudio-is/css-data", - "version": "0.2.0", + "version": "0.3.0", "description": "CSS Data", "author": "Webstudio ", "homepage": "https://webstudio.is", diff --git a/packages/css-engine/package.json b/packages/css-engine/package.json index dd7a39740f..92d21ac25f 100644 --- a/packages/css-engine/package.json +++ b/packages/css-engine/package.json @@ -1,6 +1,6 @@ { "name": "@webstudio-is/css-engine", - "version": "0.2.0", + "version": "0.3.0", "description": "CSS Renderer for Webstudio", "author": "Webstudio ", "homepage": "https://webstudio.is", diff --git a/packages/design-system/lostpixel.config.ts b/packages/design-system/lostpixel.config.ts index 9214fcc2ae..b323fce887 100644 --- a/packages/design-system/lostpixel.config.ts +++ b/packages/design-system/lostpixel.config.ts @@ -6,6 +6,7 @@ export const config: CustomProjectConfig = { storybookUrl: "packages/design-system/storybook-static", }, lostPixelProjectId: "cl8a8xmx714904901m9oce4vhqc", + lostPixelUrl: 'https://app.gitbased.lost-pixel.com/api/callback', s3: { endPoint: "ams3.digitaloceanspaces.com", accessKey: process.env.S3_ACCESS_KEY, diff --git a/packages/icons/package.json b/packages/icons/package.json index 66ecd93488..2e9fa6cc5d 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@webstudio-is/icons", - "version": "0.2.0", + "version": "0.3.0", "description": "Webstudio Icons", "author": "Webstudio ", "homepage": "https://webstudio.is", diff --git a/packages/image/package.json b/packages/image/package.json index bc7846dc16..9bcb0f9f5d 100644 --- a/packages/image/package.json +++ b/packages/image/package.json @@ -1,6 +1,6 @@ { "name": "@webstudio-is/image", - "version": "0.0.1", + "version": "0.1.0", "description": "Image optimization", "author": "Webstudio ", "homepage": "https://webstudio.is", @@ -11,11 +11,13 @@ "checks": "yarn typecheck && yarn lint && yarn test", "dev": "tsup --watch", "build:args": "generate-arg-types './src/*.tsx !./src/**/*.stories.tsx !./src/**/*.ws.tsx' && prettier --write \"**/*.props.json\"", - "build": "rm -fr lib tsconfig.tsbuildinfo && tsc", + "build": "rm -fr lib tsconfig.tsbuildinfo && yarn build:cjs && tsc", + "build:cjs": "tsc --module commonjs --outDir lib/cjs && find lib/cjs -name '*.js' | while read NAME; do mv $NAME ${NAME%.js}.cjs; done", "lint": "eslint ./src --ext .ts,.tsx --max-warnings 0", "storybook:run": "start-storybook -p 6006", "storybook:build": "build-storybook", - "generate-types": "generate-arg-types" + "generate-types": "generate-arg-types", + "publish-to-npm": "bash ../../bin/publish-to-npm.sh" }, "dependencies": {}, "devDependencies": { @@ -38,8 +40,8 @@ }, "module": "./lib/index.js", "exports": { - ".": "./lib/index.js", - "./server": "./server.js" + "import": "./lib/index.js", + "require": "./lib/cjs/index.cjs" }, "types": "lib/index.d.ts", "files": [ diff --git a/packages/image/src/image-dev.stories.tsx b/packages/image/src/image-dev.stories.tsx index d1573f1279..d9a776f12c 100644 --- a/packages/image/src/image-dev.stories.tsx +++ b/packages/image/src/image-dev.stories.tsx @@ -2,11 +2,7 @@ import React, { type ComponentProps, type HTMLAttributes } from "react"; import type { ComponentMeta, ComponentStory } from "@storybook/react"; -import { - Image as ImagePrimitive, - cloudflareImageLoader, - localImageLoader, -} from "./"; +import { Image as ImagePrimitive, loaders } from "./"; // to not allow include local assets everywhere, just enable it for this file // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -33,8 +29,8 @@ const imageSrc = USE_CLOUDFLARE_IMAGE_TRANSFORM : localLogoImage; const imageLoader = USE_CLOUDFLARE_IMAGE_TRANSFORM - ? cloudflareImageLoader({ resizeOrigin: "https://webstudio.is" }) - : localImageLoader(); + ? loaders.cloudflareImageLoader({ resizeOrigin: "https://webstudio.is" }) + : loaders.localImageLoader(); const ImageBase: ComponentStory< React.ForwardRefExoticComponent< diff --git a/packages/image/src/index.ts b/packages/image/src/index.ts index 1049e16356..62882121e1 100644 --- a/packages/image/src/index.ts +++ b/packages/image/src/index.ts @@ -1,4 +1,4 @@ export { Image } from "./image"; export type { ImageLoader } from "./image-optimize"; -export { cloudflareImageLoader, localImageLoader } from "./image-loaders"; +export * as loaders from "./image-loaders"; export { default as imageProps } from "./__generated__/image.props.json"; diff --git a/packages/project/package.json b/packages/project/package.json index 32dce16744..de93580069 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -1,6 +1,6 @@ { "name": "@webstudio-is/project", - "version": "0.1.0", + "version": "0.2.0", "description": "Webstudio Project", "author": "Webstudio ", "homepage": "https://webstudio.is", diff --git a/packages/project/src/shared/tree-utils/create-instance.ts b/packages/project/src/shared/tree-utils/create-instance.ts index 8995cef2bc..8d50b9bc38 100644 --- a/packages/project/src/shared/tree-utils/create-instance.ts +++ b/packages/project/src/shared/tree-utils/create-instance.ts @@ -1,5 +1,5 @@ import ObjectId from "bson-objectid"; -import { type Instance, components } from "@webstudio-is/react-sdk"; +import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk"; import { CssRule } from "@webstudio-is/css-data"; export const createInstanceId = () => { @@ -17,7 +17,7 @@ export const createInstance = ({ children?: Instance["children"]; cssRules?: Array; }): Instance => { - const componentMeta = components[component]; + const componentMeta = getComponentMeta(component); return { component, id: id === undefined ? createInstanceId() : id, diff --git a/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts b/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts index da5a74e182..0126842d62 100644 --- a/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts +++ b/packages/project/src/shared/tree-utils/find-closest-non-inline-parent.ts @@ -1,4 +1,4 @@ -import { Instance, components } from "@webstudio-is/react-sdk"; +import { Instance, getComponentMeta } from "@webstudio-is/react-sdk"; import { getInstancePath } from "./get-instance-path"; export const findClosestNonInlineParent = ( @@ -7,5 +7,7 @@ export const findClosestNonInlineParent = ( ): Instance | undefined => { const path = getInstancePath(rootInstance, instanceId); path.reverse(); - return path.find((item) => components[item.component].isInlineOnly === false); + return path.find( + (item) => getComponentMeta(item.component).isInlineOnly === false + ); }; diff --git a/packages/project/src/shared/tree-utils/populate.ts b/packages/project/src/shared/tree-utils/populate.ts index 6da307af1e..e647a46b92 100644 --- a/packages/project/src/shared/tree-utils/populate.ts +++ b/packages/project/src/shared/tree-utils/populate.ts @@ -1,5 +1,5 @@ import { Breakpoint } from "@webstudio-is/css-data"; -import { type Instance, components } from "@webstudio-is/react-sdk"; +import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk"; /** * Populate instance with: @@ -11,7 +11,7 @@ export const populateInstance = ( breakpoint: Breakpoint["id"] = "" ): Instance => { const populatedInstance = { ...instance }; - const componentMeta = components[instance.component]; + const componentMeta = getComponentMeta(instance.component); if (componentMeta !== undefined && "defaultStyle" in componentMeta) { const cssRule = { breakpoint, diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 75a168b75b..581d73f7b6 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@webstudio-is/react-sdk", - "version": "0.8.0", + "version": "0.9.0", "description": "Webstudio JavaScript / TypeScript API", "author": "Webstudio ", "homepage": "https://webstudio.is", @@ -35,7 +35,6 @@ "@storybook/testing-library": "^0.0.11", "@types/node": "^17.0.21", "@webstudio-is/jest-config": "*", - "@webstudio-is/prisma-client": "*", "@webstudio-is/generate-arg-types": "*", "babel-loader": "^8.2.5", "esbuild": "^0.14.25", @@ -61,9 +60,9 @@ "detect-font": "^0.1.5", "@webstudio-is/asset-uploader": "^*", "@webstudio-is/css-data": "*", - "@webstudio-is/fonts": "*", "@webstudio-is/icons": "*", - "@webstudio-is/jest-config": "*", + "@webstudio-is/image": "*", + "@webstudio-is/prisma-client": "*", "immer": "^9.0.12", "mitt": "^3.0.0", "react-nano-state": "^0.4.0", diff --git a/packages/react-sdk/src/app/custom-components/image.tsx b/packages/react-sdk/src/app/custom-components/image.tsx new file mode 100644 index 0000000000..6b9e5ac4f1 --- /dev/null +++ b/packages/react-sdk/src/app/custom-components/image.tsx @@ -0,0 +1,40 @@ +import { + forwardRef, + useMemo, + type ComponentProps, + type ElementRef, +} from "react"; +import { Image as WebstudioImage, loaders } from "@webstudio-is/image"; +import { Image as SdkImage } from "../../components/image"; +import { useUserPropsAsset } from "../../user-props/use-user-props-asset"; +import { idAttribute } from "../../tree/wrapper-component"; +import { getParams } from "../params"; + +const defaultTag = "img"; + +type Props = ComponentProps & { [idAttribute]: string }; + +export const Component = forwardRef, Props>( + (props, ref) => { + const componentId = props[idAttribute] as string; + + const asset = useUserPropsAsset(componentId, "src"); + const params = getParams(); + + const loader = useMemo(() => { + if (asset == null) return null; + if (asset.location === "REMOTE") + return loaders.cloudflareImageLoader(params); + return loaders.localImageLoader(); + }, [asset, params]); + + if (asset === null || loader == null) + return ; + + return ( + + ); + } +); + +Component.displayName = "Image"; diff --git a/packages/react-sdk/src/app/custom-components/index.ts b/packages/react-sdk/src/app/custom-components/index.ts new file mode 100644 index 0000000000..e2620a03d0 --- /dev/null +++ b/packages/react-sdk/src/app/custom-components/index.ts @@ -0,0 +1 @@ +export * as Image from "./image"; diff --git a/packages/react-sdk/src/remix/handle-request.server.tsx b/packages/react-sdk/src/app/handle-request.server.tsx similarity index 100% rename from packages/react-sdk/src/remix/handle-request.server.tsx rename to packages/react-sdk/src/app/handle-request.server.tsx diff --git a/packages/react-sdk/src/remix/index.ts b/packages/react-sdk/src/app/index.ts similarity index 71% rename from packages/react-sdk/src/remix/index.ts rename to packages/react-sdk/src/app/index.ts index 1287530143..aed4524196 100644 --- a/packages/react-sdk/src/remix/index.ts +++ b/packages/react-sdk/src/app/index.ts @@ -1,2 +1,3 @@ export * from "./root"; export * from "./handle-request.server"; +export * from "./params"; diff --git a/packages/react-sdk/src/app/params.ts b/packages/react-sdk/src/app/params.ts new file mode 100644 index 0000000000..fd4b2dbdaf --- /dev/null +++ b/packages/react-sdk/src/app/params.ts @@ -0,0 +1,13 @@ +export type Params = { + resizeOrigin?: string; +}; + +let params: Params | null = {}; + +const emptyParams: Params = {}; + +export const getParams = (): Params => params ?? emptyParams; + +export const setParams = (newParams: Params | null) => { + params = newParams; +}; diff --git a/packages/react-sdk/src/remix/root.tsx b/packages/react-sdk/src/app/root.tsx similarity index 100% rename from packages/react-sdk/src/remix/root.tsx rename to packages/react-sdk/src/app/root.tsx diff --git a/packages/react-sdk/src/components/__generated__/rich-text-link.props.json b/packages/react-sdk/src/components/__generated__/rich-text-link.props.json new file mode 100644 index 0000000000..00e71a3c93 --- /dev/null +++ b/packages/react-sdk/src/components/__generated__/rich-text-link.props.json @@ -0,0 +1,547 @@ +{ + "slot": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "style": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "title": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "download": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "href": { + "defaultValue": "", + "required": false, + "type": "text" + }, + "hrefLang": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "media": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "ping": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "rel": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "target": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "type": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "referrerPolicy": { + "defaultValue": null, + "options": [ + "\"\"", + "no-referrer", + "no-referrer-when-downgrade", + "origin", + "origin-when-cross-origin", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin", + "unsafe-url" + ], + "required": false, + "type": "select" + }, + "defaultChecked": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "defaultValue": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "suppressContentEditableWarning": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "suppressHydrationWarning": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "accessKey": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "className": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "contentEditable": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "contextMenu": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "dir": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "draggable": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "hidden": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "id": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "lang": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "placeholder": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "spellCheck": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "tabIndex": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "translate": { + "defaultValue": null, + "options": ["yes", "no"], + "required": false, + "type": "radio" + }, + "radioGroup": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "role": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "about": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "datatype": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "inlist": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "prefix": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "property": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "resource": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "typeof": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "vocab": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "autoCapitalize": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "autoCorrect": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "autoSave": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "color": { + "defaultValue": null, + "required": false, + "type": "color" + }, + "itemProp": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "itemScope": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "itemType": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "itemID": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "itemRef": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "results": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "security": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "unselectable": { + "defaultValue": null, + "options": ["on", "off"], + "required": false, + "type": "radio" + }, + "inputMode": { + "defaultValue": null, + "options": [ + "text", + "none", + "search", + "tel", + "url", + "email", + "numeric", + "decimal" + ], + "required": false, + "type": "select" + }, + "is": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-activedescendant": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-atomic": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-autocomplete": { + "defaultValue": null, + "options": ["list", "none", "inline", "both"], + "required": false, + "type": "radio" + }, + "aria-busy": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-checked": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-colcount": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-colindex": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-colspan": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-controls": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-current": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-describedby": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-details": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-disabled": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-dropeffect": { + "defaultValue": null, + "options": ["link", "none", "copy", "execute", "move", "popup"], + "required": false, + "type": "select" + }, + "aria-errormessage": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-expanded": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-flowto": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-grabbed": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-haspopup": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-hidden": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-invalid": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-keyshortcuts": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-label": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-labelledby": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-level": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-live": { + "defaultValue": null, + "options": ["off", "assertive", "polite"], + "required": false, + "type": "radio" + }, + "aria-modal": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-multiline": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-multiselectable": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-orientation": { + "defaultValue": null, + "options": ["horizontal", "vertical"], + "required": false, + "type": "radio" + }, + "aria-owns": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-placeholder": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-posinset": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-pressed": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-readonly": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-relevant": { + "defaultValue": null, + "options": [ + "text", + "additions", + "additions removals", + "additions text", + "all", + "removals", + "removals additions", + "removals text", + "text additions", + "text removals" + ], + "required": false, + "type": "select" + }, + "aria-required": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-roledescription": { + "defaultValue": null, + "required": false, + "type": "text" + }, + "aria-rowcount": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-rowindex": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-rowspan": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-selected": { + "defaultValue": null, + "required": false, + "type": "boolean" + }, + "aria-setsize": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-sort": { + "defaultValue": null, + "options": ["none", "ascending", "descending", "other"], + "required": false, + "type": "radio" + }, + "aria-valuemax": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-valuemin": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-valuenow": { + "defaultValue": null, + "required": false, + "type": "number" + }, + "aria-valuetext": { + "defaultValue": null, + "required": false, + "type": "text" + } +} diff --git a/packages/react-sdk/src/components/index.test.ts b/packages/react-sdk/src/components/index.test.ts index 0265c11cfc..e5beaaa5e7 100644 --- a/packages/react-sdk/src/components/index.test.ts +++ b/packages/react-sdk/src/components/index.test.ts @@ -1,12 +1,6 @@ -import * as components from "./index"; +import { getComponentNames, getComponentMeta } from "./index"; import { WsComponentMeta } from "./component-type"; -test.each(Object.entries(components))( - "validating meta definition of %s", - (name, component) => { - if (name === "default") { - return; - } - WsComponentMeta.parse(component); - } -); +test.each(getComponentNames())("validating meta definition of %s", (name) => { + WsComponentMeta.parse(getComponentMeta(name)); +}); diff --git a/packages/react-sdk/src/components/index.ts b/packages/react-sdk/src/components/index.ts index bb57b1d48c..a8d49208ca 100644 --- a/packages/react-sdk/src/components/index.ts +++ b/packages/react-sdk/src/components/index.ts @@ -1,15 +1,98 @@ -export { default as Body } from "./body.ws"; -export { default as Box } from "./box.ws"; -export { default as TextBlock } from "./text-block.ws"; -export { default as Heading } from "./heading.ws"; -export { default as Paragraph } from "./paragraph.ws"; -export { default as Link } from "./link.ws"; -export { default as Span } from "./span.ws"; -export { default as Bold } from "./bold.ws"; -export { default as Italic } from "./italic.ws"; -export { default as Superscript } from "./superscript.ws"; -export { default as Subscript } from "./subscript.ws"; -export { default as Button } from "./button.ws"; -export { default as Input } from "./input.ws"; -export { default as Form } from "./form.ws"; -export { default as Image } from "./image.ws"; +import Body from "./body.ws"; +import Box from "./box.ws"; +import TextBlock from "./text-block.ws"; +import Heading from "./heading.ws"; +import Paragraph from "./paragraph.ws"; +import Link from "./link.ws"; +import RichTextLink from "./rich-text-link.ws"; +import Span from "./span.ws"; +import Bold from "./bold.ws"; +import Italic from "./italic.ws"; +import Superscript from "./superscript.ws"; +import Subscript from "./subscript.ws"; +import Button from "./button.ws"; +import Input from "./input.ws"; +import Form from "./form.ws"; +import Image from "./image.ws"; + +const components = { + Body, + Box, + TextBlock, + Heading, + Paragraph, + Link, + RichTextLink, + Span, + Bold, + Italic, + Superscript, + Subscript, + Button, + Input, + Form, + Image, +} as const; + +type RegisteredComponents = Partial<{ + [p in ComponentName]: { + // @todo: Anyone knows what the type can be for any React Component? + // eslint-disable-next-line @typescript-eslint/ban-types + Component: {}; + meta?: Partial>; + }; +}>; + +let registeredComponents: RegisteredComponents | null = null; + +export type ComponentName = keyof typeof components; + +const componentNames = Object.keys(components) as ComponentName[]; + +export const getComponentNames = (): ComponentName[] => { + const uniqueNames = new Set([ + ...componentNames, + ...Object.keys(registeredComponents || {}), + ]); + + return [...uniqueNames.values()] as ComponentName[]; +}; + +export const getComponentMeta = ( + name: Name +): Omit => { + if ( + registeredComponents != null && + name in registeredComponents && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + registeredComponents[name]!.meta != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { ...components[name], ...registeredComponents[name]!.meta }; + } + + return components[name]; +}; + +/** + * @todo Remove Component from meta and read directly (via import), + * as meta information is not needed during production render. + **/ +export const getComponent = ( + name: Name +): typeof components[Name]["Component"] => { + return registeredComponents != null && name in registeredComponents + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (registeredComponents[name]! + .Component as typeof components[Name]["Component"]) + : components[name].Component; +}; + +/** + * @todo: Allow register any component. + * Now we can register only existings Components, as all our type system would + * break otherwise, see getComponent etc. So its overwriteComponent now + **/ +export const registerComponents = (components: RegisteredComponents) => { + registeredComponents = components; +}; diff --git a/packages/react-sdk/src/components/meta.ts b/packages/react-sdk/src/components/meta.ts index 9ecd091efe..999f85ccff 100644 --- a/packages/react-sdk/src/components/meta.ts +++ b/packages/react-sdk/src/components/meta.ts @@ -8,6 +8,7 @@ import Input from "./__generated__/input.props.json"; import Italic from "./__generated__/italic.props.json"; import Superscript from "./__generated__/superscript.props.json"; import Subscript from "./__generated__/subscript.props.json"; +import RichTextLink from "./__generated__/rich-text-link.props.json"; import Link from "./__generated__/link.props.json"; import Paragraph from "./__generated__/paragraph.props.json"; import Span from "./__generated__/span.props.json"; @@ -25,6 +26,7 @@ const meta = { Italic, Superscript, Subscript, + RichTextLink, Link, Paragraph, Span, @@ -56,8 +58,8 @@ type MetaProp = options: string[]; }; -type MetaProps = Record; +type MetaProps = { [p in string]?: MetaProp }; -// Probably better instead of JSON to generate ts with `as const` -export const componentsMeta: Record = - meta as Record; +export const getComponentMetaProps = (name: keyof typeof meta): MetaProps => { + return meta[name] as MetaProps; +}; diff --git a/packages/react-sdk/src/components/rich-text-link.stories.tsx b/packages/react-sdk/src/components/rich-text-link.stories.tsx new file mode 100644 index 0000000000..fe4255d922 --- /dev/null +++ b/packages/react-sdk/src/components/rich-text-link.stories.tsx @@ -0,0 +1,16 @@ +import type { ComponentStory, ComponentMeta } from "@storybook/react"; +import { RichTextLink as LinkPrimitive } from "./rich-text-link"; + +export default { + title: "Components/RichTextLink", + component: LinkPrimitive, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +export const RichTextLink = Template.bind({}); +RichTextLink.args = { + children: "RichTextLink", +}; diff --git a/packages/react-sdk/src/components/rich-text-link.tsx b/packages/react-sdk/src/components/rich-text-link.tsx new file mode 100644 index 0000000000..268bb99033 --- /dev/null +++ b/packages/react-sdk/src/components/rich-text-link.tsx @@ -0,0 +1,14 @@ +import { forwardRef, type ElementRef, type ComponentProps } from "react"; + +const defaultTag = "a"; + +type RichTextLinkProps = Omit, "href"> & { + href?: string; +}; + +export const RichTextLink = forwardRef< + ElementRef, + RichTextLinkProps +>(({ href = "", ...props }, ref) => ); + +RichTextLink.displayName = "RichTextLink"; diff --git a/packages/react-sdk/src/components/rich-text-link.ws.tsx b/packages/react-sdk/src/components/rich-text-link.ws.tsx new file mode 100644 index 0000000000..5074f6e1da --- /dev/null +++ b/packages/react-sdk/src/components/rich-text-link.ws.tsx @@ -0,0 +1,15 @@ +import { Link2Icon } from "@webstudio-is/icons"; +import { RichTextLink } from "./rich-text-link"; +import type { WsComponentMeta } from "./component-type"; + +const meta: WsComponentMeta = { + Icon: Link2Icon, + Component: RichTextLink, + canAcceptChildren: false, + isContentEditable: false, + isInlineOnly: true, + isListed: false, + label: "Link", +}; + +export default meta; diff --git a/packages/react-sdk/src/css/categories.ts b/packages/react-sdk/src/css/categories.ts index 2289e58444..8587c8b4a2 100644 --- a/packages/react-sdk/src/css/categories.ts +++ b/packages/react-sdk/src/css/categories.ts @@ -60,6 +60,7 @@ const size = [ "maxHeight", "overflow", "objectFit", + "aspectRatio", ] as const; const position = [ diff --git a/packages/react-sdk/src/db/instance.ts b/packages/react-sdk/src/db/instance.ts index 2bf3904626..f901149477 100644 --- a/packages/react-sdk/src/db/instance.ts +++ b/packages/react-sdk/src/db/instance.ts @@ -1,12 +1,12 @@ import { CssRule } from "@webstudio-is/css-data"; import { z } from "zod"; -import * as components from "../components"; +import { ComponentName } from "../components"; // This should be used when passing a lot of data is potentially costly. // For example, when passing data from an iframe. export type BaseInstance = { id: string; - component: keyof typeof components; + component: ComponentName; cssRules: Array; }; diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts index 910e6096c7..a4b245bd94 100644 --- a/packages/react-sdk/src/index.ts +++ b/packages/react-sdk/src/index.ts @@ -1,8 +1,9 @@ export * from "./css"; export * from "./tree"; -export * as components from "./components"; +export * from "./components"; export * from "./components/meta"; export * from "./user-props"; export * from "./pubsub"; export * from "./db"; -export * from "./remix"; +export * from "./app"; +export * as customComponents from "./app/custom-components"; diff --git a/packages/react-sdk/src/tree/root.ts b/packages/react-sdk/src/tree/root.ts index 6360782d27..697162e3ee 100644 --- a/packages/react-sdk/src/tree/root.ts +++ b/packages/react-sdk/src/tree/root.ts @@ -5,6 +5,9 @@ import { WrapperComponent } from "./wrapper-component"; import type { Asset } from "@webstudio-is/asset-uploader"; import { type ComponentProps } from "react"; import type { Breakpoint } from "@webstudio-is/css-data"; +import { registerComponents } from "../components"; +import * as customComponents from "../app/custom-components"; +import { setParams, type Params } from "../app/params"; export type Data = { tree: Tree | null; @@ -12,6 +15,7 @@ export type Data = { designTokens: Array; props: Array; assets: Array; + params?: Params; }; type RootProps = { @@ -27,6 +31,10 @@ export const InstanceRoot = ({ throw new Error("Tree is null"); } useAllUserProps(data.props); + setParams(data.params ?? null); + + registerComponents(customComponents); + return createElementsTree({ instance: data.tree.root, Component: Component ?? WrapperComponent, diff --git a/packages/react-sdk/src/tree/wrapper-component.tsx b/packages/react-sdk/src/tree/wrapper-component.tsx index 526941caaf..41369234ee 100644 --- a/packages/react-sdk/src/tree/wrapper-component.tsx +++ b/packages/react-sdk/src/tree/wrapper-component.tsx @@ -1,6 +1,6 @@ import { Fragment } from "react"; import type { Instance } from "../db"; -import * as components from "../components"; +import { getComponent } from "../components"; import { useUserProps } from "../user-props/use-user-props"; import type { OnChangeChildren } from "./create-elements-tree"; @@ -37,7 +37,7 @@ export const WrapperComponent = ({ children, ...rest }: WrapperComponentProps) => { - const { Component } = components[instance.component]; + const Component = getComponent(instance.component); const userProps = useUserProps(instance.id); const props = { ...userProps, ...rest, [idAttribute]: instance.id }; return ( diff --git a/packages/react-sdk/src/user-props/use-user-props-asset.ts b/packages/react-sdk/src/user-props/use-user-props-asset.ts new file mode 100644 index 0000000000..a537db5559 --- /dev/null +++ b/packages/react-sdk/src/user-props/use-user-props-asset.ts @@ -0,0 +1,26 @@ +import { useMemo } from "react"; +import { type Instance } from "../db"; +import { type UserProp } from "./schema"; +import { useAllUserProps } from "./all-user-props"; +import type { Asset } from "@webstudio-is/asset-uploader"; + +/** + * Get asset for prop, like src on the Image component + */ +export const useUserPropsAsset = ( + instanceId: Instance["id"], + propName: UserProp["prop"] +): Asset | null => { + const [allUserProps] = useAllUserProps(); + + const propsData = allUserProps[instanceId]; + const asset = useMemo(() => { + if (propsData == null) return null; + const prop = propsData.props.find((prop) => prop.prop === propName); + if (prop == null) return null; + + return prop.asset ?? null; + }, [propName, propsData]); + + return asset; +}; diff --git a/packages/react-sdk/src/user-props/use-user-props.ts b/packages/react-sdk/src/user-props/use-user-props.ts index 6f9e33aca7..fe0a193d20 100644 --- a/packages/react-sdk/src/user-props/use-user-props.ts +++ b/packages/react-sdk/src/user-props/use-user-props.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useMemo } from "react"; import { type Instance } from "../db"; import { type UserProp } from "./schema"; import { useAllUserProps } from "./all-user-props"; @@ -13,24 +13,13 @@ export const useUserProps = (instanceId: Instance["id"]) => { const [allUserProps] = useAllUserProps(); const propsData = allUserProps[instanceId]; - const initialUserProps = useMemo(() => { - if (propsData === undefined) return {}; + const props = useMemo(() => { + if (propsData == null) return {}; return propsData.props.reduce((props, { prop, value }) => { props[prop] = value; return props; }, {} as UserProps); }, [propsData]); - const [props, setProps] = useState(initialUserProps); - - useEffect(() => { - if (propsData == undefined) return; - const nextProps: UserProps = {}; - for (const prop of propsData.props) { - nextProps[prop.prop] = prop.value; - } - setProps(nextProps); - }, [propsData]); - return props; }; diff --git a/packages/react-sdk/tsconfig.json b/packages/react-sdk/tsconfig.json index 651a772bea..47c6c0fb2a 100644 --- a/packages/react-sdk/tsconfig.json +++ b/packages/react-sdk/tsconfig.json @@ -23,10 +23,6 @@ "path": "../css-data" }, - { - "path": "../fonts" - }, - { "path": "../generate-arg-types" }, @@ -35,6 +31,10 @@ "path": "../icons" }, + { + "path": "../image" + }, + { "path": "../prisma-client" } diff --git a/yarn.lock b/yarn.lock index ffa83ca1eb..81784cdbb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5553,16 +5553,7 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "18.0.15" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe" - integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@^17", "@types/react@^17.0.24": +"@types/react@*", "@types/react@^17", "@types/react@^17.0.24": version "17.0.47" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.47.tgz#4ee71aaf4c5a9e290e03aa4d0d313c5d666b3b78" integrity sha512-mk0BL8zBinf2ozNr3qPnlu1oyVTYq+4V7WA76RgxUAtf0Em/Wbid38KN6n4abEkvO4xMTBWmnP1FtQzgkEiJoA==