From 028cd799dbfc321ff75235369e0bb22b167c8299 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Sat, 1 Jun 2024 01:17:39 +0100 Subject: [PATCH] experimental: Paste from Webflow 3 (#3466) ## Description More components: Richt Text, Supescript, Subscript, Emphasized, Strong, Section, Container, Quick Stack, VStack, HStack ## Steps for reproduction 1. click button 2. expect xyz ## 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.example` and the `builder/env-check.js` if mandatory --- .../shared/copy-paste/plugin-webflow.test.ts | 356 +++++++++++++++++- .../app/shared/copy-paste/plugin-webflow.ts | 85 ++++- 2 files changed, 430 insertions(+), 11 deletions(-) diff --git a/apps/builder/app/shared/copy-paste/plugin-webflow.test.ts b/apps/builder/app/shared/copy-paste/plugin-webflow.test.ts index 25dd30c3ad..8ef96e2cf4 100644 --- a/apps/builder/app/shared/copy-paste/plugin-webflow.test.ts +++ b/apps/builder/app/shared/copy-paste/plugin-webflow.test.ts @@ -57,7 +57,7 @@ test("Heading", () => { ]); }); -test("Link", () => { +test("Link Block, Button, Text Link", () => { const fragment = toWebstudioFragment({ type: "@webflow/XscpData", payload: { @@ -341,6 +341,360 @@ test("Blockquote", () => { ]); }); +test("Strong", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "Strong", + tag: "strong", + classes: [], + children: ["25ffefdf-c015-5edd-7673-933b41a25329"], + }, + { + _id: "25ffefdf-c015-5edd-7673-933b41a25329", + text: true, + v: "Bold Text", + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Bold", + children: [ + { + type: "text", + value: "Bold Text", + }, + ], + }, + ]); +}); + +test("Emphasized", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "Emphasized", + tag: "em", + classes: [], + children: ["25ffefdf-c015-5edd-7673-933b41a25329"], + }, + { + _id: "25ffefdf-c015-5edd-7673-933b41a25329", + text: true, + v: "Emphasis", + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Italic", + children: [ + { + type: "text", + value: "Emphasis", + }, + ], + }, + ]); +}); + +test("Superscript", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "Superscript", + tag: "sup", + classes: [], + children: ["25ffefdf-c015-5edd-7673-933b41a25329"], + }, + { + _id: "25ffefdf-c015-5edd-7673-933b41a25329", + text: true, + v: "Superscript", + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Superscript", + children: [ + { + type: "text", + value: "Superscript", + }, + ], + }, + ]); +}); + +test("Subscript", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "Subscript", + tag: "sub", + classes: [], + children: ["25ffefdf-c015-5edd-7673-933b41a25329"], + }, + { + _id: "25ffefdf-c015-5edd-7673-933b41a25329", + text: true, + v: "Subscript", + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Subscript", + children: [ + { + type: "text", + value: "Subscript", + }, + ], + }, + ]); +}); + +test("Section", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "Section", + tag: "section", + classes: [], + children: ["25ffefdf-c015-5edd-7673-933b41a25329"], + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [], + }, + ]); +}); + +test("BlockContainer", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "BlockContainer", + tag: "div", + classes: [], + children: [], + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [], + }, + ]); +}); + +test("Block", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "Block", + tag: "div", + classes: [], + children: [], + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [], + }, + ]); +}); + +test("V Flex", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "VFlex", + tag: "div", + classes: [], + children: [], + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [], + }, + ]); +}); + +test("H Flex", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "25ffefdf-c015-5edd-7673-933b41a25328", + type: "HFlex", + tag: "div", + classes: [], + children: [], + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [], + }, + ]); +}); + +test("Quick Stack", () => { + const fragment = toWebstudioFragment({ + type: "@webflow/XscpData", + payload: { + nodes: [ + { + _id: "91782272-bf55-194d-ce85-9ddc69c51dee", + type: "Layout", + tag: "div", + classes: [], + children: [ + "91782272-bf55-194d-ce85-9ddc69c51def", + "91782272-bf55-194d-ce85-9ddc69c51df0", + ], + data: { + style: { + base: { + main: { + noPseudo: { + gridTemplateColumns: "1fr 1fr", + gridTemplateRows: "auto", + }, + }, + }, + }, + }, + }, + { + _id: "91782272-bf55-194d-ce85-9ddc69c51def", + type: "Cell", + tag: "div", + classes: [], + children: [], + }, + { + _id: "91782272-bf55-194d-ce85-9ddc69c51df0", + type: "Cell", + tag: "div", + classes: [], + children: [], + }, + ], + styles: [], + }, + }); + + expect(fragment.instances).toEqual([ + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [], + }, + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [], + }, + { + id: expect.not.stringMatching("instanceId"), + type: "instance", + component: "Box", + children: [ + { type: "id", value: expect.not.stringMatching("instanceId") }, + { type: "id", value: expect.not.stringMatching("instanceId") }, + ], + }, + ]); +}); + test("Basic styles with a class", () => { const fragment = toWebstudioFragment({ type: "@webflow/XscpData", diff --git a/apps/builder/app/shared/copy-paste/plugin-webflow.ts b/apps/builder/app/shared/copy-paste/plugin-webflow.ts index 01bf8580ca..6a0645fe63 100644 --- a/apps/builder/app/shared/copy-paste/plugin-webflow.ts +++ b/apps/builder/app/shared/copy-paste/plugin-webflow.ts @@ -21,11 +21,65 @@ export const mimeType = "application/json"; // A list of Webflow component names that need to be mapped. const componentMappers = { - Block(wfNode: WfNode) { - if ("type" in wfNode === false || wfNode.type !== "Block") { - return; + Block(wfNode: WfElementNode) { + if (wfNode.type === "Block") { + return wfNode.data?.text ? "Text" : "Box"; } - return wfNode.data.text ? "Text" : "Box"; + return wfNode.type; + }, + Section(wfNode: WfElementNode) { + if (wfNode.type === "Section") { + return "Box"; + } + return wfNode.type; + }, + RichText(wfNode: WfElementNode) { + if (wfNode.type === "RichText") { + return "Box"; + } + return wfNode.type; + }, + Strong(wfNode: WfElementNode) { + if (wfNode.type === "Strong") { + return "Bold"; + } + return wfNode.type; + }, + Emphasized(wfNode: WfElementNode) { + if (wfNode.type === "Emphasized") { + return "Italic"; + } + return wfNode.type; + }, + BlockContainer(wfNode: WfElementNode) { + if (wfNode.type === "BlockContainer") { + return "Box"; + } + return wfNode.type; + }, + Layout(wfNode: WfElementNode) { + if (wfNode.type === "Layout") { + return "Box"; + } + return wfNode.type; + }, + Cell(wfNode: WfElementNode) { + if (wfNode.type === "Cell") { + return "Box"; + } + return wfNode.type; + }, + VFlex(wfNode: WfElementNode) { + if (wfNode.type === "VFlex") { + return "Box"; + } + return wfNode.type; + }, + HFlex(wfNode: WfElementNode) { + if (wfNode.type === "HFlex") { + return "Box"; + } + return wfNode.type; }, }; @@ -43,11 +97,11 @@ const WfTextNode = z.object({ text: z.boolean(), }); -const WfNode = z.union([ +const WfElementNode = z.union([ WfBaseNode.extend({ type: z.enum(["Heading"]) }), WfBaseNode.extend({ type: z.enum(["Block"]), - data: z.object({ text: z.boolean().optional() }), + data: z.object({ text: z.boolean().optional() }).optional(), }), WfBaseNode.extend({ type: z.enum(["List"]) }), WfBaseNode.extend({ type: z.enum(["ListItem"]) }), @@ -62,8 +116,21 @@ const WfNode = z.union([ }), WfBaseNode.extend({ type: z.enum(["Paragraph"]) }), WfBaseNode.extend({ type: z.enum(["Blockquote"]) }), - WfTextNode, + WfBaseNode.extend({ type: z.enum(["RichText"]) }), + WfBaseNode.extend({ type: z.enum(["Strong"]) }), + WfBaseNode.extend({ type: z.enum(["Emphasized"]) }), + WfBaseNode.extend({ type: z.enum(["Superscript"]) }), + WfBaseNode.extend({ type: z.enum(["Subscript"]) }), + WfBaseNode.extend({ type: z.enum(["Section"]) }), + WfBaseNode.extend({ type: z.enum(["BlockContainer"]) }), + WfBaseNode.extend({ type: z.enum(["Layout"]) }), + WfBaseNode.extend({ type: z.enum(["Cell"]) }), + WfBaseNode.extend({ type: z.enum(["VFlex"]) }), + WfBaseNode.extend({ type: z.enum(["HFlex"]) }), ]); +type WfElementNode = z.infer; + +const WfNode = z.union([WfElementNode, WfTextNode]); type WfNode = z.infer; const WfStyle = z.object({ @@ -181,9 +248,7 @@ const addInstance = ( const component = wfNode.type in componentMappers - ? componentMappers[wfNode.type as keyof typeof componentMappers]( - wfNode - ) ?? wfNode.type + ? componentMappers[wfNode.type as keyof typeof componentMappers](wfNode) : wfNode.type; fragment.instances.push({