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
```
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-08-01 22:40:16 +04:00
1 parent f751045e22
commit 40e71b6c5b
13 files changed
+69 -64

No files matched your search

+5 -5
View File
@@ -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,
]);
};
@@ -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"]
@@ -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
+3 -3
View File
@@ -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(),
});
@@ -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",
+5
View File
@@ -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,
+2 -2
View File
@@ -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";
+14 -16
View File
@@ -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 = <T extends { id: string }>(item: T) =>
@@ -21,7 +21,7 @@ const createMeta = (meta?: Partial<WsComponentMeta>) => {
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],
])
);
});
+12 -20
View File
@@ -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<Instance["id"], number>;
export const getIndexesOfTypeWithinRequiredAncestors = (
export const getIndexesWithinAncestors = (
metas: Map<Instance["component"], WsComponentMeta>,
instances: Instances,
rootIds: Instance["id"][]
) => {
const requiredAncestors = new Set<Instance["component"]>();
const ancestors = new Set<Instance["component"]>();
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);
}
}
+10 -8
View File
@@ -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<Instance["id"], Prop[]>;
@@ -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<Prop["name"], unknown> = {};
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<string, unknown>,
ancestorComponent: Instance["component"]
export const getIndexWithinAncestorFromComponentProps = (
props: Record<string, unknown>
) => {
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;
};
@@ -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<DataSourceValues>;
onDataSourceUpdate: (newValues: DataSourceValues) => void;
indexesOfTypeWithinRequiredAncestors: IndexesOfTypeWithinRequiredAncestors;
indexesWithinAncestors: IndexesWithinAncestors;
Component: ForwardRefExoticComponent<
WebstudioComponentProps & RefAttributes<HTMLElement>
@@ -91,7 +91,7 @@ export const createElementsTree = ({
renderer,
imageBaseUrl,
assetBaseUrl,
indexesOfTypeWithinRequiredAncestors,
indexesWithinAncestors,
executeEffectfulExpression,
setDataSourceValues: onDataSourceUpdate,
setBoundDataSourceValue: (instanceId, propName, value) => {
+4 -4
View File
@@ -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<Data, "build"> & {
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,
@@ -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 = {