From a3f4a1b8a85d0bede2c70d97e00272f3f4e9bf6f Mon Sep 17 00:00:00 2001 From: Felipe Mota Date: Thu, 27 Jun 2024 00:01:42 -0300 Subject: [PATCH] fix(figma-importer): Properly handle exposed instances props Issue: https://linear.app/plasmic/issue/PLA-10940 Change-Id: Ib760117a3c3615eae76bfd869cb2a3ce8c45a14c GitOrigin-RevId: f4499e93587c2fc7db2175bb2e81ead7065d082c --- .../wab/client/figma-importer/props.spec.ts | 221 ++++++++++++++++++ .../src/wab/client/figma-importer/props.ts | 31 ++- 2 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 platform/wab/src/wab/client/figma-importer/props.spec.ts diff --git a/platform/wab/src/wab/client/figma-importer/props.spec.ts b/platform/wab/src/wab/client/figma-importer/props.spec.ts new file mode 100644 index 00000000000..ef325047d1c --- /dev/null +++ b/platform/wab/src/wab/client/figma-importer/props.spec.ts @@ -0,0 +1,221 @@ +import { InstanceNode } from "@/wab/client/figma-importer/plugin-types"; +import { fromFigmaComponentToTplProps } from "@/wab/client/figma-importer/props"; +import { fakeStudioCtx } from "@/wab/client/test/fake-init-ctx"; +import { mkComponentVariantGroup, mkVariant } from "@/wab/shared/Variants"; +import { hackyCast } from "@/wab/shared/common"; +import { ComponentType, mkComponent } from "@/wab/shared/core/components"; +import { ParamExportType, mkParam, mkVar } from "@/wab/shared/core/lang"; +import { mkTplTagX } from "@/wab/shared/core/tpls"; +import { StateParam } from "@/wab/shared/model/classes"; +import { typeFactory } from "@/wab/shared/model/model-util"; + +function createFigmaTestData(getCodeComponentMeta: jest.FunctionLike) { + const child = { + type: "INSTANCE", + name: "ButtonSwap", + componentPropertyReferences: { + mainComponent: "swapChild", + }, + children: [], + }; + + const node: Partial = { + componentProperties: { + "Error message#1": { + type: "TEXT", + value: "ERROR_MESSAGE_1", + }, + "Filled value#2": { + type: "TEXT", + value: "FILLED_VALUE_2", + }, + "value#3": { + type: "TEXT", + value: "VALUE_3", + }, + "isDisabled#4": { + type: "BOOLEAN", + value: "false", + }, + color: { + type: "VARIANT", + value: "primary", + }, + swapChild: { + type: "INSTANCE_SWAP", + value: "FIGMA_INTERNAL_ID1", + }, + slotValue: { + type: "TEXT", + value: "SLOT_VALUE", + }, + Type: { + type: "VARIANT", + value: "ghost", + }, + }, + exposedInstances: [ + hackyCast({ + name: "exposedInst", + type: "INSTANCE", + children: [], + componentProperties: { + "Exposed prop 1#12:34": { + type: "TEXT", + value: "exposedProp1", + }, + }, + }), + ], + type: "INSTANCE", + children: [ + // @ts-expect-error - child is not a full InstanceNode + child, + ], + mainComponent: { + id: "FIGMA_INTERNAL_ID2", + name: "Button", + }, + parent: null, + }; + + const TypeVariantParam = { + variable: mkVar("type"), + } as StateParam; + + const component = mkComponent({ + name: "Button", + type: ComponentType.Code, + params: [ + mkParam({ + // We should identify that this prop matches "Error message#1" + name: "errorMessage", + type: typeFactory.text(), + exportType: ParamExportType.External, + paramType: "prop", + }), + mkParam({ + name: "filledValue", + type: typeFactory.text(), + exportType: ParamExportType.External, + paramType: "prop", + }), + mkParam({ + name: "value", + type: typeFactory.text(), + exportType: ParamExportType.External, + paramType: "prop", + }), + mkParam({ + name: "isDisabled", + type: typeFactory.bool(), + exportType: ParamExportType.External, + paramType: "prop", + }), + mkParam({ + name: "color", + type: typeFactory.text(), + exportType: ParamExportType.External, + paramType: "prop", + }), + mkParam({ + name: "secondaryColor", + type: typeFactory.text(), + exportType: ParamExportType.External, + paramType: "prop", + }), + mkParam({ + name: "slotValue", + type: typeFactory.renderable(), + exportType: ParamExportType.External, + paramType: "slot", + }), + TypeVariantParam, + ], + variantGroups: [ + mkComponentVariantGroup({ + // The param is neglible for this test + param: TypeVariantParam, + multi: false, + variants: ["primary", "secondary", "ghost"].map((type) => { + return mkVariant({ + name: `btn-${type}`, + }); + }), + }), + ], + tplTree: mkTplTagX("div"), + }); + + const { studioCtx } = fakeStudioCtx(); + // @ts-expect-error - assign fake function to get code component meta + studioCtx.getCodeComponentMeta = getCodeComponentMeta; + + return { + studioCtx, + node, + component, + }; +} + +describe("Figma importer slot handling", () => { + describe("fromFigmaComponentToTplProps", () => { + it("should directly map props if no transform function is provided", () => { + const getCodeComponentMeta = jest.fn().mockReturnValue({}); + const { studioCtx, node, component } = + createFigmaTestData(getCodeComponentMeta); + expect( + fromFigmaComponentToTplProps(studioCtx, component, node as InstanceNode) + ).toEqual([ + ["errorMessage", "ERROR_MESSAGE_1"], + ["filledValue", "FILLED_VALUE_2"], + ["value", "VALUE_3"], + ["color", "primary"], + ]); + expect(getCodeComponentMeta).toHaveBeenCalledWith(component); + }); + + it("should call transform function if provided", () => { + const figmaPropsTransform = jest.fn().mockImplementation((props) => { + return { + ...props, + secondaryColor: `derived-${props.color}`, + type: `btn-${props.Type}`, + }; + }); + + const getCodeComponentMeta = jest.fn().mockReturnValue({ + figmaPropsTransform, + }); + const { studioCtx, node, component } = + createFigmaTestData(getCodeComponentMeta); + expect( + fromFigmaComponentToTplProps(studioCtx, component, node as InstanceNode) + ).toEqual([ + ["errorMessage", "ERROR_MESSAGE_1"], + ["filledValue", "FILLED_VALUE_2"], + ["value", "VALUE_3"], + ["color", "primary"], + ["secondaryColor", "derived-primary"], + [ + "type", + expect.objectContaining({ + variants: [component.variantGroups[0].variants[2]], + }), + ], + ]); + expect(getCodeComponentMeta).toHaveBeenCalledWith(component); + expect(figmaPropsTransform).toHaveBeenCalledWith({ + // All props expect for ones that match slots should be here + "Error message": "ERROR_MESSAGE_1", + "Filled value": "FILLED_VALUE_2", + value: "VALUE_3", + isDisabled: false, + color: "primary", + swapChild: "ButtonSwap", + Type: "ghost", + "Exposed prop 1": "exposedProp1", + }); + }); + }); +}); diff --git a/platform/wab/src/wab/client/figma-importer/props.ts b/platform/wab/src/wab/client/figma-importer/props.ts index fd5e8d22d5d..b20d8324f60 100644 --- a/platform/wab/src/wab/client/figma-importer/props.ts +++ b/platform/wab/src/wab/client/figma-importer/props.ts @@ -5,11 +5,14 @@ import { InstanceNode, } from "@/wab/client/figma-importer/plugin-types"; import { StudioCtx } from "@/wab/client/studio-ctx/StudioCtx"; -import { hackyCast, isJsonScalar, withoutNils } from "@/wab/shared/common"; -import { getParamByVarName, isCodeComponent } from "@/wab/shared/core/components"; import { isSlot } from "@/wab/shared/SlotUtils"; import { isStandaloneVariantGroup } from "@/wab/shared/Variants"; import { toVarName } from "@/wab/shared/codegen/util"; +import { isJsonScalar, withoutNils } from "@/wab/shared/common"; +import { + getParamByVarName, + isCodeComponent, +} from "@/wab/shared/core/components"; import { Component, VariantsRef } from "@/wab/shared/model/classes"; import { isBoolType, isNumType } from "@/wab/shared/model/model-util"; import { notification } from "antd"; @@ -37,7 +40,8 @@ function getChildComponentNameFromPropertyKey( function fixComponentFigmaPropKey(key: string, prop: ComponentProperty) { // Fix property name, removing the suffix that figma adds to text and boolean properties - if (prop.type === "TEXT" || prop.type === "BOOLEAN") { + if ((prop.type === "TEXT" || prop.type === "BOOLEAN") && key.includes("#")) { + // We only run to remove if it does have a "#" in the key return key.substring(0, key.lastIndexOf("#")); } return key; @@ -122,7 +126,15 @@ function fromFigmaNodeToFigmaProps( ): ComponentPropertiesEntries { const localProps: ComponentPropertiesEntries = Object.entries( inst.componentProperties ?? {} - ); + ).map(([key, prop]) => { + // We fix directly in the source, since running the fix functions twice can cause issues + // For example. If we have a key "text#other#id" and we run the fixComponentFigmaPropKey function + // twice, it will transform it to "text", which is not what we want + return [ + fixComponentFigmaPropKey(key, prop), + fixComponentFigmaPropValue(key, prop, descendants), + ]; + }); const exposedInstances: InstanceNode[] = inst.exposedInstances ?? []; @@ -144,12 +156,7 @@ function fromFigmaNodeToFigmaProps( // to the component in Plasmic, so that the user can transform them includePropsWithoutParam: true, } - ).map(([key, prop]) => { - return [ - fixComponentFigmaPropKey(key, prop), - fixComponentFigmaPropValue(key, prop, descendants), - ]; - }); + ); } function getAllDescendants(inst: InstanceNode): InstanceNode[] { @@ -255,10 +262,10 @@ function maybeTransformFigmaProps( if (isCodeComponent(component)) { const meta = studioCtx.getCodeComponentMeta(component); - if (meta && hackyCast(meta).figmaPropsTransform) { + if (meta && meta.figmaPropsTransform) { const transformResult = safeTransformFigmaProps( component, - hackyCast(meta).figmaPropsTransform, + meta.figmaPropsTransform, componentProps );