Skip to content

Commit

Permalink
refactor: generalize insertable lookup (#4398)
Browse files Browse the repository at this point in the history
Here replaced a bunch boilerplaty code with more general
findClosestInsertable. This also reduced amount of explicit instance
selector calls. Prevented inserting list item into body. Refactored
tests with jsx.
  • Loading branch information
TrySound authored Nov 10, 2024
1 parent b2a844a commit bc0f6bb
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 436 deletions.
46 changes: 8 additions & 38 deletions apps/builder/app/builder/features/components/insert.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { ROOT_INSTANCE_ID } from "@webstudio-is/sdk";
import type { WsComponentMeta } from "@webstudio-is/react-sdk";
import { toast } from "@webstudio-is/design-system";
import { $registeredComponentMetas } from "~/shared/nano-states";
import {
$instances,
$registeredComponentMetas,
$selectedInstanceSelector,
} from "~/shared/nano-states";
import {
computeInstancesConstraints,
findClosestDroppableTarget,
findClosestInsertable,
getComponentTemplateData,
insertTemplateData,
} from "~/shared/instance-utils";
import { $selectedPage } from "~/shared/awareness";

const formatInsertionError = (component: string, meta: WsComponentMeta) => {
const or = new Intl.ListFormat("en", {
Expand All @@ -36,41 +29,18 @@ const formatInsertionError = (component: string, meta: WsComponentMeta) => {
// Insert a component depending on currently selected instance.
// Used for 1-click insert from the components panel.
export const insert = (component: string) => {
const selectedPage = $selectedPage.get();
if (selectedPage === undefined) {
return;
}
const templateData = getComponentTemplateData(component);
if (templateData === undefined) {
return;
}
const newInstances = new Map(
templateData.instances.map((instance) => [instance.id, instance])
);
const rootInstanceIds = templateData.children
.filter((child) => child.type === "id")
.map((child) => child.value);
const instanceSelector = $selectedInstanceSelector.get() ?? [
selectedPage.rootInstanceId,
];
if (instanceSelector[0] === ROOT_INSTANCE_ID) {
toast.error(`${component} cannot be added into Global Root`);
const fragment = getComponentTemplateData(component);
if (fragment === undefined) {
return;
}
const metas = $registeredComponentMetas.get();
const dropTarget = findClosestDroppableTarget(
metas,
$instances.get(),
// fallback to root as drop target
instanceSelector,
computeInstancesConstraints(metas, newInstances, rootInstanceIds)
);
if (dropTarget === undefined) {
const insertable = findClosestInsertable(fragment);
if (insertable === undefined) {
const metas = $registeredComponentMetas.get();
const meta = metas.get(component);
if (meta) {
toast.error(formatInsertionError(component, meta));
}
return;
}
insertTemplateData(templateData, dropTarget);
insertTemplateData(fragment, insertable);
};
45 changes: 8 additions & 37 deletions apps/builder/app/shared/copy-paste/plugin-embed-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@ import {
WsEmbedTemplate,
generateDataFromEmbedTemplate,
} from "@webstudio-is/react-sdk";
import {
$selectedInstanceSelector,
$instances,
$registeredComponentMetas,
$breakpoints,
} from "../nano-states";
import {
computeInstancesConstraints,
findClosestDroppableTarget,
insertTemplateData,
} from "../instance-utils";
import { $registeredComponentMetas, $breakpoints } from "../nano-states";
import { findClosestInsertable, insertTemplateData } from "../instance-utils";
import { isBaseBreakpoint } from "../breakpoints";
import { $selectedPage } from "~/shared/awareness";

const version = "@webstudio/template";

Expand All @@ -34,44 +24,25 @@ export const mimeType = "text/plain";

export const onPaste = (clipboardData: string) => {
const template = parse(clipboardData);

const selectedPage = $selectedPage.get();

if (template === undefined || selectedPage === undefined) {
if (template === undefined) {
return false;
}

// paste to the root if nothing is selected
const instanceSelector = $selectedInstanceSelector.get() ?? [
selectedPage.rootInstanceId,
];
const metas = $registeredComponentMetas.get();
const breakpoints = $breakpoints.get();
const breakpointValues = Array.from(breakpoints.values());
const baseBreakpoint = breakpointValues.find(isBaseBreakpoint);
if (baseBreakpoint === undefined) {
return false;
}
const metas = $registeredComponentMetas.get();
const templateData = generateDataFromEmbedTemplate(
const fragment = generateDataFromEmbedTemplate(
template,
metas,
baseBreakpoint.id
);
const newInstances = new Map(
templateData.instances.map((instance) => [instance.id, instance])
);
const rootInstanceIds = templateData.children
.filter((child) => child.type === "id")
.map((child) => child.value);
const dropTarget = findClosestDroppableTarget(
metas,
$instances.get(),
instanceSelector,
computeInstancesConstraints(metas, newInstances, rootInstanceIds)
);
if (dropTarget === undefined) {
const insertable = findClosestInsertable(fragment);
if (insertable === undefined) {
return false;
}
insertTemplateData(templateData, dropTarget);
insertTemplateData(fragment, insertable);
return true;
};
63 changes: 20 additions & 43 deletions apps/builder/app/shared/copy-paste/plugin-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,19 @@ import {
WebstudioFragment,
findTreeInstanceIdsExcludingSlotDescendants,
} from "@webstudio-is/sdk";
import {
$selectedInstanceSelector,
$project,
$registeredComponentMetas,
$instances,
} from "../nano-states";
import { $selectedInstanceSelector, $instances } from "../nano-states";
import type { InstanceSelector, DroppableTarget } from "../tree-utils";
import {
computeInstancesConstraints,
deleteInstanceMutable,
findAvailableDataSources,
findClosestDroppableTarget,
extractWebstudioFragment,
insertWebstudioFragmentCopy,
isInstanceDetachable,
updateWebstudioData,
getWebstudioData,
insertInstanceChildrenMutable,
findClosestInsertable,
} from "../instance-utils";
import { $selectedPage } from "~/shared/awareness";

const version = "@webstudio/instance/v0.1";

Expand Down Expand Up @@ -92,14 +85,16 @@ export const getPortalFragmentSelector = (
return [instance.children[0].value, ...instanceSelector];
};

const getPasteTarget = (
data: InstanceData,
instanceSelector: InstanceSelector
): undefined | DroppableTarget => {
const findPasteTarget = (data: InstanceData): undefined | DroppableTarget => {
const instances = $instances.get();

const instanceSelector = $selectedInstanceSelector.get();

// paste after selected instance
if (shallowEqual(instanceSelector, data.instanceSelector)) {
if (
instanceSelector &&
shallowEqual(instanceSelector, data.instanceSelector)
) {
// body is not allowed to copy
// so clipboard always have at least two level instance selector
const [currentInstanceId, parentInstanceId] = instanceSelector;
Expand All @@ -116,22 +111,15 @@ const getPasteTarget = (
};
}

const insertable = findClosestInsertable(data);
if (insertable === undefined) {
return;
}

const newInstances: Instances = new Map();
for (const instance of data.instances) {
newInstances.set(instance.id, instance);
}
const metas = $registeredComponentMetas.get();
const rootInstanceId = data.instances[0].id;
const pasteTarget = findClosestDroppableTarget(
metas,
instances,
instanceSelector,
computeInstancesConstraints(metas, newInstances, [rootInstanceId])
);
if (pasteTarget === undefined) {
return;
}

const newInstanceIds = findTreeInstanceIdsExcludingSlotDescendants(
newInstances,
data.instances[0].id
Expand All @@ -151,36 +139,25 @@ const getPasteTarget = (
const dropTargetSelector =
// consider portal fragment when check for cycles to avoid cases
// like pasting portal directly into portal
getPortalFragmentSelector(instances, pasteTarget.parentSelector) ??
pasteTarget.parentSelector;
getPortalFragmentSelector(instances, insertable.parentSelector) ??
insertable.parentSelector;
for (const instanceId of dropTargetSelector) {
if (preservedChildIds.has(instanceId)) {
return;
}
}

return pasteTarget;
return insertable;
};

export const onPaste = (clipboardData: string) => {
const fragment = parse(clipboardData);

const selectedPage = $selectedPage.get();
const project = $project.get();

if (
fragment === undefined ||
selectedPage === undefined ||
project === undefined
) {
if (fragment === undefined) {
return false;
}

// paste to the root if nothing is selected
const instanceSelector = $selectedInstanceSelector.get() ?? [
selectedPage.rootInstanceId,
];
const pasteTarget = getPasteTarget(fragment, instanceSelector);
const pasteTarget = findPasteTarget(fragment);
if (pasteTarget === undefined) {
return false;
}
Expand All @@ -192,7 +169,7 @@ export const onPaste = (clipboardData: string) => {
availableDataSources: findAvailableDataSources(
data.dataSources,
data.instances,
instanceSelector
pasteTarget.parentSelector
),
});
const newRootInstanceId = newInstanceIds.get(fragment.instances[0].id);
Expand Down
46 changes: 8 additions & 38 deletions apps/builder/app/shared/copy-paste/plugin-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,10 @@ import type {
Instance,
WebstudioFragment,
} from "@webstudio-is/sdk";
import {
computeInstancesConstraints,
findClosestDroppableTarget,
insertTemplateData,
} from "../instance-utils";
import {
$breakpoints,
$instances,
$registeredComponentMetas,
$selectedInstanceSelector,
} from "../nano-states";
import { findClosestInsertable, insertTemplateData } from "../instance-utils";
import { $breakpoints } from "../nano-states";
import { isBaseBreakpoint } from "../breakpoints";
import { denormalizeSrcProps } from "./asset-upload";
import { $selectedPage } from "~/shared/awareness";

const micromarkOptions = {
extensions: [gfm()],
Expand Down Expand Up @@ -221,36 +211,16 @@ const parse = (clipboardData: string, options?: Options) => {
};

export const onPaste = async (clipboardData: string) => {
let data = parse(clipboardData);

const selectedPage = $selectedPage.get();
if (data === undefined || selectedPage === undefined) {
let fragment = parse(clipboardData);
if (fragment === undefined) {
return false;
}

data = await denormalizeSrcProps(data);

const metas = $registeredComponentMetas.get();
const newInstances = new Map(
data.instances.map((instance) => [instance.id, instance])
);
const rootInstanceIds = data.children
.filter((child) => child.type === "id")
.map((child) => child.value);
// paste to the root if nothing is selected
const instanceSelector = $selectedInstanceSelector.get() ?? [
selectedPage.rootInstanceId,
];
const dropTarget = findClosestDroppableTarget(
metas,
$instances.get(),
instanceSelector,
computeInstancesConstraints(metas, newInstances, rootInstanceIds)
);
if (dropTarget === undefined) {
fragment = await denormalizeSrcProps(fragment);
const insertable = findClosestInsertable(fragment);
if (insertable === undefined) {
return false;
}
insertTemplateData(data, dropTarget);
insertTemplateData(fragment, insertable);
return true;
};

Expand Down
Loading

0 comments on commit bc0f6bb

Please sign in to comment.