Skip to content

Commit e9e0fb7

Browse files
authored
feat: add wrap in element command (#5228)
Ref #3632 Here "Wrap in Element" is replacing "Wrap in Box" as more general option.
1 parent de61cff commit e9e0fb7

File tree

2 files changed

+65
-43
lines changed

2 files changed

+65
-43
lines changed

apps/builder/app/builder/shared/commands.test.tsx

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, test } from "vitest";
22
import * as baseMetas from "@webstudio-is/sdk-components-react/metas";
33
import { createDefaultPages } from "@webstudio-is/project-build";
4-
import { $, renderData } from "@webstudio-is/template";
4+
import { $, renderData, ws } from "@webstudio-is/template";
55
import {
66
$instances,
77
$pages,
@@ -10,6 +10,7 @@ import {
1010
import { registerContainers } from "~/shared/sync";
1111
import { $awareness, selectInstance } from "~/shared/awareness";
1212
import { deleteSelectedInstance, unwrap, wrapIn } from "./commands";
13+
import { elementComponent } from "@webstudio-is/sdk";
1314

1415
registerContainers();
1516

@@ -81,71 +82,86 @@ describe("wrap in", () => {
8182
test("wrap instance in link", () => {
8283
$instances.set(
8384
renderData(
84-
<$.Body ws:id="body">
85-
<$.Box ws:id="box"></$.Box>
86-
</$.Body>
85+
<ws.element ws:tag="body" ws:id="bodyId">
86+
<ws.element ws:tag="div" ws:id="divId"></ws.element>
87+
</ws.element>
8788
).instances
8889
);
89-
selectInstance(["box", "body"]);
90-
wrapIn("Link");
90+
selectInstance(["divId", "bodyId"]);
91+
wrapIn(elementComponent, "a");
9192
expect($instances.get()).toEqual(
9293
renderData(
93-
<$.Body ws:id="body">
94-
<$.Link ws:id={expect.any(String)}>
95-
<$.Box ws:id="box"></$.Box>
96-
</$.Link>
97-
</$.Body>
94+
<ws.element ws:tag="body" ws:id="bodyId">
95+
<ws.element ws:tag="a" ws:id={expect.any(String)}>
96+
<ws.element ws:tag="div" ws:id="divId"></ws.element>
97+
</ws.element>
98+
</ws.element>
9899
).instances
99100
);
100101
});
101102

102-
test("wrap image in box", () => {
103+
test("wrap image in element", () => {
103104
$instances.set(
104105
renderData(
105-
<$.Body ws:id="body">
106-
<$.Image ws:id="image" />
107-
</$.Body>
106+
<ws.element ws:tag="body" ws:id="bodyId">
107+
<ws.element ws:tag="img" ws:id="imageId"></ws.element>
108+
</ws.element>
108109
).instances
109110
);
110-
selectInstance(["image", "body"]);
111-
wrapIn("Box");
111+
selectInstance(["imageId", "bodyId"]);
112+
wrapIn(elementComponent);
112113
expect($instances.get()).toEqual(
113114
renderData(
114-
<$.Body ws:id="body">
115-
<$.Box ws:id={expect.any(String)}>
116-
<$.Image ws:id="image" />
117-
</$.Box>
118-
</$.Body>
115+
<ws.element ws:tag="body" ws:id="bodyId">
116+
<ws.element ws:tag="div" ws:id={expect.any(String)}>
117+
<ws.element ws:tag="img" ws:id="imageId"></ws.element>
118+
</ws.element>
119+
</ws.element>
119120
).instances
120121
);
121122
});
122123

123124
test("avoid wrapping text with link in link", () => {
124125
const { instances } = renderData(
125-
<$.Body ws:id="body">
126-
<$.Text ws:id="text">
127-
<$.RichTextLink ws:id="richtextlink"></$.RichTextLink>
128-
</$.Text>
129-
</$.Body>
126+
<ws.element ws:tag="body" ws:id="bodyId">
127+
<ws.element ws:tag="p" ws:id="textId">
128+
<ws.element ws:tag="a" ws:id="linkId"></ws.element>
129+
</ws.element>
130+
</ws.element>
130131
);
131132
$instances.set(instances);
132-
selectInstance(["text", "body"]);
133-
wrapIn("Link");
133+
selectInstance(["textId", "bodyId"]);
134+
wrapIn(elementComponent, "a");
134135
// nothing is changed
135136
expect($instances.get()).toEqual(instances);
136137
});
137138

138139
test("avoid wrapping textual content", () => {
139140
const { instances } = renderData(
140-
<$.Body ws:id="body">
141-
<$.Text ws:id="text">
142-
<$.Bold ws:id="bold"></$.Bold>
143-
</$.Text>
144-
</$.Body>
141+
<ws.element ws:tag="body" ws:id="bodyId">
142+
<ws.element ws:tag="div" ws:id="textId">
143+
<ws.element ws:tag="bold" ws:id="boldId"></ws.element>
144+
</ws.element>
145+
</ws.element>
146+
);
147+
$instances.set(instances);
148+
selectInstance(["boldId", "textId", "bodyId"]);
149+
wrapIn(elementComponent);
150+
// nothing is changed
151+
expect($instances.get()).toEqual(instances);
152+
});
153+
154+
test("avoid wrapping list item", () => {
155+
const { instances } = renderData(
156+
<ws.element ws:tag="body" ws:id="bodyId">
157+
<ws.element ws:tag="ul" ws:id="listId">
158+
<ws.element ws:tag="li" ws:id="listItemId"></ws.element>
159+
</ws.element>
160+
</ws.element>
145161
);
146162
$instances.set(instances);
147-
selectInstance(["bold", "text", "body"]);
148-
wrapIn("Box");
163+
selectInstance(["listItemId", "listId", "bodyId"]);
164+
wrapIn(elementComponent);
149165
// nothing is changed
150166
expect($instances.get()).toEqual(instances);
151167
});

apps/builder/app/builder/shared/commands.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { nanoid } from "nanoid";
22
import {
33
blockTemplateComponent,
4+
elementComponent,
45
isComponentDetachable,
56
} from "@webstudio-is/sdk";
67
import type { Instance } from "@webstudio-is/sdk";
@@ -51,6 +52,7 @@ import {
5152
} from "~/shared/content-model";
5253
import { generateFragmentFromHtml } from "~/shared/html";
5354
import { generateFragmentFromTailwind } from "~/shared/tailwind/tailwind";
55+
import { getInstanceLabel } from "./instance-label";
5456

5557
export const $styleSourceInputElement = atom<HTMLInputElement | undefined>();
5658

@@ -141,7 +143,7 @@ export const deleteSelectedInstance = () => {
141143
});
142144
};
143145

144-
export const wrapIn = (component: string) => {
146+
export const wrapIn = (component: string, tag?: string) => {
145147
const instancePath = $selectedInstancePath.get();
146148
// global root or body are selected
147149
if (instancePath === undefined || instancePath.length === 1) {
@@ -170,6 +172,9 @@ export const wrapIn = (component: string) => {
170172
component,
171173
children: [{ type: "id", value: selectedInstance.id }],
172174
};
175+
if (tag || elementComponent) {
176+
newInstance.tag = tag ?? "div";
177+
}
173178
const parentInstance = data.instances.get(parentItem.instance.id);
174179
data.instances.set(newInstanceId, newInstance);
175180
if (parentInstance) {
@@ -179,14 +184,15 @@ export const wrapIn = (component: string) => {
179184
}
180185
}
181186
}
182-
const matches = isTreeSatisfyingContentModel({
187+
const isSatisfying = isTreeSatisfyingContentModel({
183188
instances: data.instances,
184189
props: data.props,
185190
metas,
186191
instanceSelector: newInstanceSelector,
187192
});
188-
if (matches === false) {
189-
toast.error(`Cannot wrap in "${component}"`);
193+
if (isSatisfying === false) {
194+
const label = getInstanceLabel({ component, tag }, {});
195+
toast.error(`Cannot wrap in ${label}`);
190196
throw Error("Abort transaction");
191197
}
192198
});
@@ -510,12 +516,12 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({
510516
},
511517
},
512518
{
513-
name: "wrapInBox",
514-
handler: () => wrapIn("Box"),
519+
name: "wrapInElement",
520+
handler: () => wrapIn(elementComponent),
515521
},
516522
{
517523
name: "wrapInLink",
518-
handler: () => wrapIn("Link"),
524+
handler: () => wrapIn(elementComponent, "a"),
519525
},
520526
{
521527
name: "unwrap",

0 commit comments

Comments
 (0)