Skip to content

Commit a449f66

Browse files
authored
refactor: describe descendants in component content model (#5140)
Here replaced "transparent" keyword with more explicit "descendants" list in component content model. This will enable use case with child constraint while allowing other instances to be children like here ``` contentModel: { category: "instance", children: [ "instance", "AnimateText", "StaggerAnimation", "VideoAnimation", ], }, ``` and descendants are described like this ``` contentModel: { category: "instance", children: ["instance"], descendants: ["VimeoSpinner", "VimeoPlayButton", "VimeoPreviewImage"], }, ```
1 parent 4e13b0d commit a449f66

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+106
-168
lines changed

apps/builder/app/shared/content-model.test.tsx

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -561,41 +561,6 @@ describe("component content model", () => {
561561
).toBeFalsy();
562562
});
563563

564-
test("prevent self nesting with descendants restriction", () => {
565-
expect(
566-
isTreeSatisfyingContentModel({
567-
...renderData(
568-
<ws.element ws:tag="body" ws:id="bodyId">
569-
<$.Vimeo>
570-
<$.VimeoSpinner>
571-
<$.VimeoSpinner></$.VimeoSpinner>
572-
</$.VimeoSpinner>
573-
</$.Vimeo>
574-
</ws.element>
575-
),
576-
metas: defaultMetas,
577-
instanceSelector: ["bodyId"],
578-
})
579-
).toBeFalsy();
580-
expect(
581-
isTreeSatisfyingContentModel({
582-
...renderData(
583-
<ws.element ws:tag="body" ws:id="bodyId">
584-
<$.Vimeo>
585-
<$.VimeoSpinner>
586-
<$.Vimeo>
587-
<$.VimeoSpinner></$.VimeoSpinner>
588-
</$.Vimeo>
589-
</$.VimeoSpinner>
590-
</$.Vimeo>
591-
</ws.element>
592-
),
593-
metas: defaultMetas,
594-
instanceSelector: ["bodyId"],
595-
})
596-
).toBeTruthy();
597-
});
598-
599564
test("pass constraints when check deep in the tree", () => {
600565
expect(
601566
isTreeSatisfyingContentModel({

apps/builder/app/shared/content-model.ts

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ const computeAllowedCategories = ({
178178

179179
const defaultComponentContentModel: ContentModel = {
180180
category: "instance",
181-
children: ["rich-text", "instance", "transparent"],
181+
children: ["rich-text", "instance"],
182182
};
183183

184184
const getComponentContentModel = (meta: undefined | WsComponentMeta) =>
@@ -187,45 +187,29 @@ const getComponentContentModel = (meta: undefined | WsComponentMeta) =>
187187
const isComponentSatisfyingContentModel = ({
188188
metas,
189189
component,
190-
allowedCategories,
190+
allowedParentCategories,
191+
allowedAncestorCategories,
191192
}: {
192193
metas: Metas;
193194
component: string;
194-
allowedCategories: undefined | string[];
195+
allowedParentCategories: undefined | string[];
196+
allowedAncestorCategories: undefined | string[];
195197
}) => {
196198
const contentModel = getComponentContentModel(metas.get(component));
197199
return (
198200
// body does not have parent
199-
allowedCategories === undefined ||
201+
allowedParentCategories === undefined ||
200202
// parents may restrict specific components with none category
201203
// any instances
202204
// or nothing
203-
allowedCategories.includes(component) ||
204-
allowedCategories.includes(contentModel.category)
205+
allowedParentCategories.includes(component) ||
206+
allowedParentCategories.includes(contentModel.category) ||
207+
allowedAncestorCategories?.includes(component) === true ||
208+
allowedAncestorCategories?.includes(contentModel.category) === true
205209
);
206210
};
207211

208-
const getComponentChildrenCategories = ({
209-
metas,
210-
component,
211-
allowedCategories,
212-
}: {
213-
metas: Metas;
214-
component: string;
215-
allowedCategories: undefined | string[];
216-
}) => {
217-
const contentModel = getComponentContentModel(metas.get(component));
218-
let childrenCategories = contentModel.children;
219-
// transparent categories makes component inherit constraints from parent
220-
if (childrenCategories.includes("transparent") && allowedCategories) {
221-
childrenCategories = Array.from(
222-
new Set([...childrenCategories, ...allowedCategories])
223-
);
224-
}
225-
return childrenCategories;
226-
};
227-
228-
const computeAllowedComponentCategories = ({
212+
const computeAllowedAncestorCategories = ({
229213
instances,
230214
metas,
231215
instanceSelector,
@@ -234,24 +218,43 @@ const computeAllowedComponentCategories = ({
234218
metas: Metas;
235219
instanceSelector: InstanceSelector;
236220
}) => {
237-
let instance: undefined | Instance;
238221
let allowedCategories: undefined | string[];
239222
// skip selected instance for which these constraints are computed
240223
for (const instanceId of instanceSelector.slice(1).reverse()) {
241-
instance = instances.get(instanceId);
224+
const instance = instances.get(instanceId);
242225
// collection item can be undefined
243226
if (instance === undefined) {
244227
continue;
245228
}
246-
allowedCategories = getComponentChildrenCategories({
247-
metas,
248-
component: instance.component,
249-
allowedCategories,
250-
});
229+
const contentModel = getComponentContentModel(
230+
metas.get(instance.component)
231+
);
232+
if (contentModel.descendants) {
233+
allowedCategories ??= [];
234+
allowedCategories = [...allowedCategories, ...contentModel.descendants];
235+
}
251236
}
252237
return allowedCategories;
253238
};
254239

240+
const getAllowedParentCategories = ({
241+
instances,
242+
metas,
243+
instanceSelector,
244+
}: {
245+
instances: Instances;
246+
metas: Metas;
247+
instanceSelector: InstanceSelector;
248+
}) => {
249+
const instanceId = instanceSelector[1];
250+
const instance = instances.get(instanceId);
251+
if (instance === undefined) {
252+
return;
253+
}
254+
const contentModel = getComponentContentModel(metas.get(instance.component));
255+
return contentModel.children;
256+
};
257+
255258
/**
256259
* Check all tags starting with specified instance select
257260
* for example
@@ -294,15 +297,17 @@ export const isTreeSatisfyingContentModel = ({
294297
instanceSelector,
295298
onError,
296299
_allowedCategories: allowedCategories,
297-
_allowedComponentCategories: allowedComponentCategories,
300+
_allowedAncestorCategories: allowedAncestorCategories,
301+
_allowedParentCategories: allowedParentCategories,
298302
}: {
299303
instances: Instances;
300304
props: Props;
301305
metas: Metas;
302306
instanceSelector: InstanceSelector;
303307
onError?: (message: string) => void;
304308
_allowedCategories?: string[];
305-
_allowedComponentCategories?: string[];
309+
_allowedAncestorCategories?: string[];
310+
_allowedParentCategories?: string[];
306311
}): boolean => {
307312
// compute constraints only when not passed from parent
308313
allowedCategories ??= computeAllowedCategories({
@@ -311,7 +316,12 @@ export const isTreeSatisfyingContentModel = ({
311316
props,
312317
metas,
313318
});
314-
allowedComponentCategories ??= computeAllowedComponentCategories({
319+
allowedParentCategories ??= getAllowedParentCategories({
320+
instanceSelector,
321+
instances,
322+
metas,
323+
});
324+
allowedAncestorCategories ??= computeAllowedAncestorCategories({
315325
instanceSelector,
316326
instances,
317327
metas,
@@ -344,7 +354,8 @@ export const isTreeSatisfyingContentModel = ({
344354
const isComponentSatisfying = isComponentSatisfyingContentModel({
345355
metas,
346356
component: instance.component,
347-
allowedCategories: allowedComponentCategories,
357+
allowedParentCategories,
358+
allowedAncestorCategories,
348359
});
349360
if (isComponentSatisfying === false) {
350361
const [_namespace, name] = parseComponentName(instance.component);
@@ -363,6 +374,16 @@ export const isTreeSatisfyingContentModel = ({
363374
}
364375
}
365376
let isSatisfying = isTagSatisfying && isComponentSatisfying;
377+
const contentModel = getComponentContentModel(metas.get(instance.component));
378+
allowedCategories = getTagChildrenCategories(tag, allowedCategories);
379+
allowedParentCategories = contentModel.children;
380+
if (contentModel.descendants) {
381+
allowedAncestorCategories ??= [];
382+
allowedAncestorCategories = [
383+
...allowedAncestorCategories,
384+
...contentModel.descendants,
385+
];
386+
}
366387
for (const child of instance.children) {
367388
if (child.type === "id") {
368389
isSatisfying &&= isTreeSatisfyingContentModel({
@@ -371,12 +392,9 @@ export const isTreeSatisfyingContentModel = ({
371392
metas,
372393
instanceSelector: [child.value, ...instanceSelector],
373394
onError,
374-
_allowedCategories: getTagChildrenCategories(tag, allowedCategories),
375-
_allowedComponentCategories: getComponentChildrenCategories({
376-
metas,
377-
component: instance.component,
378-
allowedCategories: allowedComponentCategories,
379-
}),
395+
_allowedCategories: allowedCategories,
396+
_allowedParentCategories: allowedParentCategories,
397+
_allowedAncestorCategories: allowedAncestorCategories,
380398
});
381399
}
382400
}

packages/sdk-components-animation/src/animate-children.ws.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AnimationGroupIcon } from "@webstudio-is/icons/svg";
22
import type { WsComponentMeta, WsComponentPropsMeta } from "@webstudio-is/sdk";
3+
import { animation } from "./shared/meta";
34

45
export const meta: WsComponentMeta = {
56
category: "animations",
@@ -8,9 +9,14 @@ export const meta: WsComponentMeta = {
89
icon: AnimationGroupIcon,
910
order: 5,
1011
label: "Animation Group",
11-
constraints: {
12-
relation: "child",
13-
text: false,
12+
contentModel: {
13+
category: "instance",
14+
children: [
15+
"instance",
16+
animation.AnimateText,
17+
animation.StaggerAnimation,
18+
animation.VideoAnimation,
19+
],
1420
},
1521
};
1622

packages/sdk-components-animation/src/animate-text.ws.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { TextAnimationIcon } from "@webstudio-is/icons/svg";
22
import type { WsComponentMeta, WsComponentPropsMeta } from "@webstudio-is/sdk";
3-
import { animation } from "./shared/meta";
4-
import { props } from "./__generated__/animate-text.props";
53
import { div } from "@webstudio-is/sdk/normalize.css";
4+
import { props } from "./__generated__/animate-text.props";
65

76
export const meta: WsComponentMeta = {
87
category: "animations",
@@ -12,13 +11,10 @@ export const meta: WsComponentMeta = {
1211
icon: TextAnimationIcon,
1312
order: 6,
1413
label: "Text Animation",
15-
constraints: [
16-
{ relation: "parent", component: { $eq: animation.AnimateChildren } },
17-
{
18-
relation: "child",
19-
text: false,
20-
},
21-
],
14+
contentModel: {
15+
category: "none",
16+
children: ["instance"],
17+
},
2218
presetStyle: {
2319
div,
2420
},

packages/sdk-components-animation/src/stagger-animation.ws.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { StaggerAnimationIcon } from "@webstudio-is/icons/svg";
22
import type { WsComponentMeta, WsComponentPropsMeta } from "@webstudio-is/sdk";
3-
import { animation } from "./shared/meta";
4-
import { props } from "./__generated__/stagger-animation.props";
53
import { div } from "@webstudio-is/sdk/normalize.css";
4+
import { props } from "./__generated__/stagger-animation.props";
65

76
export const meta: WsComponentMeta = {
87
category: "animations",
@@ -12,13 +11,10 @@ export const meta: WsComponentMeta = {
1211
icon: StaggerAnimationIcon,
1312
order: 6,
1413
label: "Stagger Animation",
15-
constraints: [
16-
{ relation: "parent", component: { $eq: animation.AnimateChildren } },
17-
{
18-
relation: "child",
19-
text: false,
20-
},
21-
],
14+
contentModel: {
15+
category: "none",
16+
children: ["instance"],
17+
},
2218
presetStyle: {
2319
div,
2420
},

packages/sdk-components-animation/src/video-animation.ws.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
import { Youtube1cIcon } from "@webstudio-is/icons/svg";
22
import type { WsComponentMeta, WsComponentPropsMeta } from "@webstudio-is/sdk";
3-
import { animation } from "./shared/meta";
43
import { props } from "./__generated__/video-animation.props";
54

65
export const meta: WsComponentMeta = {
76
type: "container",
87
icon: Youtube1cIcon,
98
label: "Video Animation",
10-
constraints: [
11-
{ relation: "parent", component: { $eq: animation.AnimateChildren } },
12-
{
13-
relation: "child",
14-
text: false,
15-
},
16-
],
9+
contentModel: {
10+
category: "none",
11+
children: ["instance"],
12+
},
1713
};
1814

1915
export const propsMeta: WsComponentPropsMeta = {

packages/sdk-components-react/src/blockquote.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ const presetStyle = {
6060
} satisfies PresetStyle<typeof defaultTag>;
6161

6262
export const meta: WsComponentMeta = {
63-
type: "container",
6463
placeholder: "Blockquote",
6564
icon: BlockquoteIcon,
6665
states: defaultStates,

packages/sdk-components-react/src/body.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const presetStyle = {
2424
} satisfies PresetStyle<typeof defaultTag>;
2525

2626
export const meta: WsComponentMeta = {
27-
type: "container",
2827
icon: BodyIcon,
2928
states: defaultStates,
3029
presetStyle,

packages/sdk-components-react/src/bold.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const presetStyle = {
1414
} satisfies PresetStyle<typeof defaultTag>;
1515

1616
export const meta: WsComponentMeta = {
17-
type: "container",
1817
label: "Bold Text",
1918
icon: BoldIcon,
2019
states: defaultStates,

packages/sdk-components-react/src/box.ws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { props } from "./__generated__/box.props";
2020

2121
export const meta: WsComponentMeta = {
2222
category: "general",
23-
type: "container",
2423
description:
2524
"A container for content. By default this is a Div, but the tag can be changed in settings.",
2625
icon: BoxIcon,

0 commit comments

Comments
 (0)