From 40e71b6c5b116cabda7fe5bcd9c8e5df8dfbaa0f Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Tue, 1 Aug 2023 22:40:16 +0400 Subject: [PATCH] Simplify indexes within ancestors (#2021) Here added dedicated field to meta to simplify computing and using indexes. ``` { type: 'container', label: 'Tab Trigger', // this one indexWithinAncestor: 'Tabs' } ``` Since it's single value, we no longer need to provide ancestor name in runtime and no longer have bug with namespaces. Index now provided with props. ``` getIndexWithinAncestorFromComponentProps(props): string | undefined ``` --- apps/builder/app/canvas/canvas.tsx | 10 +++--- packages/react-sdk/src/component-renderer.tsx | 4 +-- .../src/components/component-meta.ts | 5 +++ packages/react-sdk/src/context.tsx | 6 ++-- packages/react-sdk/src/embed-template.test.ts | 2 ++ packages/react-sdk/src/embed-template.ts | 5 +++ packages/react-sdk/src/index.ts | 4 +-- packages/react-sdk/src/instance-utils.test.ts | 30 ++++++++--------- packages/react-sdk/src/instance-utils.ts | 32 +++++++------------ packages/react-sdk/src/props.ts | 18 ++++++----- .../src/tree/create-elements-tree.tsx | 8 ++--- packages/react-sdk/src/tree/root.ts | 8 ++--- .../src/tree/webstudio-component.tsx | 1 + 13 files changed, 69 insertions(+), 64 deletions(-) diff --git a/apps/builder/app/canvas/canvas.tsx b/apps/builder/app/canvas/canvas.tsx index eabe0df143..51bc689969 100644 --- a/apps/builder/app/canvas/canvas.tsx +++ b/apps/builder/app/canvas/canvas.tsx @@ -10,7 +10,7 @@ import { executeEffectfulExpression, encodeVariablesMap, decodeVariablesMap, - getIndexesOfTypeWithinRequiredAncestors, + getIndexesWithinAncestors, } from "@webstudio-is/react-sdk"; import * as baseComponents from "@webstudio-is/sdk-components-react"; import * as baseComponentMetas from "@webstudio-is/sdk-components-react/metas"; @@ -108,8 +108,8 @@ const useElementsTree = ( [] ); - const indexesOfTypeWithinRequiredAncestors = useMemo(() => { - return getIndexesOfTypeWithinRequiredAncestors( + const indexesWithinAncestors = useMemo(() => { + return getIndexesWithinAncestors( metas, instances, page ? [page.rootInstanceId] : [] @@ -123,7 +123,7 @@ const useElementsTree = ( assetBaseUrl: params.assetBaseUrl, instances, rootInstanceId, - indexesOfTypeWithinRequiredAncestors, + indexesWithinAncestors, propsByInstanceIdStore, assetsStore, pagesStore: pagesMapStore, @@ -147,7 +147,7 @@ const useElementsTree = ( components, pagesMapStore, isPreviewMode, - indexesOfTypeWithinRequiredAncestors, + indexesWithinAncestors, ]); }; diff --git a/packages/react-sdk/src/component-renderer.tsx b/packages/react-sdk/src/component-renderer.tsx index 9b3747d401..abe8e4f31a 100644 --- a/packages/react-sdk/src/component-renderer.tsx +++ b/packages/react-sdk/src/component-renderer.tsx @@ -12,7 +12,7 @@ import { executeComputingExpressions, executeEffectfulExpression, } from "./expression"; -import { getIndexesOfTypeWithinRequiredAncestors } from "./instance-utils"; +import { getIndexesWithinAncestors } from "./instance-utils"; export const renderComponentTemplate = ({ name, @@ -106,7 +106,7 @@ export const renderComponentTemplate = ({ }} Component={WebstudioComponent} components={new Map(Object.entries(components))} - indexesOfTypeWithinRequiredAncestors={getIndexesOfTypeWithinRequiredAncestors( + indexesWithinAncestors={getIndexesWithinAncestors( metas, new Map(instances), ["root"] diff --git a/packages/react-sdk/src/components/component-meta.ts b/packages/react-sdk/src/components/component-meta.ts index 030dba53fd..afc9659e02 100644 --- a/packages/react-sdk/src/components/component-meta.ts +++ b/packages/react-sdk/src/components/component-meta.ts @@ -53,6 +53,11 @@ const WsComponentMeta = z.object({ type: z.enum(["container", "control", "embed", "rich-text-child"]), requiredAncestors: z.optional(z.array(z.string())), invalidAncestors: z.optional(z.array(z.string())), + // when this field is specified component receives + // prop with index of same components withiin specified ancestor + // important to automatically enumerate collections without + // naming every item manually + indexWithinAncestor: z.optional(z.string()), stylable: z.optional(z.boolean()), // specifies whether the instance can be deleted, // copied or dragged out of its parent instance diff --git a/packages/react-sdk/src/context.tsx b/packages/react-sdk/src/context.tsx index 4de233ffaa..8e66b1cef5 100644 --- a/packages/react-sdk/src/context.tsx +++ b/packages/react-sdk/src/context.tsx @@ -3,7 +3,7 @@ import { createContext } from "react"; import type { Assets } from "@webstudio-is/asset-uploader"; import type { DataSource, Instance, Prop } from "@webstudio-is/project-build"; import type { Pages, PropsByInstanceId } from "./props"; -import type { IndexesOfTypeWithinRequiredAncestors } from "./instance-utils"; +import type { IndexesWithinAncestors } from "./instance-utils"; export type Params = { renderer?: "canvas" | "preview"; @@ -51,7 +51,7 @@ export const ReactSdkContext = createContext< prop: Prop["name"], value: unknown ) => void; - indexesOfTypeWithinRequiredAncestors: IndexesOfTypeWithinRequiredAncestors; + indexesWithinAncestors: IndexesWithinAncestors; } >({ imageBaseUrl: "/", @@ -69,5 +69,5 @@ export const ReactSdkContext = createContext< setBoundDataSourceValue: () => { throw Error("React SDK setBoundDataSourceValue is not implemented"); }, - indexesOfTypeWithinRequiredAncestors: new Map(), + indexesWithinAncestors: new Map(), }); diff --git a/packages/react-sdk/src/embed-template.test.ts b/packages/react-sdk/src/embed-template.test.ts index 5e9e980192..34390d9ab3 100644 --- a/packages/react-sdk/src/embed-template.test.ts +++ b/packages/react-sdk/src/embed-template.test.ts @@ -497,6 +497,7 @@ test("add namespace to selected components in embed template", () => { icon: "", requiredAncestors: ["Button", "Box"], invalidAncestors: ["Tooltip"], + indexWithinAncestor: "Tooltip", template: [ { type: "instance", @@ -527,6 +528,7 @@ test("add namespace to selected components in embed template", () => { icon: "", requiredAncestors: ["my-namespace:Button", "Box"], invalidAncestors: ["my-namespace:Tooltip"], + indexWithinAncestor: "my-namespace:Tooltip", template: [ { type: "instance", diff --git a/packages/react-sdk/src/embed-template.ts b/packages/react-sdk/src/embed-template.ts index 96bcdfc009..452fe43194 100644 --- a/packages/react-sdk/src/embed-template.ts +++ b/packages/react-sdk/src/embed-template.ts @@ -380,6 +380,11 @@ export const namespaceMeta = ( components.has(component) ? `${namespace}:${component}` : component ); } + if (newMeta.indexWithinAncestor) { + newMeta.indexWithinAncestor = components.has(newMeta.indexWithinAncestor) + ? `${namespace}:${newMeta.indexWithinAncestor}` + : newMeta.indexWithinAncestor; + } if (newMeta.template) { newMeta.template = namespaceEmbedTemplateComponents( newMeta.template, diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts index a53925c941..4f7800b448 100644 --- a/packages/react-sdk/src/index.ts +++ b/packages/react-sdk/src/index.ts @@ -18,7 +18,7 @@ export { usePropUrl, usePropAsset, getInstanceIdFromComponentProps, - useIndexOfTypeWithinRequiredAncestors, + getIndexWithinAncestorFromComponentProps, } from "./props"; export { type Params, ReactSdkContext } from "./context"; export { @@ -34,4 +34,4 @@ export { decodeVariablesMap, } from "./expression"; export { renderComponentTemplate } from "./component-renderer"; -export { getIndexesOfTypeWithinRequiredAncestors } from "./instance-utils"; +export { getIndexesWithinAncestors } from "./instance-utils"; diff --git a/packages/react-sdk/src/instance-utils.test.ts b/packages/react-sdk/src/instance-utils.test.ts index 610d141dc9..add530e484 100644 --- a/packages/react-sdk/src/instance-utils.test.ts +++ b/packages/react-sdk/src/instance-utils.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "@jest/globals"; import type { Instance, Instances } from "@webstudio-is/project-build"; -import { getIndexesOfTypeWithinRequiredAncestors } from "./instance-utils"; +import { getIndexesWithinAncestors } from "./instance-utils"; import type { WsComponentMeta } from "."; const getIdValuePair = (item: T) => @@ -21,7 +21,7 @@ const createMeta = (meta?: Partial) => { return { type: "container", label: "", icon: "", ...meta } as const; }; -test("get indexes of type within required ancestors", () => { +test("get indexes within ancestors", () => { // body0 // tabs1 // tabs1list @@ -70,22 +70,20 @@ test("get indexes of type within required ancestors", () => { ["Body", createMeta()], ["Box", createMeta()], ["Tabs", createMeta()], - ["TabsList", createMeta({ requiredAncestors: ["Tabs"] })], - ["TabsTrigger", createMeta({ requiredAncestors: ["TabsList"] })], - ["TabsContent", createMeta({ requiredAncestors: ["Tabs"] })], + ["TabsList", createMeta({ indexWithinAncestor: "Tabs" })], + ["TabsTrigger", createMeta({ indexWithinAncestor: "TabsList" })], + ["TabsContent", createMeta({ indexWithinAncestor: "Tabs" })], ]); - expect( - getIndexesOfTypeWithinRequiredAncestors(metas, instances, ["body0"]) - ).toEqual( + expect(getIndexesWithinAncestors(metas, instances, ["body0"])).toEqual( new Map([ - ["Tabs:tabs1list", 0], - ["TabsList:tabs1trigger1", 0], - ["TabsList:tabs1trigger2", 1], - ["Tabs:tabs1content1", 0], - ["Tabs:tabs1content2", 1], - ["Tabs:tabs2list", 0], - ["TabsList:tabs2trigger1", 0], - ["Tabs:tabs2content1", 0], + ["tabs1list", 0], + ["tabs1trigger1", 0], + ["tabs1trigger2", 1], + ["tabs1content1", 0], + ["tabs1content2", 1], + ["tabs2list", 0], + ["tabs2trigger1", 0], + ["tabs2content1", 0], ]) ); }); diff --git a/packages/react-sdk/src/instance-utils.ts b/packages/react-sdk/src/instance-utils.ts index d3064e8f70..f895866d90 100644 --- a/packages/react-sdk/src/instance-utils.ts +++ b/packages/react-sdk/src/instance-utils.ts @@ -1,27 +1,21 @@ import type { Instance, Instances } from "@webstudio-is/project-build"; import type { WsComponentMeta } from "./components/component-meta"; -export type IndexesOfTypeWithinRequiredAncestors = Map< - // ancestorInstanceComponent;childInstanceId - `${Instance["component"]}:${Instance["id"]}`, - number ->; +export type IndexesWithinAncestors = Map; -export const getIndexesOfTypeWithinRequiredAncestors = ( +export const getIndexesWithinAncestors = ( metas: Map, instances: Instances, rootIds: Instance["id"][] ) => { - const requiredAncestors = new Set(); + const ancestors = new Set(); for (const meta of metas.values()) { - if (meta.requiredAncestors) { - for (const ancestorComponent of meta.requiredAncestors) { - requiredAncestors.add(ancestorComponent); - } + if (meta.indexWithinAncestor !== undefined) { + ancestors.add(meta.indexWithinAncestor); } } - const indexes: IndexesOfTypeWithinRequiredAncestors = new Map(); + const indexes: IndexesWithinAncestors = new Map(); const traverseInstances = ( instances: Instances, @@ -39,21 +33,19 @@ export const getIndexesOfTypeWithinRequiredAncestors = ( if (meta === undefined) { return; } - if (requiredAncestors.has(instance.component)) { + + if (ancestors.has(instance.component)) { latestIndexes = new Map(latestIndexes); latestIndexes.set(instance.component, new Map()); } - if (meta.requiredAncestors) { - for (const ancestorComponent of meta.requiredAncestors) { - const ancestorIndexes = latestIndexes.get(ancestorComponent); - if (ancestorIndexes === undefined) { - continue; - } + if (meta.indexWithinAncestor !== undefined) { + const ancestorIndexes = latestIndexes.get(meta.indexWithinAncestor); + if (ancestorIndexes !== undefined) { let index = ancestorIndexes.get(instance.component) ?? -1; index += 1; ancestorIndexes.set(instance.component, index); - indexes.set(`${ancestorComponent}:${instance.id}`, index); + indexes.set(instance.id, index); } } diff --git a/packages/react-sdk/src/props.ts b/packages/react-sdk/src/props.ts index ffa2cc2e1c..6e7cf6e2d0 100644 --- a/packages/react-sdk/src/props.ts +++ b/packages/react-sdk/src/props.ts @@ -4,7 +4,7 @@ import { useStore } from "@nanostores/react"; import type { Instance, Page, Prop, Props } from "@webstudio-is/project-build"; import type { Asset, Assets } from "@webstudio-is/asset-uploader"; import { ReactSdkContext } from "./context"; -import { idAttribute } from "./tree/webstudio-component"; +import { idAttribute, indexAttribute } from "./tree/webstudio-component"; export type PropsByInstanceId = Map; @@ -32,12 +32,17 @@ export const useInstanceProps = (instanceId: Instance["id"]) => { executeEffectfulExpression, setDataSourceValues, renderer, + indexesWithinAncestors, } = useContext(ReactSdkContext); + const index = indexesWithinAncestors.get(instanceId); const instancePropsObjectStore = useMemo(() => { return computed( [propsByInstanceIdStore, dataSourceValuesStore], (propsByInstanceId, dataSourceValues) => { const instancePropsObject: Record = {}; + if (index !== undefined) { + instancePropsObject[indexAttribute] = index.toString(); + } const instanceProps = propsByInstanceId.get(instanceId); if (instanceProps === undefined) { return instancePropsObject; @@ -89,6 +94,7 @@ export const useInstanceProps = (instanceId: Instance["id"]) => { renderer, executeEffectfulExpression, setDataSourceValues, + index, ]); const instancePropsObject = useStore(instancePropsObjectStore); return instancePropsObject; @@ -214,12 +220,8 @@ export const getInstanceIdFromComponentProps = ( return props[idAttribute] as string; }; -export const useIndexOfTypeWithinRequiredAncestors = ( - props: Record, - ancestorComponent: Instance["component"] +export const getIndexWithinAncestorFromComponentProps = ( + props: Record ) => { - const { indexesOfTypeWithinRequiredAncestors } = useContext(ReactSdkContext); - const instanceId = getInstanceIdFromComponentProps(props); - const key = `${ancestorComponent}:${instanceId}` as const; - return indexesOfTypeWithinRequiredAncestors.get(key) ?? -1; + return props[indexAttribute] as string | undefined; }; diff --git a/packages/react-sdk/src/tree/create-elements-tree.tsx b/packages/react-sdk/src/tree/create-elements-tree.tsx index bfc980bdb9..e099a81172 100644 --- a/packages/react-sdk/src/tree/create-elements-tree.tsx +++ b/packages/react-sdk/src/tree/create-elements-tree.tsx @@ -15,7 +15,7 @@ import { } from "../context"; import type { Pages, PropsByInstanceId } from "../props"; import type { WebstudioComponentProps } from "./webstudio-component"; -import type { IndexesOfTypeWithinRequiredAncestors } from "../instance-utils"; +import type { IndexesWithinAncestors } from "../instance-utils"; type InstanceSelector = Instance["id"][]; @@ -31,7 +31,7 @@ export const createElementsTree = ({ dataSourceValuesStore, executeEffectfulExpression, onDataSourceUpdate, - indexesOfTypeWithinRequiredAncestors, + indexesWithinAncestors, Component, components, scripts, @@ -48,7 +48,7 @@ export const createElementsTree = ({ ) => DataSourceValues; dataSourceValuesStore: ReadableAtom; onDataSourceUpdate: (newValues: DataSourceValues) => void; - indexesOfTypeWithinRequiredAncestors: IndexesOfTypeWithinRequiredAncestors; + indexesWithinAncestors: IndexesWithinAncestors; Component: ForwardRefExoticComponent< WebstudioComponentProps & RefAttributes @@ -91,7 +91,7 @@ export const createElementsTree = ({ renderer, imageBaseUrl, assetBaseUrl, - indexesOfTypeWithinRequiredAncestors, + indexesWithinAncestors, executeEffectfulExpression, setDataSourceValues: onDataSourceUpdate, setBoundDataSourceValue: (instanceId, propName, value) => { diff --git a/packages/react-sdk/src/tree/root.ts b/packages/react-sdk/src/tree/root.ts index 6d06cd90b0..fbb1214c53 100644 --- a/packages/react-sdk/src/tree/root.ts +++ b/packages/react-sdk/src/tree/root.ts @@ -21,7 +21,7 @@ import { import { getPropsByInstanceId } from "../props"; import type { Components } from "../components/components-utils"; import type { Params, DataSourceValues } from "../context"; -import type { IndexesOfTypeWithinRequiredAncestors } from "../instance-utils"; +import type { IndexesWithinAncestors } from "../instance-utils"; export type Data = { page: Page; @@ -37,7 +37,7 @@ export type RootPropsData = Omit & { type RootProps = { data: RootPropsData; - indexesOfTypeWithinRequiredAncestors: IndexesOfTypeWithinRequiredAncestors; + indexesWithinAncestors: IndexesWithinAncestors; executeComputingExpressions: (values: DataSourceValues) => DataSourceValues; executeEffectfulExpression: ( expression: string, @@ -53,7 +53,7 @@ type RootProps = { export const InstanceRoot = ({ data, - indexesOfTypeWithinRequiredAncestors, + indexesWithinAncestors, executeComputingExpressions, executeEffectfulExpression, Component, @@ -123,7 +123,7 @@ export const InstanceRoot = ({ ), assetsStore: atom(new Map(data.assets.map((asset) => [asset.id, asset]))), pagesStore: atom(new Map(data.pages.map((page) => [page.id, page]))), - indexesOfTypeWithinRequiredAncestors, + indexesWithinAncestors, executeEffectfulExpression, dataSourceValuesStore, onDataSourceUpdate, diff --git a/packages/react-sdk/src/tree/webstudio-component.tsx b/packages/react-sdk/src/tree/webstudio-component.tsx index 072914b4df..1f7c9958c0 100644 --- a/packages/react-sdk/src/tree/webstudio-component.tsx +++ b/packages/react-sdk/src/tree/webstudio-component.tsx @@ -65,6 +65,7 @@ export const idAttribute = "data-ws-id" as const; export const selectorIdAttribute = "data-ws-selector" as const; export const componentAttribute = "data-ws-component" as const; export const showAttribute = "data-ws-show" as const; +export const indexAttribute = "data-ws-index" as const; export const collapsedAttribute = "data-ws-collapsed" as const; export type WebstudioAttributes = {