From f6a82b895dd6b808e0ac2f442394d1e3b93b54f0 Mon Sep 17 00:00:00 2001 From: sarah Date: Mon, 16 Dec 2024 17:43:01 +0500 Subject: [PATCH] fix(wab): Force install new copy of a template component Change-Id: I08183511fe716655b01381989830a76e5d7eecf2 GitOrigin-RevId: 261ff399b13e8cb019755cdcc27a19de53213df2 --- platform/wab/src/wab/client/Dnd.tsx | 11 +- .../wab/client/ProjectDependencyManager.ts | 7 +- .../client/components/canvas/FreestyleBox.tsx | 6 +- .../wab/client/components/canvas/view-ops.tsx | 8 +- .../components/insert-panel/InsertPanel.tsx | 353 ++++++++++-------- .../ProjectPanel/NavigationDropdown.tsx | 4 +- .../studio/add-drawer/AddDrawer.tsx | 172 +++++---- .../components/widgets/NewComponentModal.tsx | 3 +- .../wab/client/definitions/insertables.tsx | 25 +- .../src/wab/client/insertable-templates.ts | 20 +- .../src/wab/client/studio-ctx/StudioCtx.tsx | 10 +- .../src/wab/shared/insertable-templates.ts | 9 +- .../component-importer.ts | 42 ++- .../wab/shared/insertable-templates/types.ts | 4 + 14 files changed, 389 insertions(+), 285 deletions(-) diff --git a/platform/wab/src/wab/client/Dnd.tsx b/platform/wab/src/wab/client/Dnd.tsx index 4083d7767b1..ae15622b8cb 100644 --- a/platform/wab/src/wab/client/Dnd.tsx +++ b/platform/wab/src/wab/client/Dnd.tsx @@ -82,6 +82,7 @@ import { Side, sideToOrient, } from "@/wab/shared/geom"; +import { CloneOpts } from "@/wab/shared/insertable-templates/types"; import { ContainerLayoutType, ContainerType, @@ -785,11 +786,15 @@ export class DragInsertManager { public static async build( studioCtx: StudioCtx, - spec: AddTplItem + spec: AddTplItem, + opts?: CloneOpts ): Promise { const targeters: NodeTargeter[] = []; const extraInfo = spec.asyncExtraInfo - ? await spec.asyncExtraInfo(studioCtx, { isDragging: true }) + ? await spec.asyncExtraInfo(studioCtx, { + isDragging: true, + ...(opts ?? {}), + }) : undefined; for (const vc of studioCtx.viewCtxs) { // Ignore ViewCtx whose root is invisible. @@ -839,7 +844,7 @@ export class DragInsertManager { this.tentativeInsertion && this.tentativeInsertion.type !== "ErrorInsertion" ) { - const tpl = spec?.factory(this.tentativeVc, extraInfo, undefined); + const tpl = spec?.factory(this.tentativeVc, extraInfo); if (tpl) { this.studioCtx.setStudioFocusOnFrameContents( this.tentativeVc.arenaFrame() diff --git a/platform/wab/src/wab/client/ProjectDependencyManager.ts b/platform/wab/src/wab/client/ProjectDependencyManager.ts index 385bf635e52..b49ac3723c7 100644 --- a/platform/wab/src/wab/client/ProjectDependencyManager.ts +++ b/platform/wab/src/wab/client/ProjectDependencyManager.ts @@ -565,7 +565,8 @@ export class ProjectDependencyManager { */ getInsertableTemplate(meta: { projectId: string; - componentName: string; + componentName?: string; + componentId?: string; }): { component: Component; site: Site } | undefined { const projectId = meta.projectId; if (!this.insertableSites[projectId]) { @@ -574,7 +575,9 @@ export class ProjectDependencyManager { const site = this.insertableSites[projectId]; const components = site.components.filter((c) => !isFrameComponent(c)); - const component = components.find((c) => c.name === meta.componentName); + const component = components.find( + (c) => c.name === meta.componentName || c.uuid === meta.componentId + ); return !component ? undefined : { diff --git a/platform/wab/src/wab/client/components/canvas/FreestyleBox.tsx b/platform/wab/src/wab/client/components/canvas/FreestyleBox.tsx index e759ec3364f..ab01fad72a3 100644 --- a/platform/wab/src/wab/client/components/canvas/FreestyleBox.tsx +++ b/platform/wab/src/wab/client/components/canvas/FreestyleBox.tsx @@ -267,11 +267,7 @@ function insertFreestyleAsWrapper(viewCtx: ViewCtx, e: React.MouseEvent): void { $(targetElt as HTMLElement) ); if (tplToWrap && (isTplTagOrComponent(tplToWrap) || isTplSlot(tplToWrap))) { - const newNode = freestyleState.spec.factory( - viewOps.viewCtx(), - undefined, - undefined - ); + const newNode = freestyleState.spec.factory(viewOps.viewCtx(), undefined); if (newNode && isTplTag(newNode)) { const wrapper = ensureKnownTplTag($$$(newNode).clear().one()); viewOps.insertAsParent(wrapper, tplToWrap); diff --git a/platform/wab/src/wab/client/components/canvas/view-ops.tsx b/platform/wab/src/wab/client/components/canvas/view-ops.tsx index 55ff4169d50..27feefba4e7 100644 --- a/platform/wab/src/wab/client/components/canvas/view-ops.tsx +++ b/platform/wab/src/wab/client/components/canvas/view-ops.tsx @@ -4720,11 +4720,7 @@ export class ViewOps { tryInsertInsertableSpec( spec: { key?: AddItemKey | string; - factory: ( - viewCtx: ViewCtx, - extraInfo: T, - drawnRect?: Rect - ) => TplNode | undefined; + factory: (viewCtx: ViewCtx, extraInfo: T) => TplNode | undefined; }, loc: InsertRelLoc, extraInfo: T, @@ -4741,7 +4737,7 @@ export class ViewOps { if (!tpl) { return; } - const cmptTpl = spec.factory(this.viewCtx(), extraInfo, undefined); + const cmptTpl = spec.factory(this.viewCtx(), extraInfo); if (!cmptTpl) { return; } diff --git a/platform/wab/src/wab/client/components/insert-panel/InsertPanel.tsx b/platform/wab/src/wab/client/components/insert-panel/InsertPanel.tsx index da1ab38f8b9..2822241628f 100644 --- a/platform/wab/src/wab/client/components/insert-panel/InsertPanel.tsx +++ b/platform/wab/src/wab/client/components/insert-panel/InsertPanel.tsx @@ -5,6 +5,7 @@ import { getValidInsertLocs, InsertRelLoc, } from "@/wab/client/components/canvas/view-ops"; +import { WithContextMenu } from "@/wab/client/components/ContextMenu"; import S from "@/wab/client/components/insert-panel/InsertPanel.module.scss"; import InsertPanelTabGroup from "@/wab/client/components/insert-panel/InsertPanelTabGroup"; import InsertPanelTabItem from "@/wab/client/components/insert-panel/InsertPanelTabItem"; @@ -42,6 +43,7 @@ import { AddItemType, AddTplItem, INSERTABLES_MAP, + isTemplateComponent, isTplAddItem, } from "@/wab/client/definitions/insertables"; import { DragInsertManager } from "@/wab/client/Dnd"; @@ -126,6 +128,7 @@ import { InsertPanelConfig, } from "@/wab/shared/ui-config-utils"; import { placeholderImgUrl } from "@/wab/shared/urls"; +import { Menu } from "antd"; import cn from "classnames"; import { UseComboboxGetItemPropsOptions } from "downshift"; import L, { groupBy, last, uniq } from "lodash"; @@ -470,8 +473,8 @@ const AddDrawerContent = observer(function AddDrawerContent(props: { const index = allSectionKeysFlattened.findIndex((sec) => sec === section); const nextSection = allSectionKeysFlattened[ - (index + step + allSectionKeysFlattened.length) % - allSectionKeysFlattened.length + (index + step + allSectionKeysFlattened.length) % + allSectionKeysFlattened.length ]; setSection(nextSection); // This does not work because of some mysterious interaction with the focus tricks we're playing. Not digging into this for now. @@ -691,20 +694,20 @@ const AddDrawerContext = React.createContext( type VirtualItem = | { - type: "header"; - group: AddItemGroup; - item?: never; - } + type: "header"; + group: AddItemGroup; + item?: never; + } | { - type: "item"; - item: AddItem; - group: AddItemGroup; - itemIndex: number; - } + type: "item"; + item: AddItem; + group: AddItemGroup; + itemIndex: number; + } | { - type: "separator"; - item?: never; - }; + type: "separator"; + item?: never; + }; const Row = React.memo(function Row(props: { data: VirtualItem[][]; @@ -773,70 +776,97 @@ const Row = React.memo(function Row(props: { const showCompact = shouldShowCompact(virtualItem); const width = showCompact ? compactItemWidth : "100%"; return ( -
  • ( + ( + + { + // safe because of `cond={isTemplateComponent(item)}` check + const addTplItem = item as AddTplItem; + const tplNode = await studioCtx.tryInsertTplItem(addTplItem, { + skipDuplicateCheck: true, + }); + onInserted(addTplItem, tplNode); + }} + > + Create a new copy of this component + + + )} + > + {children} + + )} > - ( - - {children} - - )} +
  • - {showPreview ? ( - - ) : ( - { - onInserted(item, tplNode); - }} - indent={indent} - /> - )} - -
  • + ( + + {children} + + )} + > + {showPreview ? ( + + ) : ( + { + onInserted(item, tplNode); + }} + indent={indent} + /> + )} + + + ); } else if (virtualItem.type === "separator") { return ( @@ -857,7 +887,7 @@ const Row = React.memo(function Row(props: { ); }, -areEqual); + areEqual); const getTemplateComponents = memoizeOne(function getTemplateComponent( studioCtx: StudioCtx @@ -963,9 +993,8 @@ function getCodeComponentsGroups(studioCtx: StudioCtx): AddItemGroup[] { return Object.entries(subGroups).map( ([subSection, subSectionComponents]) => { return { - key: `code-components-${section}${ - subSection ? `-${subSection}` : "" - }`, + key: `code-components-${section}${subSection ? `-${subSection}` : "" + }`, isHeaderLess: !subSection, sectionKey: section, sectionLabel: section, @@ -1286,18 +1315,18 @@ export function buildAddItemGroups({ // Insertable Templates ...(!isApp && - (!contentEditorMode || customInsertableTemplates) && - !!insertableTemplatesMeta + (!contentEditorMode || customInsertableTemplates) && + !!insertableTemplatesMeta ? insertableTemplatesMeta.items - .filter( - (i) => - i.type === "insertable-templates-group" && - i.onlyShownIn !== "old" && - !i.isPageTemplatesGroup - ) - .map((g) => - getInsertableTemplatesSection(g as InsertableTemplatesGroup) - ) + .filter( + (i) => + i.type === "insertable-templates-group" && + i.onlyShownIn !== "old" && + !i.isPageTemplatesGroup + ) + .map((g) => + getInsertableTemplatesSection(g as InsertableTemplatesGroup) + ) : []), canInsertAlias(uiConfig, "icon", canInsertContext) && { @@ -1319,15 +1348,15 @@ export function buildAddItemGroups({ }, includeFrames && - canInsertAlias(uiConfig, "frame", canInsertContext) && { - key: "frames", - label: FRAMES_CAP, - items: [ - INSERTABLES_MAP.pageFrame, - INSERTABLES_MAP.componentFrame, - INSERTABLES_MAP.screenFrame, - ], - }, + canInsertAlias(uiConfig, "frame", canInsertContext) && { + key: "frames", + label: FRAMES_CAP, + items: [ + INSERTABLES_MAP.pageFrame, + INSERTABLES_MAP.componentFrame, + INSERTABLES_MAP.screenFrame, + ], + }, // Plume components. // List both un-materialized and all materialized Plume components. @@ -1342,53 +1371,53 @@ export function buildAddItemGroups({ // You can choose to show the package, but it's temporary to the session. // The section won't show when you re-open the project, you need to choose to re-show it. canInsertHostlessPackage(uiConfig, "plume", canInsertContext) && - studioCtx.shownSyntheticSections.get("plume") && { - key: "synthetic-plume", - label: 'Customizable "headless" components', - sectionLabel: "Headless components", - familyKey: "imported-packages", - items: naturalSort( - [ - ...sortComponentsByName( - studioCtx.site.components.filter((c) => c.plumeInfo) - ).map((comp) => createAddTplComponent(comp)), - ...makePlumeInsertables(studioCtx).map((item) => ({ - ...item, - previewImageUrl: undefined, - })), - ], - (item) => item.label - ), - }, + studioCtx.shownSyntheticSections.get("plume") && { + key: "synthetic-plume", + label: 'Customizable "headless" components', + sectionLabel: "Headless components", + familyKey: "imported-packages", + items: naturalSort( + [ + ...sortComponentsByName( + studioCtx.site.components.filter((c) => c.plumeInfo) + ).map((comp) => createAddTplComponent(comp)), + ...makePlumeInsertables(studioCtx).map((item) => ({ + ...item, + previewImageUrl: undefined, + })), + ], + (item) => item.label + ), + }, hasPlexus ? { - key: "ui-kits", - sectionLabel: "Design systems", - sectionKey: "Design systems", - familyKey: "hostless-packages", - isHeaderLess: true, - items: studioCtx.appCtx.appConfig.installables - .filter((meta) => meta.type === "ui-kit") - .map(createAddInstallable), - } + key: "ui-kits", + sectionLabel: "Design systems", + sectionKey: "Design systems", + familyKey: "hostless-packages", + isHeaderLess: true, + items: studioCtx.appCtx.appConfig.installables + .filter((meta) => meta.type === "ui-kit") + .map(createAddInstallable), + } : undefined, canInsertHostlessPackage(uiConfig, "unstyled", canInsertContext) && - studioCtx.shownSyntheticSections.get("unstyled") && { - key: "synthetic-unstyled", - label: "More HTML elements", - sectionLabel: "More HTML elements", - familyKey: "imported-packages", - items: [ - INSERTABLES_MAP.button, - INSERTABLES_MAP.textbox, - INSERTABLES_MAP.password, - INSERTABLES_MAP.textarea, - INSERTABLES_MAP.ul, - INSERTABLES_MAP.ol, - INSERTABLES_MAP.li, - ], - }, + studioCtx.shownSyntheticSections.get("unstyled") && { + key: "synthetic-unstyled", + label: "More HTML elements", + sectionLabel: "More HTML elements", + familyKey: "imported-packages", + items: [ + INSERTABLES_MAP.button, + INSERTABLES_MAP.textbox, + INSERTABLES_MAP.password, + INSERTABLES_MAP.textarea, + INSERTABLES_MAP.ul, + INSERTABLES_MAP.ol, + INSERTABLES_MAP.li, + ], + }, // Imported hostless packages ...naturalSort( @@ -1406,7 +1435,7 @@ export function buildAddItemGroups({ !(hostLessComponentsMeta ?? []).some( (group) => getLeafProjectIdForHostLessPackageMeta(group) === - dep.projectId && group.hiddenWhenInstalled + dep.projectId && group.hiddenWhenInstalled ) ) ).map((comp) => createAddTplComponent(comp)), @@ -1441,27 +1470,27 @@ export function buildAddItemGroups({ ...(!!hostLessComponentsMeta ? getHostLess(studioCtx) - .filter((group) => - canInsertHostlessPackage( - uiConfig, - group.codeName!, - canInsertContext - ) + .filter((group) => + canInsertHostlessPackage( + uiConfig, + group.codeName!, + canInsertContext ) - .map((group) => ({ - ...group, - items: group.items.filter( - // We want to hide the listings that were shown in "Default components" - // This is just a simple way to ensure things don't show up in both menus. - (item) => { - if (isTplAddItem(item) && item.systemName) { - return !installedHostlessComponents.has(item.systemName); - } else { - return true; - } + ) + .map((group) => ({ + ...group, + items: group.items.filter( + // We want to hide the listings that were shown in "Default components" + // This is just a simple way to ensure things don't show up in both menus. + (item) => { + if (isTplAddItem(item) && item.systemName) { + return !installedHostlessComponents.has(item.systemName); + } else { + return true; } - ), - })) + } + ), + })) : []), ]); diff --git a/platform/wab/src/wab/client/components/sidebar-tabs/ProjectPanel/NavigationDropdown.tsx b/platform/wab/src/wab/client/components/sidebar-tabs/ProjectPanel/NavigationDropdown.tsx index 2d9a5f92529..ea64370b7d7 100644 --- a/platform/wab/src/wab/client/components/sidebar-tabs/ProjectPanel/NavigationDropdown.tsx +++ b/platform/wab/src/wab/client/components/sidebar-tabs/ProjectPanel/NavigationDropdown.tsx @@ -412,13 +412,13 @@ function NavigationDropdown_( chosenTemplate.projectId && chosenTemplate.componentName, "" ); + const { screenVariant } = await getScreenVariantToInsertableTemplate(studioCtx); info = await studioCtx.appCtx.app.withSpinner( buildInsertableExtraInfo( studioCtx, - chosenTemplate.projectId, - chosenTemplate.componentName, + chosenTemplate as { projectId: string; componentName: string }, screenVariant ) ); diff --git a/platform/wab/src/wab/client/components/studio/add-drawer/AddDrawer.tsx b/platform/wab/src/wab/client/components/studio/add-drawer/AddDrawer.tsx index 69cffc96910..9c2064a97ad 100644 --- a/platform/wab/src/wab/client/components/studio/add-drawer/AddDrawer.tsx +++ b/platform/wab/src/wab/client/components/studio/add-drawer/AddDrawer.tsx @@ -28,6 +28,7 @@ import { AddItem, AddItemType, AddTplItem, + INSERTABLE_TEMPLATE_COMPONENT_KEY_PREFIX, isTplAddItem, } from "@/wab/client/definitions/insertables"; import { @@ -76,7 +77,6 @@ import { codeLit } from "@/wab/shared/core/exprs"; import { ImageAssetType } from "@/wab/shared/core/image-asset-type"; import { syncGlobalContexts } from "@/wab/shared/core/project-deps"; import { isTagListContainer } from "@/wab/shared/core/rich-text-util"; -import { allComponents } from "@/wab/shared/core/sites"; import { SlotSelection } from "@/wab/shared/core/slots"; import { unbundleProjectDependency } from "@/wab/shared/core/tagged-unbundle"; import * as Tpls from "@/wab/shared/core/tpls"; @@ -94,6 +94,7 @@ import { cloneInsertableTemplateComponent, } from "@/wab/shared/insertable-templates"; import { + CloneOpts, InsertableTemplateArenaExtraInfo, InsertableTemplateComponentExtraInfo, InsertableTemplateExtraInfo, @@ -287,7 +288,48 @@ export function createAddInstallable(meta: Installable): AddInstallableItem { }; } -export function createAddTplComponent(component: Component): AddTplItem { +function cloneTemplateComponent( + vc: ViewCtx, + { skipDuplicateCheck, ...extraInfo }: CreateAddTemplateComponentExtraInfo, + defaultKind?: string +) { + trackEvent("Insertable template component", { + insertableName: `${extraInfo.projectId}-${extraInfo.component.name}`, + }); + const { component: comp, seenFonts } = cloneInsertableTemplateComponent( + vc.site, + extraInfo, + vc.studioCtx.projectDependencyManager.plumeSite, + { skipDuplicateCheck } + ); + if (defaultKind) { + setTimeout(() => { + void vc.studioCtx.change(({ success }) => { + // ASK: If I try to do this, the Studio hangs (no longer responds to click events) and needs to be restarted. Why? + // I had to put it inside a settimeout and then wrap it in a .change to make it work. + vc.studioCtx + .tplMgr() + .addComponentToDefaultComponents(comp, defaultKind); + return success(); + }); + }, 1000); + } + postInsertableTemplate(vc.studioCtx, seenFonts); + return addTplComponent(vc, comp); +} + +function addTplComponent(vc: ViewCtx, component: Component) { + const tpl = vc.variantTplMgr().mkTplComponentWithDefaults(component); + const plugin = getPlumeEditorPlugin(tpl.component); + if (plugin) { + plugin.onComponentInserted?.(component, tpl); + } + return tpl; +} + +export function createAddTplComponent( + component: Component, +): AddTplItem { return { type: AddItemType.tpl as const, key: `tpl-component-${component.uuid}`, @@ -299,16 +341,52 @@ export function createAddTplComponent(component: Component): AddTplItem { ) : ( COMPONENT_ICON ), - factory: (vc: ViewCtx) => { - const tpl = vc.variantTplMgr().mkTplComponentWithDefaults(component); - const plugin = getPlumeEditorPlugin(tpl.component); - if (plugin) { - plugin.onComponentInserted?.(component, tpl); + factory: (vc: ViewCtx, extraInfo: CreateAddTplComponentExtraInfo) => { + if (extraInfo.type === "existing") { + return addTplComponent(vc, component); } - return tpl; + + return cloneTemplateComponent(vc, extraInfo); }, - asyncExtraInfo: async (sc): Promise => { - return { type: "existing", component }; + asyncExtraInfo: async ( + sc, + opts = {} + ): Promise => { + const { skipDuplicateCheck } = opts; + if (!skipDuplicateCheck) { + return { type: "existing", component }; + } + if ( + !component.templateInfo?.projectId || + !component.templateInfo?.componentId + ) { + return { type: "existing", component }; + } + const { projectId, componentId } = component.templateInfo; + const { screenVariant } = await getScreenVariantToInsertableTemplate(sc); + return sc.app.withSpinner( + (async () => { + const info = await buildInsertableExtraInfo( + sc, + { + projectId, + componentId, + }, + screenVariant + ); + assert( + info, + () => + `Template component with id ${component.templateInfo! + .componentId!} not found` + ); + return { + type: "clone", + skipDuplicateCheck, + ...info, + }; + })() + ); }, component, }; @@ -400,19 +478,14 @@ export function createAddInsertableTemplate( }, asyncExtraInfo: async ( sc, - opts?: { isDragging: boolean } + opts ): Promise => { const screenVariant = !opts?.isDragging ? (await getScreenVariantToInsertableTemplate(sc)).screenVariant : undefined; return sc.app.withSpinner( (async () => { - const info = await buildInsertableExtraInfo( - sc, - meta.projectId, - meta.componentName, - screenVariant - ); + const info = await buildInsertableExtraInfo(sc, meta, screenVariant); assert(info, () => `Cannot find template for ${meta.componentName}`); return info; })() @@ -422,8 +495,11 @@ export function createAddInsertableTemplate( } type CreateAddTemplateComponentExtraInfo = + InsertableTemplateComponentExtraInfo & CloneOpts; + +type CreateAddTplComponentExtraInfo = | { type: "existing"; component: Component } - | ({ type: "clone" } & InsertableTemplateComponentExtraInfo); + | ({ type: "clone" } & CreateAddTemplateComponentExtraInfo); /** * Creates an Insert Panel entry that lets you add the template component to the canvas. @@ -439,73 +515,29 @@ export function createAddTemplateComponent( ): AddTplItem { return { type: AddItemType.tpl as const, - key: `insertable-template-component-${meta.projectId}-${meta.componentName}`, + key: `${INSERTABLE_TEMPLATE_COMPONENT_KEY_PREFIX}-${meta.projectId}-${meta.componentName}`, label: meta.displayName ?? meta.componentName, canWrap: false, icon: COMBINATION_ICON, previewImageUrl: meta.imageUrl, - factory: ( - vc: ViewCtx, - extraInfo: CreateAddTemplateComponentExtraInfo, - _drawnRect?: Rect - ) => { - if (extraInfo.type === "existing") { - return createAddTplComponent(extraInfo.component).factory( - vc, - extraInfo, - _drawnRect - ); - } - trackEvent("Insertable template component", { - insertableName: `${meta.projectId}-${meta.componentName}`, - }); - const { component: comp, seenFonts } = cloneInsertableTemplateComponent( - vc.site, - extraInfo, - vc.studioCtx.projectDependencyManager.plumeSite - ); - if (defaultKind) { - setTimeout(() => { - void vc.studioCtx.change(({ success }) => { - // ASK: If I try to do this, the Studio hangs (no longer responds to click events) and needs to be restarted. Why? - // I had to put it inside a settimeout and then wrap it in a .change to make it work. - vc.studioCtx - .tplMgr() - .addComponentToDefaultComponents(comp, defaultKind); - return success(); - }); - }, 1000); - } - postInsertableTemplate(vc.studioCtx, seenFonts); - return createAddTplComponent(comp).factory(vc, extraInfo, _drawnRect); - }, + factory: (vc: ViewCtx, extraInfo: CreateAddTemplateComponentExtraInfo) => + cloneTemplateComponent(vc, extraInfo, defaultKind), asyncExtraInfo: async ( - sc + sc, + opts = {} ): Promise => { - const existing = allComponents(sc.site, { - includeDeps: "all", - }).find((comp) => comp.templateInfo?.name === meta.templateName); - if (existing) { - return { - type: "existing", - component: existing, - }; - } + const { skipDuplicateCheck } = opts; const { screenVariant } = await getScreenVariantToInsertableTemplate(sc); return sc.app.withSpinner( (async () => { - const info = await buildInsertableExtraInfo( - sc, - meta.projectId, - meta.componentName, - screenVariant - ); + const info = await buildInsertableExtraInfo(sc, meta, screenVariant); assert( info, () => `Template component ${meta.componentName} not found` ); return { type: "clone", + skipDuplicateCheck, ...info, }; })() diff --git a/platform/wab/src/wab/client/components/widgets/NewComponentModal.tsx b/platform/wab/src/wab/client/components/widgets/NewComponentModal.tsx index caa1f76b052..44307f1f20e 100644 --- a/platform/wab/src/wab/client/components/widgets/NewComponentModal.tsx +++ b/platform/wab/src/wab/client/components/widgets/NewComponentModal.tsx @@ -83,8 +83,7 @@ function NewComponentModal(props: NewComponentModalProps) { await getScreenVariantToInsertableTemplate(studioCtx); const templateInfo = await buildInsertableExtraInfo( studioCtx, - templateItem.projectId, - templateItem.componentName, + templateItem, screenVariant ); onSubmit({ name, insertableTemplateInfo: templateInfo }); diff --git a/platform/wab/src/wab/client/definitions/insertables.tsx b/platform/wab/src/wab/client/definitions/insertables.tsx index 7c0d3f6250a..48ff5528dda 100644 --- a/platform/wab/src/wab/client/definitions/insertables.tsx +++ b/platform/wab/src/wab/client/definitions/insertables.tsx @@ -54,6 +54,7 @@ import { HostLessPackageInfo, } from "@/wab/shared/devflags"; import { Rect } from "@/wab/shared/geom"; +import { CloneOpts } from "@/wab/shared/insertable-templates/types"; import { Arena, Component, TplNode, TplTag } from "@/wab/shared/model/classes"; import L from "lodash"; import * as React from "react"; @@ -111,15 +112,15 @@ export type AddFrameItem = AddItemCommon & { addDrawerPreviewImage?: string; // URL to a preview image }; +export type ExtraInfoOpts = { + isDragging?: boolean; +} & CloneOpts; + export type AddTplItem = AddItemCommon & { key: AddItemKey | string; type: AddItemType.tpl | AddItemType.plume; // Assumed to run inside sc.change() - factory: ( - viewCtx: ViewCtx, - extraInfo: T, - drawnRect?: Rect - ) => TplNode | undefined; + factory: (viewCtx: ViewCtx, extraInfo: T) => TplNode | undefined; /** * Assumed to be run just outside sc.change(). * This will get called twice when user is dragging an item: @@ -129,7 +130,7 @@ export type AddTplItem = AddItemCommon & { */ asyncExtraInfo?: ( studioCtx: StudioCtx, - opts?: { isDragging: boolean } + opts?: ExtraInfoOpts ) => Promise; canWrap?: boolean; component?: Component; @@ -156,6 +157,18 @@ export function isTplAddItem(item: AddItem): item is AddTplItem { return item.type === AddItemType.tpl || item.type === AddItemType.plume; } +export const INSERTABLE_TEMPLATE_COMPONENT_KEY_PREFIX = + "insertable-template-component-"; + +export function isTemplateComponent(item: AddItem): boolean { + return ( + isTplAddItem(item) && + // Ensure that we have template info either within the component or in the item's devflag meta + (!!item.component?.templateInfo || + item.key.startsWith(INSERTABLE_TEMPLATE_COMPONENT_KEY_PREFIX)) + ); +} + export type AddItem = | AddFrameItem | AddTplItem diff --git a/platform/wab/src/wab/client/insertable-templates.ts b/platform/wab/src/wab/client/insertable-templates.ts index 072a631d749..9595b335038 100644 --- a/platform/wab/src/wab/client/insertable-templates.ts +++ b/platform/wab/src/wab/client/insertable-templates.ts @@ -260,22 +260,36 @@ export const getHostLessDependenciesToInsertableTemplate = async ( export async function buildInsertableExtraInfo( studioCtx: StudioCtx, - projectId: string, - componentName: string, + componentMeta: { + projectId: string; + componentName?: string; + componentId?: string; + }, screenVariant: Variant | undefined ): Promise { + const { componentName, componentId, projectId } = componentMeta; + + if (!componentName && !componentId) { + return undefined; + } + await studioCtx.projectDependencyManager.fetchInsertableTemplate(projectId); const it = studioCtx.projectDependencyManager.getInsertableTemplate({ projectId, componentName, + componentId, }); if (!it) { return undefined; } + const compName = + componentName ?? + it.site.components.find((c) => c.uuid === componentId)?.name; + const template = getAllTemplates(studioCtx).find( - (c) => c.projectId === projectId && c.componentName === componentName + (c) => c.projectId === projectId && c.componentName === compName ); return { diff --git a/platform/wab/src/wab/client/studio-ctx/StudioCtx.tsx b/platform/wab/src/wab/client/studio-ctx/StudioCtx.tsx index 3c52dfb18f2..b0e29845c15 100644 --- a/platform/wab/src/wab/client/studio-ctx/StudioCtx.tsx +++ b/platform/wab/src/wab/client/studio-ctx/StudioCtx.tsx @@ -59,6 +59,7 @@ import { DbCtx, WithDbCtx } from "@/wab/client/db"; import { AddFakeItem, AddTplItem, + ExtraInfoOpts, INSERTABLES_MAP, } from "@/wab/client/definitions/insertables"; import { @@ -4313,11 +4314,14 @@ export class StudioCtx extends WithDbCtx { // // Inserting new tpl nodes // - async tryInsertTplItem(item: AddTplItem): Promise { + async tryInsertTplItem( + item: AddTplItem, + opts?: ExtraInfoOpts + ): Promise { const vc = this.focusedViewCtx(); if (!vc) { if (item.type === "tpl") { - const dragMgr = await DragInsertManager.build(this, item); + const dragMgr = await DragInsertManager.build(this, item, opts); await this.changeUnsafe(() => { this.setDragInsertState(new DragInsertState(dragMgr, item)); }); @@ -4398,7 +4402,7 @@ export class StudioCtx extends WithDbCtx { return null; } const extraInfo = item.asyncExtraInfo - ? await item.asyncExtraInfo(vc.studioCtx) + ? await item.asyncExtraInfo(vc.studioCtx, opts) : undefined; if (extraInfo === false) { return null; diff --git a/platform/wab/src/wab/shared/insertable-templates.ts b/platform/wab/src/wab/shared/insertable-templates.ts index b9af1dad495..efdc4706e41 100644 --- a/platform/wab/src/wab/shared/insertable-templates.ts +++ b/platform/wab/src/wab/shared/insertable-templates.ts @@ -27,6 +27,7 @@ import { inlineSlots, } from "@/wab/shared/insertable-templates/inliners"; import { + CloneOpts, CopyStateExtraInfo, InlineComponentContext, InsertableTemplateArenaExtraInfo, @@ -95,7 +96,8 @@ export function cloneInsertableTemplateArena( export function cloneInsertableTemplateComponent( site: Site, info: InsertableTemplateComponentExtraInfo, - plumeSite: Site | undefined + plumeSite: Site | undefined, + opts?: CloneOpts ) { const seenFonts = new Set(); @@ -121,7 +123,10 @@ export function cloneInsertableTemplateComponent( tokenImporter ); - return { component: componentImporter(info.component), seenFonts }; + return { + component: componentImporter(info.component, opts), + seenFonts, + }; } export function getUnownedTreeCloneUtils( diff --git a/platform/wab/src/wab/shared/insertable-templates/component-importer.ts b/platform/wab/src/wab/shared/insertable-templates/component-importer.ts index ed5d18662fd..d9363ced85f 100644 --- a/platform/wab/src/wab/shared/insertable-templates/component-importer.ts +++ b/platform/wab/src/wab/shared/insertable-templates/component-importer.ts @@ -17,12 +17,14 @@ import { makeImageAssetFixer, } from "@/wab/shared/insertable-templates/fixers"; import { ensureHostLessDepComponent } from "@/wab/shared/insertable-templates/inliners"; -import { HostLessDependencies } from "@/wab/shared/insertable-templates/types"; +import { + CloneOpts, + HostLessDependencies, +} from "@/wab/shared/insertable-templates/types"; import { Component, ComponentTemplateInfo, Site, - TplComponent, TplNode, Variant, } from "@/wab/shared/model/classes"; @@ -37,7 +39,7 @@ interface OriginInfo { export type ComponentImporter = ( comp: Component, - tpl?: TplComponent + opts?: CloneOpts ) => Component; export function importComponentsInTree( @@ -48,7 +50,7 @@ export function importComponentsInTree( ) { for (const tpl of flattenTpls(tplTree)) { if (isTplComponent(tpl)) { - const newComp = importer(tpl.component, tpl); + const newComp = importer(tpl.component); if (tpl.component === newComp) { // If the component is the same, there's no need to swap it continue; @@ -96,7 +98,7 @@ export function mkInsertableComponentImporter( } }; - const getNewComponent = (comp: Component, tpl?: TplComponent) => { + const getNewComponent: ComponentImporter = (comp, opts) => { if (oldToNewComponent.has(comp)) { return oldToNewComponent.get(comp)!; } @@ -153,20 +155,22 @@ export function mkInsertableComponentImporter( return plumeComp; } - const existing = site.components.find( - (c) => - // We can match by name if the there is one in templateInfo, or by (projectId, componentId) - // we could also just match by componentId it should be hard to collide, but let's be safe - // - // It's important to note that components coming from dependencies sites will also be added - // with the template projectId - // - // We also check if the component is a valid replacement based in the params/variants - (c.templateInfo?.name && - c.templateInfo?.name === comp.templateInfo?.name) || - (c.templateInfo?.componentId === comp.uuid && - c.templateInfo?.projectId === info.projectId) - ); + const existing = opts?.skipDuplicateCheck + ? undefined + : site.components.find( + (c) => + // We can match by name if the there is one in templateInfo, or by (projectId, componentId) + // we could also just match by componentId it should be hard to collide, but let's be safe + // + // It's important to note that components coming from dependencies sites will also be added + // with the template projectId + // + // We also check if the component is a valid replacement based in the params/variants + (c.templateInfo?.name && + c.templateInfo?.name === comp.templateInfo?.name) || + (c.templateInfo?.componentId === comp.uuid && + c.templateInfo?.projectId === info.projectId) + ); if (existing) { oldToNewComponent.set(comp, existing); return existing; diff --git a/platform/wab/src/wab/shared/insertable-templates/types.ts b/platform/wab/src/wab/shared/insertable-templates/types.ts index 9d4951262b6..202b786f610 100644 --- a/platform/wab/src/wab/shared/insertable-templates/types.ts +++ b/platform/wab/src/wab/shared/insertable-templates/types.ts @@ -20,6 +20,10 @@ export type HostLessDependencies = Record< } >; +export type CloneOpts = { + skipDuplicateCheck?: true; +}; + export interface InsertableTemplateExtraInfo { site: Site; screenVariant: Variant | undefined;