Files
webstudio/apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.ts
T
Ivan StarkovandOleg Isonen 6e41286635 experimental: Add Desktop Webflow Navbar support (#3665)
## Description

This 
<img width="856" alt="image"
src="https://github.com/webstudio-is/webstudio/assets/5077042/f8894380-0dbb-45c7-a1a5-410dbcb01fc8">

To this
<img width="1049" alt="image"
src="https://github.com/webstudio-is/webstudio/assets/5077042/bf618095-08be-4c6f-850f-56354e74e738">

## Known issues:

### All
- w-nav-link preset colors do not match

### Mobile
Mobile burger is not shown 
- w-nav-button does not have breakpoint style i.e. display: none->block
for mobile
- w-nav-menu does not have breakpoint related style i.e display: block
-> none for mobile
- we don't have widget icons like burger i.e. `"data": { "widget": {
"type": "icon", "icon": "nav-menu" } }` _having breakpoint issues and
that user in any case need to use Sheet on mobile this is ok_



## Steps for reproduction

Copypaste Navbar from webflow 2 webstudio

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file

---------

Co-authored-by: Oleg Isonen <oleg008@gmail.com>
2024-07-10 18:36:01 +03:00

245 lines
6.3 KiB
TypeScript

import type { Instance, WebstudioFragment } from "@webstudio-is/sdk";
import {
computeInstancesConstraints,
findAvailableDataSources,
findClosestDroppableTarget,
insertInstanceChildrenMutable,
insertWebstudioFragmentCopy,
updateWebstudioData,
} from "../../instance-utils";
import {
$instances,
$registeredComponentMetas,
$selectedInstanceSelector,
$selectedPage,
$project,
} from "../../nano-states";
import {
WfData,
wfNodeTypes,
type WfNode,
type WfStyle,
type WfAsset,
} from "./schema";
import { addInstanceAndProperties } from "./instances-properties";
import { addStyles } from "./styles";
import { builderApi } from "~/shared/builder-api";
import { denormalizeSrcProps } from "../asset-upload";
import { nanoHash } from "~/shared/nano-hash";
const { toast } = builderApi;
export const mimeType = "application/json";
const toWebstudioFragment = async (wfData: WfData) => {
const fragment: WebstudioFragment = {
children: [],
instances: [],
props: [],
breakpoints: [],
styles: [],
styleSources: [],
styleSourceSelections: [],
dataSources: [],
resources: [],
assets: [],
};
const wfNodes = new Map<WfNode["_id"], WfNode>();
for (const node of wfData.payload.nodes) {
if ("type" in node || "text" in node) {
wfNodes.set(node._id, node);
}
}
const wfStyles = new Map<WfStyle["_id"], WfStyle>(
wfData.payload.styles.map((style: WfStyle) => [style._id, style])
);
const wfAssets = new Map<WfAsset["_id"], WfAsset>(
wfData.payload.assets.map((asset: WfAsset) => [asset._id, asset])
);
// False value used to skip a node.
const doneNodes = new Map<WfNode["_id"], Instance["id"] | false>();
for (const wfNode of wfNodes.values()) {
addInstanceAndProperties(wfNode, doneNodes, wfNodes, fragment);
}
/**
* Generates deterministic style IDs based on sourceId or unique data.
* This simplifies merging and deduplicating styles from different sources.
*/
const generateStyleSourceId = async (sourceData: string) => {
// We are using projectId here to avoid id collisions between different projects.
const projectId = $project.get()?.id;
if (projectId === undefined) {
throw new Error("Project id is not set");
}
return nanoHash(`${projectId}-${sourceData}`);
};
await addStyles({
wfNodes,
wfStyles,
wfAssets,
doneNodes,
fragment,
generateStyleSourceId,
});
// First node should be always the root node in theory, if not
// we need to find a node that is not a child of any other node.
const rootWfNode = wfData.payload.nodes[0];
const rootInstanceId = doneNodes.get(rootWfNode._id);
if (rootInstanceId === false) {
return fragment;
}
if (rootInstanceId === undefined) {
console.error(`No root instance id found for node ${rootWfNode._id}`);
return fragment;
}
fragment.children = [
{
type: "id",
value: rootInstanceId,
},
];
return fragment;
};
const parse = (clipboardData: string) => {
let data;
try {
data = JSON.parse(clipboardData);
} catch {
return;
}
if (data.type !== "@webflow/XscpData") {
return;
}
const unsupportedNodeTypes: Set<string> = new Set(
data.payload.nodes
.filter((node: { type: string }) => {
return (
node.type !== undefined &&
wfNodeTypes.includes(node.type as (typeof wfNodeTypes)[number]) ===
false
);
})
.map((node: { type: string }) => node.type)
);
if (unsupportedNodeTypes.size !== 0) {
const message = `Skipping unsupported elements: ${[...unsupportedNodeTypes.values()].join(", ")}`;
toast.info(message);
console.info(message);
}
const result = WfData.safeParse(data);
if (result.success) {
const unpasedTypes = new Set<string>();
for (let i = 0; i !== result.data.payload.nodes.length; ++i) {
if ("type" in result.data.payload.nodes[i]) {
continue;
}
if (data.payload.nodes[i].type === undefined) {
continue;
}
const probablyUnparsedType = data.payload.nodes[i].type;
if (unsupportedNodeTypes.has(probablyUnparsedType)) {
continue;
}
unpasedTypes.add(probablyUnparsedType);
}
if (unpasedTypes.size !== 0) {
const message = `The following types were skipped due to a parsing error: ${[...unpasedTypes.values()].join(", ")}`;
toast.info(message);
console.info(message);
}
return result.data;
}
toast.error(result.error.message);
console.error(result.error.message);
};
export const onPaste = async (clipboardData: string) => {
const wfData = parse(clipboardData);
if (wfData === undefined) {
return false;
}
let fragment = await toWebstudioFragment(wfData);
const selectedPage = $selectedPage.get();
if (fragment === undefined || selectedPage === undefined) {
return false;
}
fragment = await denormalizeSrcProps(fragment);
const metas = $registeredComponentMetas.get();
const newInstances = new Map(
fragment.instances.map((instance) => [instance.id, instance])
);
// paste to the root if nothing is selected
const instanceSelector = $selectedInstanceSelector.get() ?? [
selectedPage.rootInstanceId,
];
const rootInstanceIds = fragment.children
.filter((child) => child.type === "id")
.map((child) => child.value);
const dropTarget = findClosestDroppableTarget(
metas,
$instances.get(),
instanceSelector,
computeInstancesConstraints(metas, newInstances, rootInstanceIds)
);
if (dropTarget === undefined) {
return false;
}
updateWebstudioData((data) => {
const { newInstanceIds } = insertWebstudioFragmentCopy({
data,
fragment,
availableDataSources: findAvailableDataSources(
data.dataSources,
data.instances,
instanceSelector
),
});
const children = fragment.children
.map((child) => {
if (child.type === "id") {
const value = newInstanceIds.get(child.value);
if (value) {
return { type: "id" as const, value };
}
}
})
.filter(<T>(value: T): value is NonNullable<T> => value !== undefined);
insertInstanceChildrenMutable(data, children, dropTarget);
});
return true;
};
export const __testing__ = {
toWebstudioFragment,
};