mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 14:03:33 +09:00
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:
1 parent
f751045e22
commit
40e71b6c5b
13 files changed
+69
-64
No files matched your search
@@ -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,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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
@@ -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],
|
||||
])
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user