diff --git a/apps/builder/app/builder/features/ai/apply-operations.ts b/apps/builder/app/builder/features/ai/apply-operations.ts
index 0db3f76145b5..0b46a30e85c2 100644
--- a/apps/builder/app/builder/features/ai/apply-operations.ts
+++ b/apps/builder/app/builder/features/ai/apply-operations.ts
@@ -20,7 +20,7 @@ import {
$styles,
} from "~/shared/nano-states";
import type { InstanceSelector } from "~/shared/tree-utils";
-import { $selectedInstance } from "~/shared/awareness";
+import { $selectedInstance, getInstancePath } from "~/shared/awareness";
import { isInstanceDetachable } from "~/shared/matcher";
export const applyOperations = (operations: operations.WsOperations) => {
@@ -107,7 +107,10 @@ const deleteInstanceByOp = (
) {
return;
}
- deleteInstanceMutable(data, instanceSelector);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(instanceSelector, data.instances)
+ );
});
}
};
diff --git a/apps/builder/app/builder/features/pages/page-utils.ts b/apps/builder/app/builder/features/pages/page-utils.ts
index fc1e0f58d59b..8a825c71c408 100644
--- a/apps/builder/app/builder/features/pages/page-utils.ts
+++ b/apps/builder/app/builder/features/pages/page-utils.ts
@@ -23,7 +23,12 @@ import {
$variableValuesByInstanceSelector,
} from "~/shared/nano-states";
import { insertPageCopyMutable } from "~/shared/page-utils";
-import { $selectedPage, getInstanceKey, selectPage } from "~/shared/awareness";
+import {
+ $selectedPage,
+ getInstanceKey,
+ getInstancePath,
+ selectPage,
+} from "~/shared/awareness";
/**
* When page or folder needs to be deleted or moved to a different parent,
@@ -209,7 +214,10 @@ export const deletePageMutable = (pageId: Page["id"], data: WebstudioData) => {
}
const rootInstanceId = findPageByIdOrPath(pageId, pages)?.rootInstanceId;
if (rootInstanceId !== undefined) {
- deleteInstanceMutable(data, [rootInstanceId]);
+ deleteInstanceMutable(
+ data,
+ getInstancePath([rootInstanceId], data.instances)
+ );
}
removeByMutable(pages.pages, (page) => page.id === pageId);
cleanupChildRefsMutable(pageId, pages.folders);
diff --git a/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx b/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx
index 66f797bf786e..dec4f12a128d 100644
--- a/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx
+++ b/apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx
@@ -48,6 +48,7 @@ import { skipInertHandlersAttribute } from "~/builder/shared/inert-handlers";
import { insertTemplateAt } from "./block-utils";
import { useEffectEvent } from "~/shared/hook-utils/effect-event";
+import { getInstancePath } from "~/shared/awareness";
export const TemplatesMenu = ({
onOpenChange,
@@ -382,7 +383,10 @@ export const BlockChildHoveredInstanceOutline = () => {
}
updateWebstudioData((data) => {
- deleteInstanceMutable(data, outline.selector);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(outline.selector, data.instances)
+ );
});
setButtonOutline(undefined);
diff --git a/apps/builder/app/builder/shared/commands.ts b/apps/builder/app/builder/shared/commands.ts
index 829489ebfac2..346c7a3c23ab 100644
--- a/apps/builder/app/builder/shared/commands.ts
+++ b/apps/builder/app/builder/shared/commands.ts
@@ -133,7 +133,7 @@ export const deleteSelectedInstance = () => {
newSelectedInstanceSelector = parentInstanceSelector;
}
updateWebstudioData((data) => {
- if (deleteInstanceMutable(data, selectedInstanceSelector)) {
+ if (deleteInstanceMutable(data, instancePath)) {
selectInstance(newSelectedInstanceSelector);
}
});
diff --git a/apps/builder/app/canvas/features/text-editor/text-editor.tsx b/apps/builder/app/canvas/features/text-editor/text-editor.tsx
index 9c3a3a73c6e6..5bed9ea23988 100644
--- a/apps/builder/app/canvas/features/text-editor/text-editor.tsx
+++ b/apps/builder/app/canvas/features/text-editor/text-editor.tsx
@@ -89,6 +89,7 @@ import { setDataCollapsed } from "~/canvas/collapsed";
import {
$selectedPage,
addTemporaryInstance,
+ getInstancePath,
selectInstance,
} from "~/shared/awareness";
import { shallowEqual } from "shallow-equal";
@@ -1044,7 +1045,10 @@ const RichTextContentPluginInternal = ({
if (blockChildSelector) {
updateWebstudioData((data) => {
- deleteInstanceMutable(data, rootInstanceSelector);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(rootInstanceSelector, data.instances)
+ );
});
}
}
@@ -1113,7 +1117,10 @@ const RichTextContentPluginInternal = ({
updateWebstudioData((data) => {
deleteInstanceMutable(
data,
- isLastChild ? parentInstanceSelector : rootInstanceSelector
+ getInstancePath(
+ isLastChild ? parentInstanceSelector : rootInstanceSelector,
+ data.instances
+ )
);
});
@@ -1128,7 +1135,10 @@ const RichTextContentPluginInternal = ({
onNext(editor.getEditorState(), { reason: "left" });
updateWebstudioData((data) => {
- deleteInstanceMutable(data, blockChildSelector);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(blockChildSelector, data.instances)
+ );
});
event.preventDefault();
@@ -1218,7 +1228,12 @@ const RichTextContentPluginInternal = ({
updateWebstudioData((data) => {
deleteInstanceMutable(
data,
- isLastChild ? parentInstanceSelector : rootInstanceSelector
+ getInstancePath(
+ isLastChild
+ ? parentInstanceSelector
+ : rootInstanceSelector,
+ data.instances
+ )
);
});
}
diff --git a/apps/builder/app/shared/awareness.ts b/apps/builder/app/shared/awareness.ts
index cabac0981a6a..0303c18245f6 100644
--- a/apps/builder/app/shared/awareness.ts
+++ b/apps/builder/app/shared/awareness.ts
@@ -81,19 +81,19 @@ export type InstancePath = Array<{
instanceSelector: string[];
}>;
-const getInstancePath = (
+export const getInstancePath = (
+ instanceSelector: string[],
instances: Instances,
- virtualInstances: Instances,
- temporaryInstances: Instances,
- instanceSelector: string[]
+ virtualInstances?: Instances,
+ temporaryInstances?: Instances
): InstancePath => {
const instancePath: InstancePath = [];
for (let index = 0; index < instanceSelector.length; index += 1) {
const instanceId = instanceSelector[index];
const instance =
instances.get(instanceId) ??
- virtualInstances.get(instanceId) ??
- temporaryInstances.get(instanceId);
+ virtualInstances?.get(instanceId) ??
+ temporaryInstances?.get(instanceId);
// collection item can be undefined
if (instance === undefined) {
continue;
@@ -114,10 +114,10 @@ export const $selectedInstancePath = computed(
return;
}
return getInstancePath(
+ instanceSelector,
instances,
virtualInstances,
- temporaryInstances,
- instanceSelector
+ temporaryInstances
);
}
);
@@ -134,10 +134,10 @@ export const $selectedInstancePathWithRoot = computed(
instanceSelector = [...instanceSelector, ROOT_INSTANCE_ID];
}
return getInstancePath(
+ instanceSelector,
instances,
virtualInstances,
- temporaryInstances,
- instanceSelector
+ temporaryInstances
);
}
);
diff --git a/apps/builder/app/shared/copy-paste/plugin-instance.ts b/apps/builder/app/shared/copy-paste/plugin-instance.ts
index 5faeb5f7de5e..c9ae48c63b2f 100644
--- a/apps/builder/app/shared/copy-paste/plugin-instance.ts
+++ b/apps/builder/app/shared/copy-paste/plugin-instance.ts
@@ -25,6 +25,7 @@ import {
findClosestInsertable,
} from "../instance-utils";
import { isInstanceDetachable } from "../matcher";
+import { $selectedInstancePath } from "../awareness";
const version = "@webstudio/instance/v0.1";
@@ -203,20 +204,20 @@ export const onCopy = () => {
};
export const onCut = () => {
- const selectedInstanceSelector = $selectedInstanceSelector.get();
- if (selectedInstanceSelector === undefined) {
+ const instancePath = $selectedInstancePath.get();
+ if (instancePath === undefined) {
return;
}
// @todo tell user they can't delete root
- if (selectedInstanceSelector.length === 1) {
+ if (instancePath.length === 1) {
return;
}
- const data = getTreeData(selectedInstanceSelector);
+ const data = getTreeData(instancePath[0].instanceSelector);
if (data === undefined) {
return;
}
updateWebstudioData((data) => {
- deleteInstanceMutable(data, selectedInstanceSelector);
+ deleteInstanceMutable(data, instancePath);
});
if (data === undefined) {
return;
diff --git a/apps/builder/app/shared/instance-utils.test.tsx b/apps/builder/app/shared/instance-utils.test.tsx
index dd070ff6c7c9..6b23621f0dd5 100644
--- a/apps/builder/app/shared/instance-utils.test.tsx
+++ b/apps/builder/app/shared/instance-utils.test.tsx
@@ -8,17 +8,16 @@ import {
expression,
renderTemplate,
renderData,
+ ResourceValue,
} from "@webstudio-is/template";
import * as defaultMetas from "@webstudio-is/sdk-components-react/metas";
import * as radixMetas from "@webstudio-is/sdk-components-react-radix/metas";
import type {
Asset,
Breakpoint,
- DataSource,
Instance,
Instances,
Prop,
- Resource,
StyleDecl,
StyleDeclKey,
StyleSource,
@@ -59,7 +58,7 @@ import {
$resources,
} from "./nano-states";
import { registerContainers } from "./sync";
-import { $awareness, selectInstance } from "./awareness";
+import { $awareness, getInstancePath, selectInstance } from "./awareness";
enableMapSet();
registerContainers();
@@ -122,14 +121,6 @@ const createInstance = (
return { type: "instance", id, component, children };
};
-const createInstancePair = (
- id: Instance["id"],
- component: string,
- children: Instance["children"]
-): [Instance["id"], Instance] => {
- return [id, { type: "instance", id, component, children }];
-};
-
const createStyleDecl = (
styleSourceId: string,
breakpointId: string,
@@ -746,98 +737,145 @@ const getWebstudioDataStub = (
describe("delete instance", () => {
test("delete instance with its children", () => {
- // body
- // box1
- // box11
- // box2
- const instances = new Map([
- createInstancePair("body", "Body", [
- { type: "id", value: "box1" },
- { type: "id", value: "box2" },
- ]),
- createInstancePair("box1", "Box", [{ type: "id", value: "box11" }]),
- createInstancePair("box11", "Box", []),
- createInstancePair("box2", "Box", []),
- ]);
- $registeredComponentMetas.set(createFakeComponentMetas({}));
- const data = getWebstudioDataStub({ instances });
- deleteInstanceMutable(data, ["box1", "body"]);
- expect(data.instances).toEqual(
- new Map([
- createInstancePair("body", "Body", [{ type: "id", value: "box2" }]),
- createInstancePair("box2", "Box", []),
- ])
+ const data = renderData(
+ <$.Body ws:id="bodyId">
+ <$.Box ws:id="boxId">
+ <$.Box ws:id="sectionId">$.Box>
+ $.Box>
+ <$.Box ws:id="divId">$.Box>
+ $.Body>
);
+ expect(data.instances.size).toEqual(4);
+ expect(data.instances.get("bodyId")?.children.length).toEqual(2);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(["boxId", "bodyId"], data.instances)
+ );
+ expect(data.instances.size).toEqual(2);
+ expect(data.instances.get("bodyId")?.children.length).toEqual(1);
+ });
+
+ test("delete instance from collection", () => {
+ const data = renderData(
+ <$.Body ws:id="bodyId">
+
+ <$.Box ws:id="boxId">$.Box>
+
+ $.Body>
+ );
+ expect(data.instances.size).toEqual(3);
+ expect(data.instances.get("collectionId")?.children.length).toEqual(1);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(
+ ["boxId", "collectionId[0]", "collectionId", "bodyId"],
+ data.instances
+ )
+ );
+ expect(data.instances.size).toEqual(2);
+ expect(data.instances.get("collectionId")?.children.length).toEqual(0);
});
test("delete instance from collection item", () => {
- // body
- // list
- // box
- const instances = new Map([
- createInstancePair("body", "Body", [{ type: "id", value: "list" }]),
- createInstancePair("list", collectionComponent, [
- { type: "id", value: "box" },
- ]),
- createInstancePair("box", "Box", []),
- ]);
- $registeredComponentMetas.set(createFakeComponentMetas({}));
- const data = getWebstudioDataStub({ instances });
- deleteInstanceMutable(data, ["box", "list[0]", "list", "body"]);
- expect(data.instances).toEqual(
- new Map([
- createInstancePair("body", "Body", [{ type: "id", value: "list" }]),
- createInstancePair("list", collectionComponent, []),
- ])
+ const data = renderData(
+ <$.Body ws:id="bodyId">
+
+ <$.Box ws:id="boxId">
+ <$.Text ws:id="textId">$.Text>
+ $.Box>
+
+ $.Body>
);
+ expect(data.instances.size).toEqual(4);
+ expect(data.instances.get("boxId")?.children.length).toEqual(1);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(
+ ["textId", "boxId", "collectionId[0]", "collectionId", "bodyId"],
+ data.instances
+ )
+ );
+ expect(data.instances.size).toEqual(3);
+ expect(data.instances.get("boxId")?.children.length).toEqual(0);
});
- test("delete resource along with variable", () => {
- const instances = toMap([
- createInstance("body", "Body", [{ type: "id", value: "box" }]),
- createInstance("box", "Box", []),
- ]);
- const resources = toMap([
- {
- id: "resourceId",
- name: "My Resource",
- url: `""`,
- method: "get",
- headers: [],
- },
- ]);
- const dataSources = toMap([
- {
- id: "resourceVariableId",
- scopeInstanceId: "box",
- name: "My Resource Variable",
- type: "resource",
- resourceId: "resourceId",
- },
- ]);
- $registeredComponentMetas.set(createFakeComponentMetas({}));
+ test("delete resource bound to variable", () => {
+ const myResource = new ResourceValue("My Resource", {
+ url: expression`""`,
+ method: "get",
+ headers: [],
+ });
+ const data = renderData(
+ <$.Body ws:id="bodyId">
+ <$.Box ws:id="boxId" vars={expression`${myResource}`}>$.Box>
+ $.Body>
+ );
+ expect(data.resources.size).toEqual(1);
+ expect(data.dataSources.size).toEqual(1);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(["boxId", "bodyId"], data.instances)
+ );
+ expect(data.resources.size).toEqual(0);
+ expect(data.dataSources.size).toEqual(0);
+ });
- const data = getWebstudioDataStub({ instances, resources, dataSources });
- deleteInstanceMutable(data, ["box", "body"]);
+ test("delete resource bound to prop", () => {
+ const myResource = new ResourceValue("My Resource", {
+ url: expression`""`,
+ method: "get",
+ headers: [],
+ });
+ const data = renderData(
+ <$.Body ws:id="bodyId">
+ <$.Box ws:id="boxId" action={myResource}>$.Box>
+ $.Body>
+ );
+ expect(data.resources.size).toEqual(1);
+ expect(data.props.size).toEqual(1);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(["boxId", "bodyId"], data.instances)
+ );
+ expect(data.resources.size).toEqual(0);
+ expect(data.props.size).toEqual(0);
+ });
- expect(data.instances).toEqual(toMap([createInstance("body", "Body", [])]));
- expect(data.dataSources).toEqual(new Map());
- expect(data.resources).toEqual(new Map());
+ test("delete unknown instance (just in case)", () => {
+ const data = renderData(
+ <$.Body ws:id="bodyId">
+ <$.Invalid ws:id="invalidId">$.Invalid>
+ $.Body>
+ );
+ expect(data.instances.size).toEqual(2);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(["invalidId", "bodyId"], data.instances)
+ );
+ expect(data.instances.size).toEqual(1);
});
- test("delete instance without meta", () => {
- // body
- // invalid
- const instances = new Map([
- createInstancePair("body", "Body", [{ type: "id", value: "invalid" }]),
- createInstancePair("invalid", "Invalid", []),
- ]);
- $registeredComponentMetas.set(new Map());
- const data = getWebstudioDataStub({ instances });
- deleteInstanceMutable(data, ["invalid", "body"]);
- expect(data.instances).toEqual(
- new Map([createInstancePair("body", "Body", [])])
+ test("delete slot fragment along with last child", () => {
+ const data = renderData(
+ <$.Body ws:id="bodyId">
+ <$.Slot ws:id="slotId">
+ <$.Fragment ws:id="fragmentId">
+ <$.Box ws:id="boxId">$.Box>
+ $.Fragment>
+ $.Slot>
+ $.Body>
+ );
+ expect(data.instances.size).toEqual(4);
+ expect(data.instances.get("fragmentId")?.children.length).toEqual(1);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(
+ ["boxId", "fragmentId", "slotId", "bodyId"],
+ data.instances
+ )
);
+ expect(data.instances.size).toEqual(2);
+ expect(data.instances.get("slotId")?.children.length).toEqual(0);
});
});
diff --git a/apps/builder/app/shared/instance-utils.ts b/apps/builder/app/shared/instance-utils.ts
index da4e8be4ca15..766af1c5e53d 100644
--- a/apps/builder/app/shared/instance-utils.ts
+++ b/apps/builder/app/shared/instance-utils.ts
@@ -58,7 +58,13 @@ import { humanizeString } from "./string-utils";
import { serverSyncStore } from "./sync";
import { setDifference, setUnion } from "./shim";
import { breakCyclesMutable, findCycles } from "@webstudio-is/project-build";
-import { $awareness, $selectedPage, selectInstance } from "./awareness";
+import {
+ $awareness,
+ $selectedPage,
+ getInstancePath,
+ selectInstance,
+ type InstancePath,
+} from "./awareness";
import {
findClosestNonTextualContainer,
findClosestInstanceMatchingFragment,
@@ -364,7 +370,10 @@ export const reparentInstance = (
if (reparentDropTarget === undefined) {
return;
}
- deleteInstanceMutable(data, sourceInstanceSelector);
+ deleteInstanceMutable(
+ data,
+ getInstancePath(sourceInstanceSelector, data.instances)
+ );
const { newInstanceIds } = insertWebstudioFragmentCopy({
data,
fragment,
@@ -394,8 +403,8 @@ export const reparentInstance = (
};
export const deleteInstanceMutable = (
- data: WebstudioData,
- instanceSelector: InstanceSelector
+ data: Omit,
+ instancePath: InstancePath
) => {
const {
instances,
@@ -406,14 +415,11 @@ export const deleteInstanceMutable = (
dataSources,
resources,
} = data;
- let targetInstanceId = instanceSelector[0];
- const parentInstanceId = instanceSelector[1];
+ let targetInstance = instancePath[0].instance;
let parentInstance =
- parentInstanceId === undefined
- ? undefined
- : instances.get(parentInstanceId);
- const grandparentInstanceId = instanceSelector[2];
- const grandparentInstance = instances.get(grandparentInstanceId);
+ instancePath.length > 1 ? instancePath[1]?.instance : undefined;
+ const grandparentInstance =
+ instancePath.length > 2 ? instancePath[2]?.instance : undefined;
// delete parent fragment too if its last child is going to be deleted
// use case for slots: slot became empty and remove display: contents
@@ -423,18 +429,13 @@ export const deleteInstanceMutable = (
parentInstance.children.length === 1 &&
grandparentInstance
) {
- targetInstanceId = parentInstance.id;
- parentInstance = grandparentInstance;
- }
-
- // skip parent fake "item" instance and use grandparent collection as parent
- if (grandparentInstance?.component === collectionComponent) {
+ targetInstance = parentInstance;
parentInstance = grandparentInstance;
}
const instanceIds = findTreeInstanceIdsExcludingSlotDescendants(
instances,
- targetInstanceId
+ targetInstance.id
);
const localStyleSourceIds = findLocalStyleSourcesWithinInstances(
styleSources.values(),
@@ -446,7 +447,7 @@ export const deleteInstanceMutable = (
if (parentInstance) {
removeByMutable(
parentInstance.children,
- (child) => child.type === "id" && child.value === targetInstanceId
+ (child) => child.type === "id" && child.value === targetInstance.id
);
}
@@ -457,13 +458,13 @@ export const deleteInstanceMutable = (
for (const prop of props.values()) {
if (instanceIds.has(prop.instanceId)) {
props.delete(prop.id);
+ if (prop.type === "resource") {
+ resources.delete(prop.value);
+ }
}
}
for (const dataSource of dataSources.values()) {
- if (
- dataSource.scopeInstanceId !== undefined &&
- instanceIds.has(dataSource.scopeInstanceId)
- ) {
+ if (instanceIds.has(dataSource.scopeInstanceId)) {
dataSources.delete(dataSource.id);
if (dataSource.type === "resource") {
resources.delete(dataSource.resourceId);