From 8172b077e85054486ef1acc4ec82d3e28e192a63 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sat, 26 Nov 2022 23:23:37 +0300 Subject: [PATCH 01/14] Handle loader errors in error boundary (#520) This will allow to infer loader data instead of writing type for it as recommended in https://github.com/remix-run/remix/releases/tag/remix%401.6.5. --- apps/designer/app/root.tsx | 1 - apps/designer/app/routes/$.tsx | 91 ++++++++----------- .../app/routes/designer/$projectId.tsx | 61 ++++++------- apps/designer/app/routes/index.tsx | 10 +- yarn.lock | 11 +-- 5 files changed, 73 insertions(+), 101 deletions(-) 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..6774d7b94c 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,47 @@ 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 }); + } + + return { ...canvasData, env, mode }; +}; + +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/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/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== From a9fede0a1d65e70ac52b7233ee4b55f9ba6585c4 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Sun, 27 Nov 2022 02:47:14 +0400 Subject: [PATCH 02/14] Ability to replace SDK components (#516) Whats done: - [x] - Added getComponent, getComponentMeta (old getComponenMeta renamed to getComponentMetaProps) - [x] - Added `registerComponents` (for now able only override existing components) - [x] - Renamed react-sd `remix` folder to `app` - [x] - At the `app` folder added `custom-components/Image` wrapper component, which reads assets etc, and use `webstudio/image` - [x] `getParams`, `setParams` added at `react-sdk/app/` as they are app level TODO: - [x] Create "normal" `useParams` to be able to use data.params https://github.com/webstudio-is/webstudio-designer/blob/c05f40818beaac8a811fe0a97ba49e30f01f5045/packages/react-sdk/src/app/custom-components/image.tsx#L15-L23 - [x] Reuse props meta from imported image - created issue https://github.com/webstudio-is/webstudio-designer/issues/521 Co-authored-by: istarkov --- apps/designer/app/canvas/canvas.tsx | 7 ++ .../wrapper-component/wrapper-component.tsx | 4 +- apps/designer/app/canvas/shared/instance.ts | 4 +- .../app/canvas/shared/use-drag-drop.ts | 6 +- .../app/canvas/shared/use-shortcuts.ts | 6 +- .../shared/use-track-selected-element.ts | 8 +- .../designer/features/props-panel/control.tsx | 4 +- .../props-panel/props-panel.stories.tsx | 4 +- .../features/props-panel/props-panel.tsx | 4 +- .../props-panel/use-props-logic.test.ts | 4 +- .../features/props-panel/use-props-logic.ts | 6 +- .../panels/components/component-thumb.tsx | 4 +- .../panels/components/components.tsx | 16 +-- .../workspace/canvas-tools/outline/label.tsx | 4 +- .../shared/inspector/component-info.tsx | 6 +- .../app/designer/shared/tree/index.tsx | 10 +- apps/designer/app/env.server.ts | 2 + apps/designer/app/routes/$.tsx | 8 +- packages/image/package.json | 7 +- packages/image/src/image-dev.stories.tsx | 10 +- packages/image/src/index.ts | 2 +- .../src/shared/tree-utils/create-instance.ts | 4 +- .../find-closest-non-inline-parent.ts | 6 +- .../project/src/shared/tree-utils/populate.ts | 4 +- packages/react-sdk/package.json | 1 + .../src/app/custom-components/image.tsx | 40 +++++++ .../src/app/custom-components/index.ts | 1 + .../{remix => app}/handle-request.server.tsx | 0 .../react-sdk/src/{remix => app}/index.ts | 1 + packages/react-sdk/src/app/params.ts | 13 +++ .../react-sdk/src/{remix => app}/root.tsx | 0 .../react-sdk/src/components/index.test.ts | 14 +-- packages/react-sdk/src/components/index.ts | 102 +++++++++++++++--- packages/react-sdk/src/components/meta.ts | 6 +- packages/react-sdk/src/db/instance.ts | 4 +- packages/react-sdk/src/index.ts | 5 +- packages/react-sdk/src/tree/root.ts | 8 ++ .../react-sdk/src/tree/wrapper-component.tsx | 4 +- .../src/user-props/use-user-props-asset.ts | 26 +++++ .../src/user-props/use-user-props.ts | 17 +-- packages/react-sdk/tsconfig.json | 4 + 41 files changed, 279 insertions(+), 107 deletions(-) create mode 100644 packages/react-sdk/src/app/custom-components/image.tsx create mode 100644 packages/react-sdk/src/app/custom-components/index.ts rename packages/react-sdk/src/{remix => app}/handle-request.server.tsx (100%) rename packages/react-sdk/src/{remix => app}/index.ts (71%) create mode 100644 packages/react-sdk/src/app/params.ts rename packages/react-sdk/src/{remix => app}/root.tsx (100%) create mode 100644 packages/react-sdk/src/user-props/use-user-props-asset.ts diff --git a/apps/designer/app/canvas/canvas.tsx b/apps/designer/app/canvas/canvas.tsx index 9e9ac5551c..6e3d59022a 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"; @@ -133,6 +136,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/wrapper-component/wrapper-component.tsx b/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx index a8458c8359..fbdede4503 100644 --- a/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx +++ b/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx @@ -4,7 +4,7 @@ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext import { useUserProps, renderWrapperComponentChildren, - components, + getComponent, type Instance, type OnChangeChildren, idAttribute, @@ -66,7 +66,7 @@ export const WrapperComponentDev = ({ const readonlyProps = instance.component === "Input" ? { readOnly: true } : undefined; - const { Component } = components[instance.component]; + const Component = getComponent(instance.component); const props = { ...userProps, 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/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..5a93503f68 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,5 @@ import { useCallback, useEffect, useRef } from "react"; -import { type Instance, components } from "@webstudio-is/react-sdk"; +import { type Instance, getComponentMeta } from "@webstudio-is/react-sdk"; import { useSubscribe } from "~/shared/pubsub"; import { useSelectedElement, useSelectedInstance } from "./nano-states"; import { @@ -91,10 +91,10 @@ export const useTrackSelectedElement = () => { // 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 /*|| component in components === false*/) { 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 +102,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/designer/features/props-panel/control.tsx b/apps/designer/app/designer/features/props-panel/control.tsx index dab2e52657..7662311082 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"; @@ -183,7 +183,7 @@ export function Control({ userProp, onChangePropValue, }: ControlProps) { - const meta = componentsMeta[component]; + const meta = getComponentMetaProps(component); const argType = meta[userProp.prop as keyof typeof meta]; const defaultValue = argType.defaultValue; 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..4160b4f109 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: { 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..ff1f55d2fa 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 = 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..a133664c79 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 { 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..f234c868f7 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,7 +37,7 @@ const getRequiredProps = ( selectedInstanceData: SelectedInstanceData ): UserProp[] => { const { component } = selectedInstanceData; - const meta = componentsMeta[component]; + const meta = getComponentMetaProps(component); return Object.entries(meta) .filter(([_, value]) => value.required) .map(([prop, _]) => ({ @@ -54,7 +54,7 @@ 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) .map(([prop, propObj]) => { 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); +const listedComponentNames = componentNames.filter( + (name) => getComponentMeta(name).isListed +); type DraggableThumbProps = { onClick: MouseEventHandler; @@ -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) => { @@ -142,7 +146,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"]) => ( 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/routes/$.tsx b/apps/designer/app/routes/$.tsx index 6774d7b94c..7fd42bbd59 100644 --- a/apps/designer/app/routes/$.tsx +++ b/apps/designer/app/routes/$.tsx @@ -69,7 +69,13 @@ export const loader: LoaderFunction = async ({ request }): Promise => { throw json("Page not found", { status: 404 }); } - return { ...canvasData, env, mode }; + const params: CanvasData["params"] = {}; + + if (env.RESIZE_ORIGIN != null) { + params.resizeOrigin = env.RESIZE_ORIGIN; + } + + return { ...canvasData, env, mode, params }; }; export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => { diff --git a/packages/image/package.json b/packages/image/package.json index bc7846dc16..d2e441431b 100644 --- a/packages/image/package.json +++ b/packages/image/package.json @@ -11,7 +11,8 @@ "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", @@ -38,8 +39,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/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..d5ba4d6b96 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -34,6 +34,7 @@ "@storybook/react": "^6.5.6", "@storybook/testing-library": "^0.0.11", "@types/node": "^17.0.21", + "@webstudio-is/image": "*", "@webstudio-is/jest-config": "*", "@webstudio-is/prisma-client": "*", "@webstudio-is/generate-arg-types": "*", 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/index.test.ts b/packages/react-sdk/src/components/index.test.ts index 0265c11cfc..40e39d8616 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 { componentNames, 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(componentNames)("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..de89e52907 100644 --- a/packages/react-sdk/src/components/index.ts +++ b/packages/react-sdk/src/components/index.ts @@ -1,15 +1,87 @@ -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 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, + 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; + +export const componentNames = Object.keys(components) 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..d8e783b383 100644 --- a/packages/react-sdk/src/components/meta.ts +++ b/packages/react-sdk/src/components/meta.ts @@ -58,6 +58,6 @@ type MetaProp = type MetaProps = Record; -// 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/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 bb1841bdb6..e7d06b8831 100644 --- a/packages/react-sdk/src/tree/root.ts +++ b/packages/react-sdk/src/tree/root.ts @@ -5,12 +5,16 @@ 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; breakpoints: Array; props: Array; assets: Array; + params?: Params; }; type RootProps = { @@ -26,6 +30,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..40682627c2 100644 --- a/packages/react-sdk/tsconfig.json +++ b/packages/react-sdk/tsconfig.json @@ -35,6 +35,10 @@ "path": "../icons" }, + { + "path": "../image" + }, + { "path": "../prisma-client" } From 0d40747888fa6ba150ab90a25becdd3dd2c44ae6 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 27 Nov 2022 10:13:16 +0300 Subject: [PATCH 03/14] Use serialized loader types (#522) New remix enforces types to be serialized. Prisma output dates as Date instance and this is not customizable. Had to fix only 2 cases. --- apps/designer/app/dashboard/components/header.tsx | 6 +++++- apps/designer/app/dashboard/dashboard.tsx | 7 ++++++- .../designer/features/props-panel/use-props-logic.test.ts | 4 ++-- apps/designer/app/routes/dashboard/index.tsx | 6 +++++- packages/asset-uploader/src/types.ts | 3 ++- packages/asset-uploader/src/utils/format-asset.ts | 2 ++ 6 files changed, 22 insertions(+), 6 deletions(-) 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/use-props-logic.test.ts b/apps/designer/app/designer/features/props-panel/use-props-logic.test.ts index a133664c79..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 @@ -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/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/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)), }; }; From 9e3f3c46c65373e8afb6dc869f5c2c19827d025d Mon Sep 17 00:00:00 2001 From: Ivan Starkov Date: Sun, 27 Nov 2022 11:44:48 +0400 Subject: [PATCH 04/14] Fix accidentally commented code at #516 (#524) https://github.com/webstudio-is/webstudio-designer/pull/516#pullrequestreview-1194934498 --- .../shared/use-track-selected-element.ts | 9 ++++++-- .../panels/components/components.tsx | 21 ++++++++++++------- .../react-sdk/src/components/index.test.ts | 4 ++-- packages/react-sdk/src/components/index.ts | 11 +++++++++- 4 files changed, 33 insertions(+), 12 deletions(-) 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 5a93503f68..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, getComponentMeta } 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,11 +91,12 @@ 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 } = getComponentMeta(component); diff --git a/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx b/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx index 2c121afeb8..de92671cf2 100644 --- a/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx +++ b/apps/designer/app/designer/features/sidebar-left/panels/components/components.tsx @@ -2,8 +2,9 @@ import { type MouseEventHandler, useState } from "react"; import { createPortal } from "react-dom"; import { type Instance, + type ComponentName, getComponentMeta, - componentNames, + getComponentNames, } from "@webstudio-is/react-sdk"; import { useSubscribe, type Publish } from "~/shared/pubsub"; import { Flex, useDrag, type Point } from "@webstudio-is/design-system"; @@ -14,10 +15,6 @@ import { ComponentThumb } from "./component-thumb"; import { useCanvasRect, useZoom } from "~/designer/shared/nano-states"; import { Header, CloseButton } from "../../lib/header"; -const listedComponentNames = componentNames.filter( - (name) => getComponentMeta(name).isListed -); - type DraggableThumbProps = { onClick: MouseEventHandler; component: Instance["component"]; @@ -69,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]"); @@ -96,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; } diff --git a/packages/react-sdk/src/components/index.test.ts b/packages/react-sdk/src/components/index.test.ts index 40e39d8616..e5beaaa5e7 100644 --- a/packages/react-sdk/src/components/index.test.ts +++ b/packages/react-sdk/src/components/index.test.ts @@ -1,6 +1,6 @@ -import { componentNames, getComponentMeta } from "./index"; +import { getComponentNames, getComponentMeta } from "./index"; import { WsComponentMeta } from "./component-type"; -test.each(componentNames)("validating meta definition of %s", (name) => { +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 de89e52907..092d960f24 100644 --- a/packages/react-sdk/src/components/index.ts +++ b/packages/react-sdk/src/components/index.ts @@ -45,7 +45,16 @@ let registeredComponents: RegisteredComponents | null = null; export type ComponentName = keyof typeof components; -export const componentNames = Object.keys(components) as ComponentName[]; +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 From 1ec17c41ec0cb6417e6f149af9666e9582c07e3e Mon Sep 17 00:00:00 2001 From: Ivan Starkov Date: Sun, 27 Nov 2022 14:04:59 +0400 Subject: [PATCH 05/14] Remove assets flag (#525) --- .../designer/features/props-panel/control.tsx | 19 ++++++++----------- .../features/sidebar-left/sidebar-left.tsx | 2 -- .../features/style-panel/shared/configs.ts | 3 +-- .../app/shared/feature-flags/flags.ts | 1 - 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/apps/designer/app/designer/features/props-panel/control.tsx b/apps/designer/app/designer/features/props-panel/control.tsx index 7662311082..3afc5c281b 100644 --- a/apps/designer/app/designer/features/props-panel/control.tsx +++ b/apps/designer/app/designer/features/props-panel/control.tsx @@ -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"; @@ -208,17 +207,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)) { diff --git a/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx b/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx index 7a86f14ec5..c5673df631 100644 --- a/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx +++ b/apps/designer/app/designer/features/sidebar-left/sidebar-left.tsx @@ -37,8 +37,6 @@ export const SidebarLeft = ({ publish }: SidebarLeftProps) => { 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/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; From b80b4c21f7fff55a7db610ae3bbffbd170ec956d Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 27 Nov 2022 13:33:12 +0300 Subject: [PATCH 06/14] Remove paragraphs from text editor (#514) Ref https://github.com/webstudio-is/webstudio-designer/issues/487 Before lexical created paragraphs when press enter which we tried to visually represent as line break by removing margins and converted to line break to instances. This could lead to a few issues. 1. When paragraph block is inside `` nothing can be typed in the beginning and in the end of paragraph 2. Paragraphs can be styled by users which can break text editor visually Here I overwritten default Enter command and insert line break instead of paragraph. To protect from external sources like paste from clipboard added transform to merge all paragraphs if move than one. We will still have one paragraph in the root with display: inline style. Though it will cause less damage to editor with user styles. --- .../canvas/features/text-editor/interop.ts | 8 +- .../features/text-editor/text-editor.tsx | 85 +++++++++++++++++-- apps/designer/app/canvas/shared/styles.ts | 4 - 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/apps/designer/app/canvas/features/text-editor/interop.ts b/apps/designer/app/canvas/features/text-editor/interop.ts index db7fdb779d..83410d9b12 100644 --- a/apps/designer/app/canvas/features/text-editor/interop.ts +++ b/apps/designer/app/canvas/features/text-editor/interop.ts @@ -32,16 +32,10 @@ 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()); 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 ( + { From a8f1f407b7599982752e6d27b66f514f7212c1df Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 27 Nov 2022 13:48:26 +0300 Subject: [PATCH 07/14] Bump saas packages (#527) Added publish script to image package and bumped everything changed since latest bump by checking `git log -1 -- .` --- packages/asset-uploader/package.json | 2 +- packages/css-data/package.json | 2 +- packages/css-engine/package.json | 2 +- packages/icons/package.json | 2 +- packages/image/package.json | 5 +++-- packages/project/package.json | 2 +- packages/react-sdk/package.json | 8 +++----- packages/react-sdk/tsconfig.json | 4 ---- 8 files changed, 11 insertions(+), 16 deletions(-) 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/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/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 d2e441431b..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", @@ -16,7 +16,8 @@ "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": { 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/react-sdk/package.json b/packages/react-sdk/package.json index d5ba4d6b96..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", @@ -34,9 +34,7 @@ "@storybook/react": "^6.5.6", "@storybook/testing-library": "^0.0.11", "@types/node": "^17.0.21", - "@webstudio-is/image": "*", "@webstudio-is/jest-config": "*", - "@webstudio-is/prisma-client": "*", "@webstudio-is/generate-arg-types": "*", "babel-loader": "^8.2.5", "esbuild": "^0.14.25", @@ -62,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/tsconfig.json b/packages/react-sdk/tsconfig.json index 40682627c2..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" }, From 52a4f9af6bf8b29aa1c6fb1145a2ba3cae59e502 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sun, 27 Nov 2022 13:46:53 +0100 Subject: [PATCH 08/14] added aspect ratio property to sizes --- packages/react-sdk/src/css/categories.ts | 1 + 1 file changed, 1 insertion(+) 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 = [ From 6e5c0f9c1d348f9995b1c955103da98a34c3b601 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 27 Nov 2022 17:25:24 +0300 Subject: [PATCH 09/14] Split Link and RichTextLink components (#530) Droppable link an link created in text editor have different requirements. We should split them. In the next PR they will get different types so we will avoid similar issues in the future. --- .../canvas/features/text-editor/interop.ts | 8 +- .../wrapper-component/wrapper-component.tsx | 5 +- .../__generated__/rich-text-link.props.json | 547 ++++++++++++++++++ packages/react-sdk/src/components/index.ts | 2 + packages/react-sdk/src/components/meta.ts | 2 + .../src/components/rich-text-link.stories.tsx | 16 + .../src/components/rich-text-link.tsx | 14 + .../src/components/rich-text-link.ws.tsx | 15 + 8 files changed, 606 insertions(+), 3 deletions(-) create mode 100644 packages/react-sdk/src/components/__generated__/rich-text-link.props.json create mode 100644 packages/react-sdk/src/components/rich-text-link.stories.tsx create mode 100644 packages/react-sdk/src/components/rich-text-link.tsx create mode 100644 packages/react-sdk/src/components/rich-text-link.ws.tsx diff --git a/apps/designer/app/canvas/features/text-editor/interop.ts b/apps/designer/app/canvas/features/text-editor/interop.ts index 83410d9b12..9f12c7daf1 100644 --- a/apps/designer/app/canvas/features/text-editor/interop.ts +++ b/apps/designer/app/canvas/features/text-editor/interop.ts @@ -41,7 +41,11 @@ const $writeUpdates = ( 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 @@ -109,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/wrapper-component/wrapper-component.tsx b/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx index fbdede4503..b391dbd4c2 100644 --- a/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx +++ b/apps/designer/app/canvas/features/wrapper-component/wrapper-component.tsx @@ -80,7 +80,10 @@ export const WrapperComponentDev = ({ "data-ws-component": instance.component, [idAttribute]: instance.id, onClick: (event: MouseEvent) => { - if (instance.component === "Link") { + if ( + instance.component === "Link" || + instance.component === "RichTextLink" + ) { event.preventDefault(); } }, 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.ts b/packages/react-sdk/src/components/index.ts index 092d960f24..a8d49208ca 100644 --- a/packages/react-sdk/src/components/index.ts +++ b/packages/react-sdk/src/components/index.ts @@ -4,6 +4,7 @@ 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"; @@ -21,6 +22,7 @@ const components = { Heading, Paragraph, Link, + RichTextLink, Span, Bold, Italic, diff --git a/packages/react-sdk/src/components/meta.ts b/packages/react-sdk/src/components/meta.ts index d8e783b383..1d29bea886 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, 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; From 80e2117bb7ef014d0543d9c4bb764a48691bc98f Mon Sep 17 00:00:00 2001 From: d-ivashchuk Date: Sun, 27 Nov 2022 18:09:53 +0100 Subject: [PATCH 10/14] move lost-pixel url to legacy version to ensure smooth transition to v1 of the platform --- packages/design-system/lostpixel.config.ts | 1 + 1 file changed, 1 insertion(+) 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, From 086b9fa789298b8f63c79977cd26d6e546be96d5 Mon Sep 17 00:00:00 2001 From: d-ivashchuk Date: Sun, 27 Nov 2022 18:21:55 +0100 Subject: [PATCH 11/14] =?UTF-8?q?=E2=80=9Cci=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a7125fe6957f1eb77c9e5029638c8180d5d099df Mon Sep 17 00:00:00 2001 From: Ivan Starkov Date: Sun, 27 Nov 2022 21:32:03 +0400 Subject: [PATCH 12/14] [release-fix] Fix adding new prop (#531) - [x] - New property creation was broken - [x] - Switch on `tag` was seriosly broken image --- .../designer/features/props-panel/control.tsx | 20 ++++++++++--------- .../props-panel/props-panel.stories.tsx | 2 +- .../features/props-panel/props-panel.tsx | 5 +++-- .../features/props-panel/use-props-logic.ts | 7 +++---- packages/react-sdk/src/components/meta.ts | 2 +- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/designer/app/designer/features/props-panel/control.tsx b/apps/designer/app/designer/features/props-panel/control.tsx index 3afc5c281b..7fe07995a4 100644 --- a/apps/designer/app/designer/features/props-panel/control.tsx +++ b/apps/designer/app/designer/features/props-panel/control.tsx @@ -183,10 +183,12 @@ export function Control({ onChangePropValue, }: ControlProps) { const meta = getComponentMetaProps(component); - const argType = meta[userProp.prop as keyof typeof meta]; - 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( @@ -239,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 4160b4f109..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 @@ -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 ff1f55d2fa..a19b01f331 100644 --- a/apps/designer/app/designer/features/props-panel/props-panel.tsx +++ b/apps/designer/app/designer/features/props-panel/props-panel.tsx @@ -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.ts b/apps/designer/app/designer/features/props-panel/use-props-logic.ts index f234c868f7..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 @@ -39,7 +39,7 @@ const getRequiredProps = ( const { component } = selectedInstanceData; const meta = getComponentMetaProps(component); return Object.entries(meta) - .filter(([_, value]) => value.required) + .filter(([_, value]) => value?.required) .map(([prop, _]) => ({ id: ObjectId().toString(), prop, @@ -56,10 +56,9 @@ const getPropsWithDefaultValue = ( const { component } = selectedInstanceData; 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/packages/react-sdk/src/components/meta.ts b/packages/react-sdk/src/components/meta.ts index 1d29bea886..999f85ccff 100644 --- a/packages/react-sdk/src/components/meta.ts +++ b/packages/react-sdk/src/components/meta.ts @@ -58,7 +58,7 @@ type MetaProp = options: string[]; }; -type MetaProps = Record; +type MetaProps = { [p in string]?: MetaProp }; export const getComponentMetaProps = (name: keyof typeof meta): MetaProps => { return meta[name] as MetaProps; From 61acb2dcac83100238208fc8df719fc48e3a97fd Mon Sep 17 00:00:00 2001 From: d-ivashchuk Date: Sun, 27 Nov 2022 18:32:42 +0100 Subject: [PATCH 13/14] ci From 35b23f1bc46f7f29cf32ded673cb362ac321f996 Mon Sep 17 00:00:00 2001 From: d-ivashchuk Date: Sun, 27 Nov 2022 18:33:01 +0100 Subject: [PATCH 14/14] ci