Skip to content

Commit 8238438

Browse files
authored
fix: navigator dnd bugs (#4546)
- fixed dropping into non-container instances (like image) - fixed dropping instances from another block
1 parent 1d2138a commit 8238438

File tree

5 files changed

+118
-15
lines changed

5 files changed

+118
-15
lines changed

apps/builder/app/builder/features/navigator/navigator-tree.tsx

+21-6
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import {
5959
getInstanceKey,
6060
selectInstance,
6161
} from "~/shared/awareness";
62-
import { isTreeMatching } from "~/shared/matcher";
62+
import { findClosestContainer, isTreeMatching } from "~/shared/matcher";
6363

6464
type TreeItemAncestor =
6565
| undefined
@@ -465,20 +465,35 @@ const canDrop = (
465465
) => {
466466
const dropSelector = dropTarget.itemSelector;
467467
const instances = $instances.get();
468-
469-
// in content mode allow drop only inside of block
468+
const metas = $registeredComponentMetas.get();
469+
// in content mode allow drop only within same block
470470
if ($isContentMode.get()) {
471471
const parentInstance = instances.get(dropSelector[0]);
472472
if (parentInstance?.component !== blockComponent) {
473473
return false;
474474
}
475+
// parent of dragging item should be the same as drop target
476+
if (dropSelector[0] !== dragSelector[1]) {
477+
return false;
478+
}
479+
}
480+
// prevent dropping into non-container instances
481+
const closestContainerIndex = findClosestContainer({
482+
metas,
483+
instances,
484+
instanceSelector: dropSelector,
485+
});
486+
if (closestContainerIndex !== 0) {
487+
return false;
475488
}
476-
477489
return isTreeMatching({
478490
instances,
479-
metas: $registeredComponentMetas.get(),
491+
metas,
480492
// make sure dragging tree can be put inside of drop instance
481-
instanceSelector: [dragSelector[0], ...dropSelector],
493+
instanceSelector: [
494+
dragSelector[0],
495+
...dropSelector.slice(closestContainerIndex),
496+
],
482497
});
483498
};
484499

apps/builder/app/shared/instance-utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import { setDifference, setUnion } from "./shim";
6464
import { breakCyclesMutable, findCycles } from "@webstudio-is/project-build";
6565
import { $awareness, $selectedPage, selectInstance } from "./awareness";
6666
import {
67-
findClosestContainer,
67+
findClosestNonTextualContainer,
6868
findClosestInstanceMatchingFragment,
6969
isTreeMatching,
7070
} from "./matcher";
@@ -1436,7 +1436,7 @@ export const findClosestInsertable = (
14361436
}
14371437
const metas = $registeredComponentMetas.get();
14381438
const instances = $instances.get();
1439-
const closestContainerIndex = findClosestContainer({
1439+
const closestContainerIndex = findClosestNonTextualContainer({
14401440
metas,
14411441
instances,
14421442
instanceSelector,

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

+67-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import * as baseMetas from "@webstudio-is/sdk-components-react/metas";
55
import type { WsComponentMeta } from "@webstudio-is/react-sdk";
66
import type { Matcher, WebstudioFragment } from "@webstudio-is/sdk";
77
import {
8-
findClosestContainer,
8+
findClosestNonTextualContainer,
99
findClosestInstanceMatchingFragment,
1010
isInstanceMatching,
1111
isTreeMatching,
12+
findClosestContainer,
1213
} from "./matcher";
1314

1415
const metas = new Map(Object.entries({ ...coreMetas, ...baseMetas }));
@@ -885,7 +886,7 @@ describe("find closest container", () => {
885886
).toEqual(1);
886887
});
887888

888-
test("skips containers with text", () => {
889+
test("allow containers with text", () => {
889890
expect(
890891
findClosestContainer({
891892
...renderJsx(
@@ -898,12 +899,74 @@ describe("find closest container", () => {
898899
metas,
899900
instanceSelector: ["box-with-text", "box", "body"],
900901
})
902+
).toEqual(0);
903+
});
904+
905+
test("allow containers with expression", () => {
906+
expect(
907+
findClosestContainer({
908+
...renderJsx(
909+
<$.Body ws:id="body">
910+
<$.Box ws:id="box">
911+
<$.Box ws:id="box-with-expr">
912+
{new ExpressionValue("1 + 1")}
913+
</$.Box>
914+
</$.Box>
915+
</$.Body>
916+
),
917+
metas,
918+
instanceSelector: ["box-with-expr", "box", "body"],
919+
})
920+
).toEqual(0);
921+
});
922+
923+
test("allow root with text", () => {
924+
expect(
925+
findClosestContainer({
926+
...renderJsx(<$.Body ws:id="body">text</$.Body>),
927+
metas,
928+
instanceSelector: ["body"],
929+
})
930+
).toEqual(0);
931+
});
932+
});
933+
934+
describe("find closest non textual container", () => {
935+
test("skips non-container instances", () => {
936+
expect(
937+
findClosestNonTextualContainer({
938+
...renderJsx(
939+
<$.Body ws:id="body">
940+
<$.Box ws:id="box">
941+
<$.Image ws:id="image" />
942+
</$.Box>
943+
</$.Body>
944+
),
945+
metas,
946+
instanceSelector: ["image", "box", "body"],
947+
})
948+
).toEqual(1);
949+
});
950+
951+
test("skips containers with text", () => {
952+
expect(
953+
findClosestNonTextualContainer({
954+
...renderJsx(
955+
<$.Body ws:id="body">
956+
<$.Box ws:id="box">
957+
<$.Box ws:id="box-with-text">text</$.Box>
958+
</$.Box>
959+
</$.Body>
960+
),
961+
metas,
962+
instanceSelector: ["box-with-text", "box", "body"],
963+
})
901964
).toEqual(1);
902965
});
903966

904967
test("skips containers with expression", () => {
905968
expect(
906-
findClosestContainer({
969+
findClosestNonTextualContainer({
907970
...renderJsx(
908971
<$.Body ws:id="body">
909972
<$.Box ws:id="box">
@@ -921,7 +984,7 @@ describe("find closest container", () => {
921984

922985
test("allow root with text", () => {
923986
expect(
924-
findClosestContainer({
987+
findClosestNonTextualContainer({
925988
...renderJsx(<$.Body ws:id="body">text</$.Body>),
926989
metas,
927990
instanceSelector: ["body"],

apps/builder/app/shared/matcher.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,31 @@ export const findClosestContainer = ({
301301
continue;
302302
}
303303
const meta = metas.get(instance.component);
304-
if (meta === undefined) {
304+
if (meta?.type === "container") {
305+
return index;
306+
}
307+
}
308+
return -1;
309+
};
310+
311+
export const findClosestNonTextualContainer = ({
312+
metas,
313+
instances,
314+
instanceSelector,
315+
}: {
316+
metas: Map<string, WsComponentMeta>;
317+
instances: Instances;
318+
instanceSelector: InstanceSelector;
319+
}) => {
320+
// page root with text can be used as container
321+
if (instanceSelector.length === 1) {
322+
return 0;
323+
}
324+
for (let index = 0; index < instanceSelector.length; index += 1) {
325+
const instanceId = instanceSelector[index];
326+
const instance = instances.get(instanceId);
327+
// collection item can be undefined
328+
if (instance === undefined) {
305329
continue;
306330
}
307331
let hasText = false;
@@ -313,7 +337,8 @@ export const findClosestContainer = ({
313337
if (hasText) {
314338
continue;
315339
}
316-
if (meta.type === "container") {
340+
const meta = metas.get(instance.component);
341+
if (meta?.type === "container") {
317342
return index;
318343
}
319344
}

packages/react-sdk/src/core-components.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ const blockMeta: WsComponentMeta = {
151151
icon: EditIcon,
152152
constraints: {
153153
relation: "ancestor",
154-
component: { $neq: collectionComponent },
154+
component: { $nin: [collectionComponent, blockComponent] },
155155
},
156156
stylable: false,
157157
template: [

0 commit comments

Comments
 (0)