From cf9d383310af38a5c9cd2407ee5f8861285f153e Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 13 Oct 2023 11:58:05 +0200 Subject: [PATCH 01/11] refactor parent finding function --- .../post-action-options/post-action-paste.ts | 1 - .../reparent-helpers/reparent-helpers.ts | 25 +- .../src/components/editor/insert-callbacks.ts | 74 ++-- .../navigator-item-dnd-container.tsx | 14 +- editor/src/core/shared/array-utils.ts | 11 + editor/src/core/shared/element-path.ts | 37 +- editor/src/utils/clipboard.ts | 415 +++++++++++------- 7 files changed, 334 insertions(+), 243 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts index 257c4e3b972f..5992b15051b4 100644 --- a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts +++ b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts @@ -568,7 +568,6 @@ function getTargetParentForPasteHere( editor.selectedViews, editor.canvas.openFile?.filename ?? null, editor.jsxMetadata, - editor.pasteTargetsToIgnore, { elementPaste: elementToPaste, originalContextMetadata: originalMetadata, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts index 1d80017b799f..b22b950cdf8b 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts @@ -331,40 +331,29 @@ export function getInsertionPathForReparentTarget( } function areElementsInstancesOfTheSameComponent( - firstInstance: ElementInstanceMetadata | null, - secondInstance: ElementInstanceMetadata | null, + firstElement: JSXElement, + secondElement: JSXElement, ): boolean { - if ( - firstInstance == null || - secondInstance == null || - isLeft(firstInstance.element) || - isLeft(secondInstance.element) || - !isJSXElement(firstInstance.element.value) || - !isJSXElement(secondInstance.element.value) - ) { - return false - } - - return jsxElementNameEquals(firstInstance.element.value.name, secondInstance.element.value.name) + return jsxElementNameEquals(firstElement.name, secondElement.name) } export function isElementRenderedBySameComponent( metadata: ElementInstanceMetadataMap, targetPath: ElementPath, - instance: ElementInstanceMetadata | null, + element: JSXElement, ): boolean { if (EP.isEmptyPath(targetPath)) { return false } - const currentInstance = MetadataUtils.findElementByElementPath( + const targetElement = MetadataUtils.getJSXElementFromMetadata( metadata, EP.getContainingComponent(targetPath), ) return ( - areElementsInstancesOfTheSameComponent(currentInstance, instance) || - isElementRenderedBySameComponent(metadata, EP.getContainingComponent(targetPath), instance) + (targetElement != null && areElementsInstancesOfTheSameComponent(targetElement, element)) || + isElementRenderedBySameComponent(metadata, EP.getContainingComponent(targetPath), element) ) } diff --git a/editor/src/components/editor/insert-callbacks.ts b/editor/src/components/editor/insert-callbacks.ts index e22bf691c136..a8c53bd78edd 100644 --- a/editor/src/components/editor/insert-callbacks.ts +++ b/editor/src/components/editor/insert-callbacks.ts @@ -29,10 +29,10 @@ import type { InsertionSubject } from './editor-modes' import { useDispatch } from './store/dispatch-context' import { Substores, useEditorState, useRefEditorState } from './store/store-hook' import type { InsertMenuItem } from '../canvas/ui/floating-insert-menu' -import { safeIndex } from '../../core/shared/array-utils' +import { isNonEmptyArray, safeIndex } from '../../core/shared/array-utils' import type { ElementPath } from '../../core/shared/project-file-types' import { elementToReparent } from '../canvas/canvas-strategies/strategies/reparent-utils' -import { getInsertionPath } from './store/insertion-path' +import { childInsertionPath, getInsertionPath } from './store/insertion-path' import { fixUtopiaElement, generateConsistentUID } from '../../core/shared/uid-utils' import { getAllUniqueUids } from '../../core/model/get-unique-ids' import { assertNever } from '../../core/shared/utils' @@ -40,10 +40,12 @@ import type { ComponentElementToInsert } from '../custom-code/code-file' import { notice } from '../common/notice' import * as PP from '../../core/shared/property-path' import { setJSXValueInAttributeAtPath } from '../../core/shared/jsx-attributes' -import { defaultEither } from '../../core/shared/either' +import { defaultEither, isLeft } from '../../core/shared/either' import { executeFirstApplicableStrategy } from '../inspector/inspector-strategies/inspector-strategy' import { insertAsAbsoluteStrategy } from './one-shot-insertion-strategies/insert-as-absolute-strategy' import { insertAsStaticStrategy } from './one-shot-insertion-strategies/insert-as-static-strategy' +import { getStoryboardElementPath } from '../../core/model/scene-utils' +import { getTargetParentForOneShotInsertion, reparentIntoParent } from '../../utils/clipboard' function shouldSubjectBeWrappedWithConditional( subject: InsertionSubject, @@ -162,6 +164,7 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void const allElementPropsRef = useRefEditorState((store) => store.editor.allElementProps) const elementPathTreeRef = useRefEditorState((store) => store.editor.elementPathTree) const projectContentsRef = useRefEditorState((store) => store.editor.projectContents) + const openFileRef = useRefEditorState((store) => store.editor.canvas.openFile?.filename ?? null) const nodeModulesRef = useRefEditorState((store) => store.editor.nodeModules) return React.useCallback( @@ -169,19 +172,13 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void if (elementToInsert == null) { return } - const targetParent: ElementPath | null = safeIndex(selectedViewsRef.current, 0) ?? null - if (targetParent == null) { - dispatch([ - showToast( - notice( - 'There are no elements selected', - 'INFO', - false, - 'to-insert-does-not-support-children', - ), - ), - ]) + const storyboardPath = getStoryboardElementPath( + projectContentsRef.current, + openFileRef.current, + ) + if (storyboardPath == null) { + // if there's no storyboard, there's not much you can do return } @@ -191,29 +188,6 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void allElementUids.add(wrappedUid) - const insertionPath = getInsertionPath( - targetParent, - projectContentsRef.current, - jsxMetadataRef.current, - elementPathTreeRef.current, - wrappedUid, - 1, - ) - - if (insertionPath == null) { - dispatch([ - showToast( - notice( - 'Selected element does not support children', - 'INFO', - false, - 'to-insert-does-not-support-children', - ), - ), - ]) - return - } - const elementUid = generateConsistentUID('element', allElementUids) const element = elementToReparent( @@ -224,6 +198,25 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void elementToInsert.value.importsToAdd, ) + const targetParent = !isNonEmptyArray(selectedViewsRef.current) + ? reparentIntoParent(childInsertionPath(storyboardPath)) + : getTargetParentForOneShotInsertion( + projectContentsRef.current, + selectedViewsRef.current, + jsxMetadataRef.current, + [element.element], + elementPathTreeRef.current, + ) + + if (isLeft(targetParent)) { + dispatch([ + showToast( + notice(targetParent.value, 'INFO', false, 'to-insert-does-not-support-children'), + ), + ]) + return + } + executeFirstApplicableStrategy( dispatch, jsxMetadataRef.current, @@ -233,14 +226,14 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void [ insertAsAbsoluteStrategy( element, - insertionPath, + targetParent.value.parentPath, builtInDependenciesRef.current, projectContentsRef.current, nodeModulesRef.current.files, ), insertAsStaticStrategy( element, - insertionPath, + targetParent.value.parentPath, builtInDependenciesRef.current, projectContentsRef.current, nodeModulesRef.current.files, @@ -255,6 +248,7 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void elementPathTreeRef, jsxMetadataRef, nodeModulesRef, + openFileRef, projectContentsRef, selectedViewsRef, ], diff --git a/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx b/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx index 15bb51215cd7..24642d1f2322 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx @@ -239,14 +239,12 @@ function notDroppingIntoOwnDefinition( targetParent: ElementPath, metadata: ElementInstanceMetadataMap, ) { - return selectedViews.every( - (selection) => - !isElementRenderedBySameComponent( - metadata, - targetParent, - MetadataUtils.findElementByElementPath(metadata, selection), - ), - ) + return selectedViews.every((selection) => { + const jsxElement = MetadataUtils.getJSXElementFromMetadata(metadata, selection) + return ( + jsxElement == null || !isElementRenderedBySameComponent(metadata, targetParent, jsxElement) + ) + }) } function canDropInto(editorState: EditorState, moveToEntry: ElementPath): boolean { diff --git a/editor/src/core/shared/array-utils.ts b/editor/src/core/shared/array-utils.ts index c33743a28ee9..c82610b3fed0 100644 --- a/editor/src/core/shared/array-utils.ts +++ b/editor/src/core/shared/array-utils.ts @@ -456,3 +456,14 @@ export function zip(one: A[], other: B[], make: (a: A, b: B) => C): C[] return one.length < other.length ? doZip(one, other) : doZip(one.slice(0, other.length), other) } + +// https://matiashernandez.dev/blog/post/typescript-how-to-create-a-non-empty-array-type +export type NonEmptyArray = [T, ...T[]] +export function isNonEmptyArray(array: T[]): array is NonEmptyArray { + let [first] = array + return first == null +} + +export function isEmptyArray(array: T[]): array is [] { + return array.length === 0 +} diff --git a/editor/src/core/shared/element-path.ts b/editor/src/core/shared/element-path.ts index 0ce103776bf9..4cef25864d92 100644 --- a/editor/src/core/shared/element-path.ts +++ b/editor/src/core/shared/element-path.ts @@ -13,7 +13,8 @@ import { arrayEqualsByValue, } from './utils' import { replaceAll } from './string-utils' -import { last, dropLastN, drop, splitAt, flattenArray, dropLast } from './array-utils' +import type { NonEmptyArray } from './array-utils' +import { last, dropLastN, drop, dropLast } from './array-utils' import { forceNotNull } from './optional-utils' // KILLME, except in 28 places @@ -768,22 +769,17 @@ export function replaceOrDefault( return replaceIfAncestor(path, replaceSearch, replaceWith) ?? path } -// TODO: remove null export function closestSharedAncestor( - l: ElementPath | null, - r: ElementPath | null, + l: ElementPath, + r: ElementPath, includePathsEqual: boolean, -): ElementPath | null { - const toTargetPath: (p: ElementPath) => ElementPath | null = includePathsEqual - ? identity - : parentPath +): ElementPath { + const toTargetPath: (p: ElementPath) => ElementPath = includePathsEqual ? identity : parentPath - const lTarget = l == null ? null : toTargetPath(l) - const rTarget = r == null ? null : toTargetPath(r) + const lTarget = toTargetPath(l) + const rTarget = toTargetPath(r) - if (l === null || r === null || lTarget == null || rTarget == null) { - return null - } else if (l === r) { + if (l === r) { return toTargetPath(l) } else { const fullyMatchedElementPathParts = longestCommonArray( @@ -800,7 +796,7 @@ export function closestSharedAncestor( ? [...fullyMatchedElementPathParts, nextMatchedElementPath] : fullyMatchedElementPathParts - return totalMatchedParts.length > 0 ? elementPath(totalMatchedParts) : null + return totalMatchedParts.length > 0 ? elementPath(totalMatchedParts) : emptyElementPath } } @@ -812,13 +808,18 @@ export function getCommonParent( return null } else { const parents = includeSelf ? paths : paths.map(parentPath) - return parents.reduce( - (l, r) => closestSharedAncestor(l, r, true), - parents[0], - ) + return parents.reduce((l, r) => closestSharedAncestor(l, r, true), parents[0]) } } +export function getCommonParentOfNonemptyPathArray( + paths: NonEmptyArray, + includeSelf: boolean = false, +): ElementPath { + const parents = includeSelf ? paths : paths.map(parentPath) + return parents.reduce((l, r) => closestSharedAncestor(l, r, true), parents[0]) +} + export interface ElementsTransformResult { elements: Array transformedElement: T | null diff --git a/editor/src/utils/clipboard.ts b/editor/src/utils/clipboard.ts index b1ae057d0f68..30598b7c4a6b 100644 --- a/editor/src/utils/clipboard.ts +++ b/editor/src/utils/clipboard.ts @@ -3,37 +3,25 @@ import * as EditorActions from '../components/editor/actions/action-creators' import { EditorModes } from '../components/editor/editor-modes' import type { AllElementProps, - DerivedState, EditorState, PastePostActionMenuData, } from '../components/editor/store/editor-state' import { getElementFromProjectContents, getOpenUIJSFileKey, - withUnderlyingTarget, } from '../components/editor/store/editor-state' import { getFrameAndMultiplier } from '../components/images' import * as EP from '../core/shared/element-path' import { MetadataUtils } from '../core/model/element-metadata-utils' -import type { ElementInstanceMetadataMap } from '../core/shared/element-template' -import { - isJSXConditionalExpression, - isNullJSXAttributeValue, -} from '../core/shared/element-template' -import { getUtopiaJSXComponentsFromSuccess } from '../core/model/project-file-utils' -import type { ElementPath, NodeModules } from '../core/shared/project-file-types' +import type { ElementInstanceMetadataMap, JSXElementChild } from '../core/shared/element-template' +import type { ElementPath } from '../core/shared/project-file-types' import { isParseSuccess, isTextFile } from '../core/shared/project-file-types' import type { PasteResult } from './clipboard-utils' -import { - encodeUtopiaDataToHtml, - extractFiles, - extractUtopiaDataFromClipboardData, -} from './clipboard-utils' +import { extractFiles, extractUtopiaDataFromClipboardData } from './clipboard-utils' import Utils from './utils' import type { FileResult, ImageResult } from '../core/shared/file-utils' import type { CanvasPoint, MaybeInfinityCanvasRectangle } from '../core/shared/math-utils' -import { isInfinityRectangle, rectanglesEqual } from '../core/shared/math-utils' -import * as json5 from 'json5' +import { isInfinityRectangle } from '../core/shared/math-utils' import { fastForEach } from '../core/shared/utils' import urljoin from 'url-join' import type { ProjectContentTreeRoot } from '../components/assets' @@ -42,23 +30,20 @@ import { normalisePathSuccessOrThrowError, normalisePathToUnderlyingTarget, } from '../components/custom-code/code-file' -import { mapDropNulls, stripNulls } from '../core/shared/array-utils' +import type { NonEmptyArray } from '../core/shared/array-utils' +import { mapDropNulls, isNonEmptyArray } from '../core/shared/array-utils' import ClipboardPolyfill from 'clipboard-polyfill' import { mapValues, pick } from '../core/shared/object-utils' import { getStoryboardElementPath } from '../core/model/scene-utils' import { getRequiredImportsForElement } from '../components/editor/import-utils' import type { BuiltInDependencies } from '../core/es-modules/package-manager/built-in-dependencies-list' import type { InsertionPath } from '../components/editor/store/insertion-path' -import { childInsertionPath, getInsertionPath } from '../components/editor/store/insertion-path' -import { maybeBranchConditionalCase } from '../core/model/conditionals' -import { optionalMap } from '../core/shared/optional-utils' -import { isFeatureEnabled } from './feature-switches' +import { childInsertionPath } from '../components/editor/store/insertion-path' import type { ElementPathTrees } from '../core/shared/element-path-tree' import { isElementRenderedBySameComponent, replaceJSXElementCopyData, } from '../components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers' -import CanvasActions from '../components/canvas/canvas-actions' import { PropsPreservedPastePostActionChoice, PropsReplacedPastePostActionChoice, @@ -66,8 +51,6 @@ import { import type { Either } from '../core/shared/either' import { isLeft, left, right } from '../core/shared/either' import { notice } from '../components/common/notice' -import { generateUidWithExistingComponents } from '../core/model/element-template-utils' -import type { RemixRoutingTable } from '../components/editor/store/remix-derived-data' export interface ElementPasteWithMetadata { elements: ElementPaste[] @@ -143,19 +126,30 @@ function getJSXElementPasteActions( ? clipboardFirstEntry.copyDataWithPropsReplaced : clipboardFirstEntry.copyDataWithPropsPreserved - const target = getTargetParentForPaste( - editor.projectContents, - editor.selectedViews, - editor.canvas.openFile?.filename ?? null, - editor.jsxMetadata, - editor.pasteTargetsToIgnore, - { - elementPaste: copyDataToUse.elements, - originalContextMetadata: copyDataToUse.targetOriginalContextMetadata, - originalContextElementPathTrees: clipboardFirstEntry.targetOriginalContextElementPathTrees, - }, - editor.elementPathTree, - ) + const openFile = editor.canvas.openFile?.filename ?? null + + const selectedViews = editor.selectedViews + + const storyboardPath = getStoryboardElementPath(editor.projectContents, openFile) + if (storyboardPath == null) { + // if there's no storyboard, there's not much you can do + return [] + } + + const target = !isNonEmptyArray(selectedViews) + ? reparentIntoParent(childInsertionPath(storyboardPath)) + : getTargetParentForPaste( + editor.projectContents, + selectedViews, + editor.jsxMetadata, + { + elementPaste: copyDataToUse.elements, + originalContextMetadata: copyDataToUse.targetOriginalContextMetadata, + originalContextElementPathTrees: + clipboardFirstEntry.targetOriginalContextElementPathTrees, + }, + editor.elementPathTree, + ) if (isLeft(target)) { return [ @@ -205,7 +199,6 @@ function getFilePasteActions( canvasViewportCenter: CanvasPoint, pastedFiles: Array, selectedViews: Array, - pasteTargetsToIgnore: ElementPath[], componentMetadata: ElementInstanceMetadataMap, canvasScale: number, elementPathTree: ElementPathTrees, @@ -213,15 +206,22 @@ function getFilePasteActions( if (pastedFiles.length == 0) { return [] } - const target = getTargetParentForPaste( - projectContents, - selectedViews, - openFile, - componentMetadata, - pasteTargetsToIgnore, - { elementPaste: [], originalContextMetadata: {}, originalContextElementPathTrees: {} }, // TODO: get rid of this when refactoring pasting images - elementPathTree, - ) + + const storyboardPath = getStoryboardElementPath(projectContents, openFile) + if (storyboardPath == null) { + // if there's no storyboard, there's not much you can do + return [] + } + + const target = !isNonEmptyArray(selectedViews) + ? reparentIntoParent(childInsertionPath(storyboardPath)) + : getTargetParentForPaste( + projectContents, + selectedViews, + componentMetadata, + { elementPaste: [], originalContextMetadata: {}, originalContextElementPathTrees: {} }, // TODO: get rid of this when refactoring pasting images + elementPathTree, + ) if (isLeft(target)) { return [ @@ -276,7 +276,6 @@ export function getActionsForClipboardItems( canvasViewportCenter, pastedFiles, editor.selectedViews, - editor.pasteTargetsToIgnore, editor.jsxMetadata, canvasScale, editor.elementPathTree, @@ -438,138 +437,168 @@ export type ReparentTargetForPaste = } | { type: 'parent'; parentPath: InsertionPath } +export const reparentIntoParent = ( + parentPath: InsertionPath, +): Either => + right({ + type: 'parent', + parentPath: parentPath, + }) + type PasteParentNotFoundError = | 'Cannot find a suitable parent' - | 'Cannot find storyboard path' | 'Cannot insert component instance into component definition' -export function getTargetParentForPaste( - projectContents: ProjectContentTreeRoot, - selectedViews: Array, - openFile: string | null | undefined, +function checkComponentNotInsertedIntoOwnDefinition( + selectedViews: NonEmptyArray, metadata: ElementInstanceMetadataMap, - pasteTargetsToIgnore: ElementPath[], + elementsToInsert: JSXElementChild[], +): boolean { + const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true) + + return elementsToInsert.some((element) => { + if (element.type !== 'JSX_ELEMENT') { + return false + } + + return isElementRenderedBySameComponent(metadata, parentTarget, element) + }) +} + +function insertIntoSlot( copyData: ParsedCopyData, - elementPathTree: ElementPathTrees, -): Either { + selectedViews: ElementPath[], + metadata: ElementInstanceMetadataMap, +): ReparentTargetForPaste | null { + if (selectedViews.length !== 1) { + return null + } + // These should exist because the check above proves there should be a values there. + const targetPath = selectedViews[0]! + const elementPasteEntry = copyData.elementPaste[0]! + const selectedViewAABB = MetadataUtils.getFrameInCanvasCoords(targetPath, metadata) + // if the pasted item's BB is the same size as the selected item's BB + const pastedElementAABB = MetadataUtils.getFrameInCanvasCoords( + elementPasteEntry.originalElementPath, + copyData.originalContextMetadata, + ) + // if the selected item's parent is autolayouted + const parentInstance = MetadataUtils.findElementByElementPath(metadata, EP.parentPath(targetPath)) + + const isSelectedViewParentAutolayouted = MetadataUtils.isFlexLayoutedContainer(parentInstance) + + const pastingAbsoluteToAbsolute = + MetadataUtils.isPositionAbsolute( + MetadataUtils.findElementByElementPath(metadata, targetPath), + ) && + MetadataUtils.isPositionAbsolute( + MetadataUtils.findElementByElementPath( + copyData.originalContextMetadata, + elementPasteEntry.originalElementPath, + ), + ) + const pastedElementNames = mapDropNulls( (element) => MetadataUtils.getJSXElementName(element.element), copyData.elementPaste, ) - if (selectedViews.length === 0) { - const storyboardPath = getStoryboardElementPath(projectContents, openFile) - if (storyboardPath == null) { - return left('Cannot find storyboard path') - } - return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) - } - - // Regular handling which attempts to find a common parent. - const parentTarget = EP.getCommonParent(selectedViews, true) - if (parentTarget == null) { - return left('Cannot find a suitable parent') - } + const parentPath = EP.parentPath(targetPath) + const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( + parentPath, + metadata, + pastedElementNames, + ) if ( - copyData.elementPaste.some((pastedElement) => - isElementRenderedBySameComponent( - metadata, - parentTarget, - MetadataUtils.findElementByElementPath( - copyData.originalContextMetadata, - pastedElement.originalElementPath, - ), - ), - ) + rectangleSizesEqual(selectedViewAABB, pastedElementAABB) && + (isSelectedViewParentAutolayouted || pastingAbsoluteToAbsolute) && + targetElementSupportsInsertedElement ) { - return left('Cannot insert component instance into component definition') + return { + type: 'sibling', + siblingPath: targetPath, + parentPath: childInsertionPath(EP.parentPath(targetPath)), + } } - // Handle "slot" like case of conditional clauses by inserting into them directly rather than their parent. - if (selectedViews.length === 1) { - // This should exist because the check above proves there should be a value. - const targetPath = selectedViews[0]! - const parentPath = EP.parentPath(targetPath) - const parentElement = withUnderlyingTarget(parentPath, projectContents, null, (_, element) => { - return element - }) - - if (parentElement != null && isJSXConditionalExpression(parentElement)) { - // Check if the target parent is an attribute, - // if so replace the target parent instead of trying to insert into it. - const wrapperFragmentUID = generateUidWithExistingComponents(projectContents) - const conditionalCase = maybeBranchConditionalCase(parentPath, parentElement, targetPath) - if (conditionalCase != null) { - const parentInsertionPath = getInsertionPath( - targetPath, - projectContents, - metadata, - elementPathTree, - wrapperFragmentUID, - copyData.elementPaste.length, - ) + return null +} - if (parentInsertionPath == null) { - return left('Cannot find a suitable parent') - } - return right({ type: 'parent', parentPath: parentInsertionPath }) - } - } +function pasteNextToSameSizedElement( + copyData: ParsedCopyData, + selectedViews: ElementPath[], + metadata: ElementInstanceMetadataMap, +): ReparentTargetForPaste | null { + if (selectedViews.length !== 1) { + return null } + // These should exist because the check above proves there should be a values there. + const targetPath = selectedViews[0]! + const elementPasteEntry = copyData.elementPaste[0]! + const selectedViewAABB = MetadataUtils.getFrameInCanvasCoords(targetPath, metadata) + // if the pasted item's BB is the same size as the selected item's BB + const pastedElementAABB = MetadataUtils.getFrameInCanvasCoords( + elementPasteEntry.originalElementPath, + copyData.originalContextMetadata, + ) + // if the selected item's parent is autolayouted + const parentInstance = MetadataUtils.findElementByElementPath(metadata, EP.parentPath(targetPath)) - // if only a single item is selected - if (selectedViews.length === 1 && copyData.elementPaste.length === 1) { - // These should exist because the check above proves there should be a values there. - const targetPath = selectedViews[0]! - const elementPasteEntry = copyData.elementPaste[0]! - const selectedViewAABB = MetadataUtils.getFrameInCanvasCoords(targetPath, metadata) - // if the pasted item's BB is the same size as the selected item's BB - const pastedElementAABB = MetadataUtils.getFrameInCanvasCoords( - elementPasteEntry.originalElementPath, - copyData.originalContextMetadata, - ) - // if the selected item's parent is autolayouted - const parentInstance = MetadataUtils.findElementByElementPath( - metadata, - EP.parentPath(targetPath), - ) + const isSelectedViewParentAutolayouted = MetadataUtils.isFlexLayoutedContainer(parentInstance) - const isSelectedViewParentAutolayouted = MetadataUtils.isFlexLayoutedContainer(parentInstance) + const pastingAbsoluteToAbsolute = + MetadataUtils.isPositionAbsolute( + MetadataUtils.findElementByElementPath(metadata, targetPath), + ) && + MetadataUtils.isPositionAbsolute( + MetadataUtils.findElementByElementPath( + copyData.originalContextMetadata, + elementPasteEntry.originalElementPath, + ), + ) - const pastingAbsoluteToAbsolute = - MetadataUtils.isPositionAbsolute( - MetadataUtils.findElementByElementPath(metadata, targetPath), - ) && - MetadataUtils.isPositionAbsolute( - MetadataUtils.findElementByElementPath( - copyData.originalContextMetadata, - elementPasteEntry.originalElementPath, - ), - ) + const pastedElementNames = mapDropNulls( + (element) => MetadataUtils.getJSXElementName(element.element), + copyData.elementPaste, + ) - const parentPath = EP.parentPath(targetPath) - const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( - parentPath, - metadata, - pastedElementNames, - ) + const parentPath = EP.parentPath(targetPath) + const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( + parentPath, + metadata, + pastedElementNames, + ) - if ( - rectangleSizesEqual(selectedViewAABB, pastedElementAABB) && - (isSelectedViewParentAutolayouted || pastingAbsoluteToAbsolute) && - targetElementSupportsInsertedElement - ) { - return right({ - type: 'sibling', - siblingPath: targetPath, - parentPath: childInsertionPath(EP.parentPath(targetPath)), - }) + if ( + rectangleSizesEqual(selectedViewAABB, pastedElementAABB) && + (isSelectedViewParentAutolayouted || pastingAbsoluteToAbsolute) && + targetElementSupportsInsertedElement + ) { + return { + type: 'sibling', + siblingPath: targetPath, + parentPath: childInsertionPath(EP.parentPath(targetPath)), } } + return null +} + +function pasteIntoParentOrGrandparent( + elementsToInsert: JSXElementChild[], + projectContents: ProjectContentTreeRoot, + selectedViews: NonEmptyArray, + metadata: ElementInstanceMetadataMap, + elementPathTree: ElementPathTrees, +): ReparentTargetForPaste | null { + const pastedElementNames = mapDropNulls( + (element) => (element.type === 'JSX_ELEMENT' ? element.name : null), + elementsToInsert, + ) + + const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true) - // we should not paste the source into itself - const insertingSourceIntoItself = EP.containsPath(parentTarget, pasteTargetsToIgnore) + // paste into parent const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( parentTarget, metadata, @@ -582,12 +611,12 @@ export function getTargetParentForPaste( parentTarget, elementPathTree, ) && - targetElementSupportsInsertedElement && - !insertingSourceIntoItself + targetElementSupportsInsertedElement ) { - return right({ type: 'parent', parentPath: childInsertionPath(parentTarget) }) + return { type: 'parent', parentPath: childInsertionPath(parentTarget) } } + // paste into parent of parent const parentOfSelected = EP.parentPath(parentTarget) if ( MetadataUtils.targetSupportsChildren( @@ -597,8 +626,78 @@ export function getTargetParentForPaste( elementPathTree, ) ) { - return right({ type: 'parent', parentPath: childInsertionPath(parentOfSelected) }) + return { type: 'parent', parentPath: childInsertionPath(parentOfSelected) } + } + return null +} + +// TODO: insert into slot +export function getTargetParentForOneShotInsertion( + projectContents: ProjectContentTreeRoot, + selectedViews: NonEmptyArray, + metadata: ElementInstanceMetadataMap, + elementsToInsert: JSXElementChild[], + elementPathTree: ElementPathTrees, +): Either { + if (!checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, elementsToInsert)) { + return left('Cannot insert component instance into component definition') } + const pasteIntoParentOrGrandparentResult = pasteIntoParentOrGrandparent( + elementsToInsert, + projectContents, + selectedViews, + metadata, + elementPathTree, + ) + if (pasteIntoParentOrGrandparentResult != null) { + return right(pasteIntoParentOrGrandparentResult) + } + return left('Cannot find a suitable parent') +} + +export function getTargetParentForPaste( + projectContents: ProjectContentTreeRoot, + selectedViews: NonEmptyArray, + metadata: ElementInstanceMetadataMap, + copyData: ParsedCopyData, + elementPathTree: ElementPathTrees, +): Either { + const pastedJSXElements = mapDropNulls( + (p) => + MetadataUtils.getJSXElementFromMetadata( + copyData.originalContextMetadata, + p.originalElementPath, + ), + copyData.elementPaste, + ) + if (!checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, pastedJSXElements)) { + return left('Cannot insert component instance into component definition') + } + + const insertIntoSlotResult = insertIntoSlot(copyData, selectedViews, metadata) + if (insertIntoSlotResult != null) { + return right(insertIntoSlotResult) + } + + const pasteNextToSameSizedElementResult = pasteNextToSameSizedElement( + copyData, + selectedViews, + metadata, + ) + if (pasteNextToSameSizedElementResult != null) { + return right(pasteNextToSameSizedElementResult) + } + + const pasteIntoParentOrGrandparentResult = pasteIntoParentOrGrandparent( + copyData.elementPaste.map((e) => e.element), + projectContents, + selectedViews, + metadata, + elementPathTree, + ) + if (pasteIntoParentOrGrandparentResult != null) { + return right(pasteIntoParentOrGrandparentResult) + } return left('Cannot find a suitable parent') } From 34bd8bd2b1018d4aca06e5639377cfa2afecac78 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 13 Oct 2023 12:13:47 +0200 Subject: [PATCH 02/11] actually extract insertIntoSlot --- editor/src/utils/clipboard.ts | 99 +++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/editor/src/utils/clipboard.ts b/editor/src/utils/clipboard.ts index 30598b7c4a6b..3ff12de6e3ba 100644 --- a/editor/src/utils/clipboard.ts +++ b/editor/src/utils/clipboard.ts @@ -9,11 +9,16 @@ import type { import { getElementFromProjectContents, getOpenUIJSFileKey, + withUnderlyingTarget, } from '../components/editor/store/editor-state' import { getFrameAndMultiplier } from '../components/images' import * as EP from '../core/shared/element-path' import { MetadataUtils } from '../core/model/element-metadata-utils' -import type { ElementInstanceMetadataMap, JSXElementChild } from '../core/shared/element-template' +import { + isJSXConditionalExpression, + type ElementInstanceMetadataMap, + type JSXElementChild, +} from '../core/shared/element-template' import type { ElementPath } from '../core/shared/project-file-types' import { isParseSuccess, isTextFile } from '../core/shared/project-file-types' import type { PasteResult } from './clipboard-utils' @@ -38,7 +43,7 @@ import { getStoryboardElementPath } from '../core/model/scene-utils' import { getRequiredImportsForElement } from '../components/editor/import-utils' import type { BuiltInDependencies } from '../core/es-modules/package-manager/built-in-dependencies-list' import type { InsertionPath } from '../components/editor/store/insertion-path' -import { childInsertionPath } from '../components/editor/store/insertion-path' +import { childInsertionPath, getInsertionPath } from '../components/editor/store/insertion-path' import type { ElementPathTrees } from '../core/shared/element-path-tree' import { isElementRenderedBySameComponent, @@ -51,6 +56,8 @@ import { import type { Either } from '../core/shared/either' import { isLeft, left, right } from '../core/shared/either' import { notice } from '../components/common/notice' +import { maybeBranchConditionalCase } from '../core/model/conditionals' +import { generateUidWithExistingComponents } from '../core/model/element-template-utils' export interface ElementPasteWithMetadata { elements: ElementPaste[] @@ -466,63 +473,48 @@ function checkComponentNotInsertedIntoOwnDefinition( } function insertIntoSlot( - copyData: ParsedCopyData, selectedViews: ElementPath[], metadata: ElementInstanceMetadataMap, + projectContents: ProjectContentTreeRoot, + elementPathTrees: ElementPathTrees, + numberOfElementsToInsert: number, ): ReparentTargetForPaste | null { if (selectedViews.length !== 1) { return null } - // These should exist because the check above proves there should be a values there. + // This should exist because the check above proves there should be a value. const targetPath = selectedViews[0]! - const elementPasteEntry = copyData.elementPaste[0]! - const selectedViewAABB = MetadataUtils.getFrameInCanvasCoords(targetPath, metadata) - // if the pasted item's BB is the same size as the selected item's BB - const pastedElementAABB = MetadataUtils.getFrameInCanvasCoords( - elementPasteEntry.originalElementPath, - copyData.originalContextMetadata, - ) - // if the selected item's parent is autolayouted - const parentInstance = MetadataUtils.findElementByElementPath(metadata, EP.parentPath(targetPath)) - - const isSelectedViewParentAutolayouted = MetadataUtils.isFlexLayoutedContainer(parentInstance) + const parentPath = EP.parentPath(targetPath) + const parentElement = withUnderlyingTarget(parentPath, projectContents, null, (_, element) => { + return element + }) - const pastingAbsoluteToAbsolute = - MetadataUtils.isPositionAbsolute( - MetadataUtils.findElementByElementPath(metadata, targetPath), - ) && - MetadataUtils.isPositionAbsolute( - MetadataUtils.findElementByElementPath( - copyData.originalContextMetadata, - elementPasteEntry.originalElementPath, - ), - ) + if (parentElement == null || !isJSXConditionalExpression(parentElement)) { + return null + } - const pastedElementNames = mapDropNulls( - (element) => MetadataUtils.getJSXElementName(element.element), - copyData.elementPaste, - ) + // Check if the target parent is an attribute, + // if so replace the target parent instead of trying to insert into it. + const wrapperFragmentUID = generateUidWithExistingComponents(projectContents) + const conditionalCase = maybeBranchConditionalCase(parentPath, parentElement, targetPath) + if (conditionalCase == null) { + return null + } - const parentPath = EP.parentPath(targetPath) - const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( - parentPath, + const parentInsertionPath = getInsertionPath( + targetPath, + projectContents, metadata, - pastedElementNames, + elementPathTrees, + wrapperFragmentUID, + numberOfElementsToInsert, ) - if ( - rectangleSizesEqual(selectedViewAABB, pastedElementAABB) && - (isSelectedViewParentAutolayouted || pastingAbsoluteToAbsolute) && - targetElementSupportsInsertedElement - ) { - return { - type: 'sibling', - siblingPath: targetPath, - parentPath: childInsertionPath(EP.parentPath(targetPath)), - } + if (parentInsertionPath == null) { + return null } - return null + return { type: 'parent', parentPath: parentInsertionPath } } function pasteNextToSameSizedElement( @@ -643,6 +635,17 @@ export function getTargetParentForOneShotInsertion( return left('Cannot insert component instance into component definition') } + const insertIntoSlotResult = insertIntoSlot( + selectedViews, + metadata, + projectContents, + elementPathTree, + elementsToInsert.length, + ) + if (insertIntoSlotResult != null) { + return right(insertIntoSlotResult) + } + const pasteIntoParentOrGrandparentResult = pasteIntoParentOrGrandparent( elementsToInsert, projectContents, @@ -675,7 +678,13 @@ export function getTargetParentForPaste( return left('Cannot insert component instance into component definition') } - const insertIntoSlotResult = insertIntoSlot(copyData, selectedViews, metadata) + const insertIntoSlotResult = insertIntoSlot( + selectedViews, + metadata, + projectContents, + elementPathTree, + copyData.elementPaste.length, + ) if (insertIntoSlotResult != null) { return right(insertIntoSlotResult) } From b7c544d426bef56fb82031ac887d5c66a555969e Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 13 Oct 2023 12:22:52 +0200 Subject: [PATCH 03/11] minor tweaks, nothing important --- .../strategies/reparent-helpers/reparent-helpers.ts | 5 +++++ editor/src/core/shared/array-utils.ts | 2 +- editor/src/utils/clipboard.ts | 13 ++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts index b22b950cdf8b..96a10fd6dab1 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts @@ -24,6 +24,7 @@ import { isJSXElement, jsExpressionValue, jsxElementNameEquals, + isIntrinsicElement, } from '../../../../../core/shared/element-template' import type { ElementPath, PropertyPath } from '../../../../../core/shared/project-file-types' import type { ProjectContentTreeRoot } from '../../../../assets' @@ -346,6 +347,10 @@ export function isElementRenderedBySameComponent( return false } + if (isIntrinsicElement(element.name)) { + return false + } + const targetElement = MetadataUtils.getJSXElementFromMetadata( metadata, EP.getContainingComponent(targetPath), diff --git a/editor/src/core/shared/array-utils.ts b/editor/src/core/shared/array-utils.ts index c82610b3fed0..e5e1f7ee9b16 100644 --- a/editor/src/core/shared/array-utils.ts +++ b/editor/src/core/shared/array-utils.ts @@ -461,7 +461,7 @@ export function zip(one: A[], other: B[], make: (a: A, b: B) => C): C[] export type NonEmptyArray = [T, ...T[]] export function isNonEmptyArray(array: T[]): array is NonEmptyArray { let [first] = array - return first == null + return first != null } export function isEmptyArray(array: T[]): array is [] { diff --git a/editor/src/utils/clipboard.ts b/editor/src/utils/clipboard.ts index 3ff12de6e3ba..0492dca2ab31 100644 --- a/editor/src/utils/clipboard.ts +++ b/editor/src/utils/clipboard.ts @@ -18,6 +18,7 @@ import { isJSXConditionalExpression, type ElementInstanceMetadataMap, type JSXElementChild, + isJSXElement, } from '../core/shared/element-template' import type { ElementPath } from '../core/shared/project-file-types' import { isParseSuccess, isTextFile } from '../core/shared/project-file-types' @@ -463,13 +464,11 @@ function checkComponentNotInsertedIntoOwnDefinition( ): boolean { const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true) - return elementsToInsert.some((element) => { - if (element.type !== 'JSX_ELEMENT') { - return false - } + const jsxElements = elementsToInsert.filter(isJSXElement) - return isElementRenderedBySameComponent(metadata, parentTarget, element) - }) + return jsxElements.some((element) => + isElementRenderedBySameComponent(metadata, parentTarget, element), + ) } function insertIntoSlot( @@ -631,7 +630,7 @@ export function getTargetParentForOneShotInsertion( elementsToInsert: JSXElementChild[], elementPathTree: ElementPathTrees, ): Either { - if (!checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, elementsToInsert)) { + if (checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, elementsToInsert)) { return left('Cannot insert component instance into component definition') } From 843aeff4c1c313d1d4285b30f092ac524e738d4b Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 13 Oct 2023 12:53:56 +0200 Subject: [PATCH 04/11] fix --- .../post-action-options/post-action-paste.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts index 5992b15051b4..691c10f3424b 100644 --- a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts +++ b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts @@ -7,7 +7,7 @@ import { } from '../../../../core/model/element-template-utils' import { getAllUniqueUids } from '../../../../core/model/get-unique-ids' import { getStoryboardElementPath } from '../../../../core/model/scene-utils' -import { stripNulls, zip } from '../../../../core/shared/array-utils' +import { isNonEmptyArray, stripNulls, zip } from '../../../../core/shared/array-utils' import type { Either } from '../../../../core/shared/either' import { isLeft, left, right } from '../../../../core/shared/either' import * as EP from '../../../../core/shared/element-path' @@ -26,7 +26,7 @@ import type { NodeModules, } from '../../../../core/shared/project-file-types' import { fixUtopiaElement } from '../../../../core/shared/uid-utils' -import { getTargetParentForPaste } from '../../../../utils/clipboard' +import { getTargetParentForPaste, reparentIntoParent } from '../../../../utils/clipboard' import type { ElementPasteWithMetadata, ReparentTargetForPaste } from '../../../../utils/clipboard' import type { IndexPosition } from '../../../../utils/utils' import { absolute, front } from '../../../../utils/utils' @@ -563,19 +563,6 @@ function getTargetParentForPasteHere( const originalPathTree = editor.internalClipboard.elements[0].targetOriginalContextElementPathTrees - const target = getTargetParentForPaste( - editor.projectContents, - editor.selectedViews, - editor.canvas.openFile?.filename ?? null, - editor.jsxMetadata, - { - elementPaste: elementToPaste, - originalContextMetadata: originalMetadata, - originalContextElementPathTrees: originalPathTree, - }, - editor.elementPathTree, - ) - const storyboardPath = getStoryboardElementPath( editor.projectContents, editor.canvas.openFile?.filename ?? null, @@ -585,6 +572,20 @@ function getTargetParentForPasteHere( return left('No storyboard found') } + const target = !isNonEmptyArray(editor.selectedViews) + ? reparentIntoParent(childInsertionPath(storyboardPath)) + : getTargetParentForPaste( + editor.projectContents, + editor.selectedViews, + editor.jsxMetadata, + { + elementPaste: elementToPaste, + originalContextMetadata: originalMetadata, + originalContextElementPathTrees: originalPathTree, + }, + editor.elementPathTree, + ) + if (isLeft(target)) { return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) } From 4695a843cb6e6cae071a87df64d312e53b65e617 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 13 Oct 2023 13:26:39 +0200 Subject: [PATCH 05/11] revert closestSharedAncestor --- editor/src/core/shared/element-path.ts | 32 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/editor/src/core/shared/element-path.ts b/editor/src/core/shared/element-path.ts index 4cef25864d92..9c123efb5214 100644 --- a/editor/src/core/shared/element-path.ts +++ b/editor/src/core/shared/element-path.ts @@ -769,17 +769,22 @@ export function replaceOrDefault( return replaceIfAncestor(path, replaceSearch, replaceWith) ?? path } +// TODO: remove null export function closestSharedAncestor( - l: ElementPath, - r: ElementPath, + l: ElementPath | null, + r: ElementPath | null, includePathsEqual: boolean, -): ElementPath { - const toTargetPath: (p: ElementPath) => ElementPath = includePathsEqual ? identity : parentPath +): ElementPath | null { + const toTargetPath: (p: ElementPath) => ElementPath | null = includePathsEqual + ? identity + : parentPath - const lTarget = toTargetPath(l) - const rTarget = toTargetPath(r) + const lTarget = l == null ? null : toTargetPath(l) + const rTarget = r == null ? null : toTargetPath(r) - if (l === r) { + if (l === null || r === null || lTarget == null || rTarget == null) { + return null + } else if (l === r) { return toTargetPath(l) } else { const fullyMatchedElementPathParts = longestCommonArray( @@ -789,14 +794,13 @@ export function closestSharedAncestor( ) const nextLPart = lTarget.parts[fullyMatchedElementPathParts.length] const nextRPart = rTarget.parts[fullyMatchedElementPathParts.length] - const nextMatchedElementPath = longestCommonArray(nextLPart ?? [], nextRPart ?? []) const totalMatchedParts = nextMatchedElementPath.length > 0 ? [...fullyMatchedElementPathParts, nextMatchedElementPath] : fullyMatchedElementPathParts - return totalMatchedParts.length > 0 ? elementPath(totalMatchedParts) : emptyElementPath + return totalMatchedParts.length > 0 ? elementPath(totalMatchedParts) : null } } @@ -808,7 +812,10 @@ export function getCommonParent( return null } else { const parents = includeSelf ? paths : paths.map(parentPath) - return parents.reduce((l, r) => closestSharedAncestor(l, r, true), parents[0]) + return parents.reduce( + (l, r) => closestSharedAncestor(l, r, true), + parents[0], + ) } } @@ -817,7 +824,10 @@ export function getCommonParentOfNonemptyPathArray( includeSelf: boolean = false, ): ElementPath { const parents = includeSelf ? paths : paths.map(parentPath) - return parents.reduce((l, r) => closestSharedAncestor(l, r, true), parents[0]) + return parents.reduce( + (l, r) => closestSharedAncestor(l, r, true) ?? emptyElementPath, + parents[0], + ) } export interface ElementsTransformResult { From c64bbb230625e2b7287e25f4c1766aca8e50042b Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 13 Oct 2023 15:34:56 +0200 Subject: [PATCH 06/11] boolean confusion --- .../strategies/reparent-helpers/reparent-helpers.ts | 6 +++++- editor/src/utils/clipboard.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts index 96a10fd6dab1..bf884866f354 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts @@ -356,8 +356,12 @@ export function isElementRenderedBySameComponent( EP.getContainingComponent(targetPath), ) + if (targetElement == null) { + return false + } + return ( - (targetElement != null && areElementsInstancesOfTheSameComponent(targetElement, element)) || + areElementsInstancesOfTheSameComponent(targetElement, element) || isElementRenderedBySameComponent(metadata, EP.getContainingComponent(targetPath), element) ) } diff --git a/editor/src/utils/clipboard.ts b/editor/src/utils/clipboard.ts index 0492dca2ab31..1f32a35ae7f1 100644 --- a/editor/src/utils/clipboard.ts +++ b/editor/src/utils/clipboard.ts @@ -673,7 +673,7 @@ export function getTargetParentForPaste( ), copyData.elementPaste, ) - if (!checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, pastedJSXElements)) { + if (checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, pastedJSXElements)) { return left('Cannot insert component instance into component definition') } From 9f67eb7b6cd2e1b1e8f47357f78b2cc99b9cbab6 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 13 Oct 2023 17:05:13 +0200 Subject: [PATCH 07/11] test to insert onto the storyboard --- .../src/components/editor/actions/actions.tsx | 4 --- .../editor/canvas-toolbar.spec.browser2.tsx | 33 ++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 1c03c125c6c7..1304268040f9 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -2173,10 +2173,6 @@ export const UPDATE_FNS = { ) }, OPEN_FLOATING_INSERT_MENU: (action: OpenFloatingInsertMenu, editor: EditorModel): EditorModel => { - if (action.mode.insertMenuMode !== 'closed' && editor.selectedViews.length === 0) { - const showToastAction = showToast(notice(`There are no elements selected`, 'WARNING')) - return UPDATE_FNS.ADD_TOAST(showToastAction, editor) - } return { ...editor, floatingInsertMenu: action.mode, diff --git a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx index a6599a3c8db4..bb6115c9fda5 100644 --- a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx +++ b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx @@ -35,7 +35,7 @@ import { InsertConditionalButtonTestId, InsertMenuButtonTestId, } from './canvas-toolbar' -import { StoryboardFilePath, PlaygroundFilePath } from './store/editor-state' +import { StoryboardFilePath, PlaygroundFilePath, navigatorEntryToKey } from './store/editor-state' function slightlyOffsetWindowPointBecauseVeryWeirdIssue(point: { x: number; y: number }) { // FIXME when running in headless chrome, the result of getBoundingClientRect will be slightly @@ -360,6 +360,37 @@ describe('canvas toolbar', () => { ) }) + it('can insert a div with no element selected via the floating insert menu', async () => { + const editor = await renderTestEditorWithCode( + makeTestProjectCodeWithSnippet(`
+
+
`), + 'await-first-dom-report', + ) + + FOR_TESTS_setNextGeneratedUids(['reserved', 'new-div']) + + await insertViaAddElementPopup(editor, 'div') + + expect(editor.getEditorState().derived.navigatorTargets.map(navigatorEntryToKey)).toEqual([ + 'regular-utopia-storyboard-uid/scene-aaa', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:container', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:container/a3d', + 'regular-utopia-storyboard-uid/new-div', + ]) + }) + it('can insert a span with sample text', async () => { const editor = await renderTestEditorWithCode( makeTestProjectCodeWithSnippet(`
Date: Mon, 16 Oct 2023 10:45:58 +0200 Subject: [PATCH 08/11] comments are not a substitute for type checking --- editor/src/utils/clipboard.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/editor/src/utils/clipboard.ts b/editor/src/utils/clipboard.ts index 1f32a35ae7f1..cce6072641b0 100644 --- a/editor/src/utils/clipboard.ts +++ b/editor/src/utils/clipboard.ts @@ -472,17 +472,13 @@ function checkComponentNotInsertedIntoOwnDefinition( } function insertIntoSlot( - selectedViews: ElementPath[], + selectedViews: NonEmptyArray, metadata: ElementInstanceMetadataMap, projectContents: ProjectContentTreeRoot, elementPathTrees: ElementPathTrees, numberOfElementsToInsert: number, ): ReparentTargetForPaste | null { - if (selectedViews.length !== 1) { - return null - } - // This should exist because the check above proves there should be a value. - const targetPath = selectedViews[0]! + const targetPath = selectedViews[0] const parentPath = EP.parentPath(targetPath) const parentElement = withUnderlyingTarget(parentPath, projectContents, null, (_, element) => { return element @@ -492,8 +488,6 @@ function insertIntoSlot( return null } - // Check if the target parent is an attribute, - // if so replace the target parent instead of trying to insert into it. const wrapperFragmentUID = generateUidWithExistingComponents(projectContents) const conditionalCase = maybeBranchConditionalCase(parentPath, parentElement, targetPath) if (conditionalCase == null) { @@ -518,15 +512,15 @@ function insertIntoSlot( function pasteNextToSameSizedElement( copyData: ParsedCopyData, - selectedViews: ElementPath[], + selectedViews: NonEmptyArray, metadata: ElementInstanceMetadataMap, ): ReparentTargetForPaste | null { - if (selectedViews.length !== 1) { + const targetPath = selectedViews[0] + const elementPasteEntry = copyData.elementPaste.at(0) + if (elementPasteEntry == null) { return null } - // These should exist because the check above proves there should be a values there. - const targetPath = selectedViews[0]! - const elementPasteEntry = copyData.elementPaste[0]! + const selectedViewAABB = MetadataUtils.getFrameInCanvasCoords(targetPath, metadata) // if the pasted item's BB is the same size as the selected item's BB const pastedElementAABB = MetadataUtils.getFrameInCanvasCoords( @@ -622,7 +616,6 @@ function pasteIntoParentOrGrandparent( return null } -// TODO: insert into slot export function getTargetParentForOneShotInsertion( projectContents: ProjectContentTreeRoot, selectedViews: NonEmptyArray, From 91f5a9bcc317f3ea443decd41096bbcadcf3cdf2 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 16 Oct 2023 12:21:30 +0200 Subject: [PATCH 09/11] expect one-shot insertion to be the same as pasting --- .../editor/canvas-toolbar.spec.browser2.tsx | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx index bb6115c9fda5..87e15a2b3edd 100644 --- a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx +++ b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx @@ -625,7 +625,7 @@ export var storyboard = ( }) describe('add element to conditional', () => { - it(`can't add element to the root of a conditional`, async () => { + it(`when the root of a conditional is selected, element is added as a sibling`, async () => { const editor = await renderTestEditorWithCode( makeTestProjectCodeWithSnippet(`
@@ -638,8 +638,6 @@ export var storyboard = ( 'await-first-dom-report', ) - const initialCode = getPrintedUiJsCode(editor.getEditorState()) - const slot = editor.renderedDOM.getByText('Conditional') await mouseClickAtPoint(slot, { x: 5, y: 5 }) @@ -647,10 +645,31 @@ export var storyboard = ( 'utopia-storyboard-uid/scene-aaa/app-entity:container/conditional', ]) - await expectNoAction(editor, () => insertViaAddElementPopup(editor, 'img')) + await insertViaAddElementPopup(editor, 'img') - expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(initialCode) - expectChildrenNotSupportedToastToBePresent(editor) + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( + makeTestProjectCodeWithSnippet(` +
+ { + // @utopia/uid=conditional + [].length === 0 ? null : ( +
"Hello there"
+ ) + } + +
+ `), + ) }) it('add element to true branch of a conditional', async () => { const editor = await renderTestEditorWithCode( From 36bc27a2873c8e5691f757f9abba64be775d5a27 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 16 Oct 2023 13:25:58 +0200 Subject: [PATCH 10/11] move storyboard fallback into parent finding function --- .../post-action-options/post-action-paste.ts | 30 ++++----- .../src/components/editor/insert-callbacks.ts | 22 +++---- editor/src/utils/clipboard.ts | 64 +++++++++---------- 3 files changed, 54 insertions(+), 62 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts index 691c10f3424b..789f44073d14 100644 --- a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts +++ b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts @@ -7,7 +7,7 @@ import { } from '../../../../core/model/element-template-utils' import { getAllUniqueUids } from '../../../../core/model/get-unique-ids' import { getStoryboardElementPath } from '../../../../core/model/scene-utils' -import { isNonEmptyArray, stripNulls, zip } from '../../../../core/shared/array-utils' +import { stripNulls, zip } from '../../../../core/shared/array-utils' import type { Either } from '../../../../core/shared/either' import { isLeft, left, right } from '../../../../core/shared/either' import * as EP from '../../../../core/shared/element-path' @@ -26,7 +26,7 @@ import type { NodeModules, } from '../../../../core/shared/project-file-types' import { fixUtopiaElement } from '../../../../core/shared/uid-utils' -import { getTargetParentForPaste, reparentIntoParent } from '../../../../utils/clipboard' +import { getTargetParentForPaste } from '../../../../utils/clipboard' import type { ElementPasteWithMetadata, ReparentTargetForPaste } from '../../../../utils/clipboard' import type { IndexPosition } from '../../../../utils/utils' import { absolute, front } from '../../../../utils/utils' @@ -45,7 +45,6 @@ import { childInsertionPath, replaceWithElementsWrappedInFragmentBehaviour, } from '../../../editor/store/insertion-path' -import type { RemixRoutingTable } from '../../../editor/store/remix-derived-data' import type { CanvasCommand } from '../../commands/commands' import { foldAndApplyCommandsInner } from '../../commands/commands' import { deleteElement } from '../../commands/delete-element-command' @@ -572,19 +571,18 @@ function getTargetParentForPasteHere( return left('No storyboard found') } - const target = !isNonEmptyArray(editor.selectedViews) - ? reparentIntoParent(childInsertionPath(storyboardPath)) - : getTargetParentForPaste( - editor.projectContents, - editor.selectedViews, - editor.jsxMetadata, - { - elementPaste: elementToPaste, - originalContextMetadata: originalMetadata, - originalContextElementPathTrees: originalPathTree, - }, - editor.elementPathTree, - ) + const target = getTargetParentForPaste( + storyboardPath, + editor.projectContents, + editor.selectedViews, + editor.jsxMetadata, + { + elementPaste: elementToPaste, + originalContextMetadata: originalMetadata, + originalContextElementPathTrees: originalPathTree, + }, + editor.elementPathTree, + ) if (isLeft(target)) { return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) diff --git a/editor/src/components/editor/insert-callbacks.ts b/editor/src/components/editor/insert-callbacks.ts index a8c53bd78edd..4c8140b62f55 100644 --- a/editor/src/components/editor/insert-callbacks.ts +++ b/editor/src/components/editor/insert-callbacks.ts @@ -29,10 +29,7 @@ import type { InsertionSubject } from './editor-modes' import { useDispatch } from './store/dispatch-context' import { Substores, useEditorState, useRefEditorState } from './store/store-hook' import type { InsertMenuItem } from '../canvas/ui/floating-insert-menu' -import { isNonEmptyArray, safeIndex } from '../../core/shared/array-utils' -import type { ElementPath } from '../../core/shared/project-file-types' import { elementToReparent } from '../canvas/canvas-strategies/strategies/reparent-utils' -import { childInsertionPath, getInsertionPath } from './store/insertion-path' import { fixUtopiaElement, generateConsistentUID } from '../../core/shared/uid-utils' import { getAllUniqueUids } from '../../core/model/get-unique-ids' import { assertNever } from '../../core/shared/utils' @@ -45,7 +42,7 @@ import { executeFirstApplicableStrategy } from '../inspector/inspector-strategie import { insertAsAbsoluteStrategy } from './one-shot-insertion-strategies/insert-as-absolute-strategy' import { insertAsStaticStrategy } from './one-shot-insertion-strategies/insert-as-static-strategy' import { getStoryboardElementPath } from '../../core/model/scene-utils' -import { getTargetParentForOneShotInsertion, reparentIntoParent } from '../../utils/clipboard' +import { getTargetParentForOneShotInsertion } from '../../utils/clipboard' function shouldSubjectBeWrappedWithConditional( subject: InsertionSubject, @@ -198,15 +195,14 @@ export function useToInsert(): (elementToInsert: InsertMenuItem | null) => void elementToInsert.value.importsToAdd, ) - const targetParent = !isNonEmptyArray(selectedViewsRef.current) - ? reparentIntoParent(childInsertionPath(storyboardPath)) - : getTargetParentForOneShotInsertion( - projectContentsRef.current, - selectedViewsRef.current, - jsxMetadataRef.current, - [element.element], - elementPathTreeRef.current, - ) + const targetParent = getTargetParentForOneShotInsertion( + storyboardPath, + projectContentsRef.current, + selectedViewsRef.current, + jsxMetadataRef.current, + [element.element], + elementPathTreeRef.current, + ) if (isLeft(targetParent)) { dispatch([ diff --git a/editor/src/utils/clipboard.ts b/editor/src/utils/clipboard.ts index cce6072641b0..da38f84df3ce 100644 --- a/editor/src/utils/clipboard.ts +++ b/editor/src/utils/clipboard.ts @@ -144,20 +144,18 @@ function getJSXElementPasteActions( return [] } - const target = !isNonEmptyArray(selectedViews) - ? reparentIntoParent(childInsertionPath(storyboardPath)) - : getTargetParentForPaste( - editor.projectContents, - selectedViews, - editor.jsxMetadata, - { - elementPaste: copyDataToUse.elements, - originalContextMetadata: copyDataToUse.targetOriginalContextMetadata, - originalContextElementPathTrees: - clipboardFirstEntry.targetOriginalContextElementPathTrees, - }, - editor.elementPathTree, - ) + const target = getTargetParentForPaste( + storyboardPath, + editor.projectContents, + selectedViews, + editor.jsxMetadata, + { + elementPaste: copyDataToUse.elements, + originalContextMetadata: copyDataToUse.targetOriginalContextMetadata, + originalContextElementPathTrees: clipboardFirstEntry.targetOriginalContextElementPathTrees, + }, + editor.elementPathTree, + ) if (isLeft(target)) { return [ @@ -221,15 +219,14 @@ function getFilePasteActions( return [] } - const target = !isNonEmptyArray(selectedViews) - ? reparentIntoParent(childInsertionPath(storyboardPath)) - : getTargetParentForPaste( - projectContents, - selectedViews, - componentMetadata, - { elementPaste: [], originalContextMetadata: {}, originalContextElementPathTrees: {} }, // TODO: get rid of this when refactoring pasting images - elementPathTree, - ) + const target = getTargetParentForPaste( + storyboardPath, + projectContents, + selectedViews, + componentMetadata, + { elementPaste: [], originalContextMetadata: {}, originalContextElementPathTrees: {} }, // TODO: get rid of this when refactoring pasting images + elementPathTree, + ) if (isLeft(target)) { return [ @@ -445,14 +442,6 @@ export type ReparentTargetForPaste = } | { type: 'parent'; parentPath: InsertionPath } -export const reparentIntoParent = ( - parentPath: InsertionPath, -): Either => - right({ - type: 'parent', - parentPath: parentPath, - }) - type PasteParentNotFoundError = | 'Cannot find a suitable parent' | 'Cannot insert component instance into component definition' @@ -617,12 +606,17 @@ function pasteIntoParentOrGrandparent( } export function getTargetParentForOneShotInsertion( + storyboardPath: ElementPath, projectContents: ProjectContentTreeRoot, - selectedViews: NonEmptyArray, + selectedViews: Array, metadata: ElementInstanceMetadataMap, elementsToInsert: JSXElementChild[], elementPathTree: ElementPathTrees, ): Either { + if (!isNonEmptyArray(selectedViews)) { + return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) + } + if (checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, elementsToInsert)) { return left('Cannot insert component instance into component definition') } @@ -652,12 +646,16 @@ export function getTargetParentForOneShotInsertion( } export function getTargetParentForPaste( + storyboardPath: ElementPath, projectContents: ProjectContentTreeRoot, - selectedViews: NonEmptyArray, + selectedViews: Array, metadata: ElementInstanceMetadataMap, copyData: ParsedCopyData, elementPathTree: ElementPathTrees, ): Either { + if (!isNonEmptyArray(selectedViews)) { + return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) + } const pastedJSXElements = mapDropNulls( (p) => MetadataUtils.getJSXElementFromMetadata( From 9493a79e4a6059ba8feadd1c9887b67afaf7f244 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 16 Oct 2023 13:37:56 +0200 Subject: [PATCH 11/11] move target finding functions into reparent-utils --- .../post-action-options/post-action-paste.ts | 15 +- .../reparent-helpers/reparent-helpers.ts | 21 +- .../strategies/reparent-utils.ts | 309 ++++++++++++++++-- .../src/components/editor/insert-callbacks.ts | 6 +- .../components/editor/store/editor-state.ts | 13 +- editor/src/utils/clipboard.ts | 305 +---------------- 6 files changed, 320 insertions(+), 349 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts index 789f44073d14..3c00d38d0254 100644 --- a/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts +++ b/editor/src/components/canvas/canvas-strategies/post-action-options/post-action-paste.ts @@ -26,8 +26,7 @@ import type { NodeModules, } from '../../../../core/shared/project-file-types' import { fixUtopiaElement } from '../../../../core/shared/uid-utils' -import { getTargetParentForPaste } from '../../../../utils/clipboard' -import type { ElementPasteWithMetadata, ReparentTargetForPaste } from '../../../../utils/clipboard' +import type { ElementPasteWithMetadata } from '../../../../utils/clipboard' import type { IndexPosition } from '../../../../utils/utils' import { absolute, front } from '../../../../utils/utils' import type { ProjectContentTreeRoot } from '../../../assets' @@ -63,8 +62,16 @@ import { } from '../strategies/reparent-helpers/reparent-property-changes' import { reparentStrategyForPaste } from '../strategies/reparent-helpers/reparent-strategy-helpers' import type { ReparentStrategy } from '../strategies/reparent-helpers/reparent-strategy-helpers' -import type { ElementToReparent, PathToReparent } from '../strategies/reparent-utils' -import { elementToReparent, getReparentOutcomeMultiselect } from '../strategies/reparent-utils' +import type { + ElementToReparent, + PathToReparent, + ReparentTargetForPaste, +} from '../strategies/reparent-utils' +import { + elementToReparent, + getReparentOutcomeMultiselect, + getTargetParentForPaste, +} from '../strategies/reparent-utils' import { adjustIntendedCoordinatesForGroups, collectGroupTrueUp } from './navigator-reparent' import type { PostActionChoice } from './post-action-options' diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts index bf884866f354..006483b6c598 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers.ts @@ -3,12 +3,9 @@ import { getConditionalActiveCase, maybeBranchConditionalCase, } from '../../../../../core/model/conditionals' -import { - MetadataUtils, - getSimpleAttributeAtPath, -} from '../../../../../core/model/element-metadata-utils' +import { MetadataUtils } from '../../../../../core/model/element-metadata-utils' import type { Either } from '../../../../../core/shared/either' -import { foldEither, isLeft, left, right } from '../../../../../core/shared/either' +import { foldEither, left, right } from '../../../../../core/shared/either' import * as EP from '../../../../../core/shared/element-path' import type { ElementInstanceMetadata, @@ -26,7 +23,7 @@ import { jsxElementNameEquals, isIntrinsicElement, } from '../../../../../core/shared/element-template' -import type { ElementPath, PropertyPath } from '../../../../../core/shared/project-file-types' +import type { ElementPath } from '../../../../../core/shared/project-file-types' import type { ProjectContentTreeRoot } from '../../../../assets' import type { AllElementProps, @@ -46,10 +43,7 @@ import { strategyApplicationResult } from '../../canvas-strategy-types' import * as PP from '../../../../../core/shared/property-path' import type { ValueAtPath } from '../../../../../core/shared/jsx-attributes' import { setJSXValuesAtPaths } from '../../../../../core/shared/jsx-attributes' -import type { - ElementPasteWithMetadata, - ReparentTargetForPaste, -} from '../../../../../utils/clipboard' +import type { ElementPasteWithMetadata } from '../../../../../utils/clipboard' import type { ElementPaste } from '../../../../editor/action-types' import { eitherRight, @@ -57,7 +51,6 @@ import { traverseArray, } from '../../../../../core/shared/optics/optic-creators' import { modify, set } from '../../../../../core/shared/optics/optic-utilities' -import type { IndexPosition } from '../../../../../utils/utils' import Utils from '../../../../../utils/utils' import type { CanvasPoint } from '../../../../../core/shared/math-utils' import { @@ -68,20 +61,16 @@ import { canvasPoint, roundTo, zeroCanvasRect, - zeroRectangle, zeroRectIfNullOrInfinity, roundPointToNearestWhole, } from '../../../../../core/shared/math-utils' import type { MetadataSnapshots } from './reparent-property-strategies' -import type { BuiltInDependencies } from '../../../../../core/es-modules/package-manager/built-in-dependencies-list' import type { ElementPathTrees } from '../../../../../core/shared/element-path-tree' import type { CanvasCommand } from '../../../commands/commands' -import type { ToReparent } from '../reparent-utils' -import type { StaticReparentTarget } from './reparent-strategy-helpers' import { mapDropNulls } from '../../../../../core/shared/array-utils' import { treatElementAsFragmentLike } from '../fragment-like-helpers' -import { optionalMap } from '../../../../../core/shared/optional-utils' import { setProperty } from '../../../commands/set-property-command' +import type { ReparentTargetForPaste } from '../reparent-utils' export function isAllowedToReparent( projectContents: ProjectContentTreeRoot, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-utils.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-utils.ts index c2e8392d6395..a211655b8014 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-utils.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-utils.ts @@ -1,9 +1,4 @@ import type { ProjectContentTreeRoot } from '../../../assets' -import { - addImport, - emptyImports, - mergeImports, -} from '../../../../core/workers/common/project-file-utils' import type { AllElementProps } from '../../../editor/store/editor-state' import { withUnderlyingTarget } from '../../../editor/store/editor-state' import type { ElementPath, Imports, NodeModules } from '../../../../core/shared/project-file-types' @@ -13,18 +8,9 @@ import type { ElementInstanceMetadataMap, JSXElementChild, } from '../../../../core/shared/element-template' -import { - isIntrinsicElement, - isJSXElement, - JSXElement, - walkElement, -} from '../../../../core/shared/element-template' +import { isJSXConditionalExpression, isJSXElement } from '../../../../core/shared/element-template' import * as EP from '../../../../core/shared/element-path' -import { - getImportsFor, - getRequiredImportsForElement, - importedFromWhere, -} from '../../../editor/import-utils' +import { getRequiredImportsForElement } from '../../../editor/import-utils' import { forceNotNull } from '../../../../core/shared/optional-utils' import { addImportsToFile } from '../../commands/add-imports-to-file-command' import type { BuiltInDependencies } from '../../../../core/es-modules/package-manager/built-in-dependencies-list' @@ -34,7 +20,6 @@ import { getStoryboardElementPath } from '../../../../core/model/scene-utils' import { generateUidWithExistingComponents } from '../../../../core/model/element-template-utils' import { addElement } from '../../commands/add-element-command' import type { CustomStrategyState, InteractionCanvasState } from '../canvas-strategy-types' -import { InteractionLifecycle } from '../canvas-strategy-types' import { duplicateElement } from '../../commands/duplicate-element-command' import { wildcardPatch } from '../../commands/wildcard-patch-command' import { hideInNavigatorCommand } from '../../commands/hide-in-navigator-command' @@ -43,7 +28,7 @@ import type { InsertionPath } from '../../../editor/store/insertion-path' import { childInsertionPath, getElementPathFromInsertionPath, - isChildInsertionPath, + getInsertionPath, } from '../../../editor/store/insertion-path' import { getUtopiaID } from '../../../../core/shared/uid-utils' import type { IndexPosition } from '../../../../utils/utils' @@ -51,7 +36,15 @@ import { fastForEach } from '../../../../core/shared/utils' import { addElements } from '../../commands/add-elements-command' import type { ElementPathTrees } from '../../../../core/shared/element-path-tree' import { getRequiredGroupTrueUps } from '../../commands/queue-group-true-up-command' -import type { RemixRoutingTable } from '../../../editor/store/remix-derived-data' +import type { Either } from '../../../../core/shared/either' +import { left, right } from '../../../../core/shared/either' +import { maybeBranchConditionalCase } from '../../../../core/model/conditionals' +import type { NonEmptyArray } from '../../../../core/shared/array-utils' +import { mapDropNulls, isNonEmptyArray } from '../../../../core/shared/array-utils' +import type { MaybeInfinityCanvasRectangle } from '../../../../core/shared/math-utils' +import { isInfinityRectangle } from '../../../../core/shared/math-utils' +import { isElementRenderedBySameComponent } from './reparent-helpers/reparent-helpers' +import type { ParsedCopyData } from '../../../../utils/clipboard' interface GetReparentOutcomeResult { commands: Array @@ -293,3 +286,281 @@ export function placeholderCloneCommands( }) return { commands: commands, duplicatedElementNewUids: duplicatedElementNewUids } } + +function rectangleSizesEqual( + a: MaybeInfinityCanvasRectangle | null, + b: MaybeInfinityCanvasRectangle | null, +): boolean { + if (a == null || b == null || isInfinityRectangle(a) || isInfinityRectangle(b)) { + return false + } + + return a.height === b.height && a.width === b.width +} + +export type ReparentTargetForPaste = + | { + type: 'sibling' + siblingPath: ElementPath + parentPath: InsertionPath + } + | { type: 'parent'; parentPath: InsertionPath } + +type PasteParentNotFoundError = + | 'Cannot find a suitable parent' + | 'Cannot insert component instance into component definition' + +function checkComponentNotInsertedIntoOwnDefinition( + selectedViews: NonEmptyArray, + metadata: ElementInstanceMetadataMap, + elementsToInsert: JSXElementChild[], +): boolean { + const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true) + + const jsxElements = elementsToInsert.filter(isJSXElement) + + return jsxElements.some((element) => + isElementRenderedBySameComponent(metadata, parentTarget, element), + ) +} + +function insertIntoSlot( + selectedViews: NonEmptyArray, + metadata: ElementInstanceMetadataMap, + projectContents: ProjectContentTreeRoot, + elementPathTrees: ElementPathTrees, + numberOfElementsToInsert: number, +): ReparentTargetForPaste | null { + const targetPath = selectedViews[0] + const parentPath = EP.parentPath(targetPath) + const parentElement = withUnderlyingTarget(parentPath, projectContents, null, (_, element) => { + return element + }) + + if (parentElement == null || !isJSXConditionalExpression(parentElement)) { + return null + } + + const wrapperFragmentUID = generateUidWithExistingComponents(projectContents) + const conditionalCase = maybeBranchConditionalCase(parentPath, parentElement, targetPath) + if (conditionalCase == null) { + return null + } + + const parentInsertionPath = getInsertionPath( + targetPath, + projectContents, + metadata, + elementPathTrees, + wrapperFragmentUID, + numberOfElementsToInsert, + ) + + if (parentInsertionPath == null) { + return null + } + + return { type: 'parent', parentPath: parentInsertionPath } +} + +function pasteNextToSameSizedElement( + copyData: ParsedCopyData, + selectedViews: NonEmptyArray, + metadata: ElementInstanceMetadataMap, +): ReparentTargetForPaste | null { + const targetPath = selectedViews[0] + const elementPasteEntry = copyData.elementPaste.at(0) + if (elementPasteEntry == null) { + return null + } + + const selectedViewAABB = MetadataUtils.getFrameInCanvasCoords(targetPath, metadata) + // if the pasted item's BB is the same size as the selected item's BB + const pastedElementAABB = MetadataUtils.getFrameInCanvasCoords( + elementPasteEntry.originalElementPath, + copyData.originalContextMetadata, + ) + // if the selected item's parent is autolayouted + const parentInstance = MetadataUtils.findElementByElementPath(metadata, EP.parentPath(targetPath)) + + const isSelectedViewParentAutolayouted = MetadataUtils.isFlexLayoutedContainer(parentInstance) + + const pastingAbsoluteToAbsolute = + MetadataUtils.isPositionAbsolute( + MetadataUtils.findElementByElementPath(metadata, targetPath), + ) && + MetadataUtils.isPositionAbsolute( + MetadataUtils.findElementByElementPath( + copyData.originalContextMetadata, + elementPasteEntry.originalElementPath, + ), + ) + + const pastedElementNames = mapDropNulls( + (element) => MetadataUtils.getJSXElementName(element.element), + copyData.elementPaste, + ) + + const parentPath = EP.parentPath(targetPath) + const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( + parentPath, + metadata, + pastedElementNames, + ) + + if ( + rectangleSizesEqual(selectedViewAABB, pastedElementAABB) && + (isSelectedViewParentAutolayouted || pastingAbsoluteToAbsolute) && + targetElementSupportsInsertedElement + ) { + return { + type: 'sibling', + siblingPath: targetPath, + parentPath: childInsertionPath(EP.parentPath(targetPath)), + } + } + return null +} + +function pasteIntoParentOrGrandparent( + elementsToInsert: JSXElementChild[], + projectContents: ProjectContentTreeRoot, + selectedViews: NonEmptyArray, + metadata: ElementInstanceMetadataMap, + elementPathTree: ElementPathTrees, +): ReparentTargetForPaste | null { + const pastedElementNames = mapDropNulls( + (element) => (element.type === 'JSX_ELEMENT' ? element.name : null), + elementsToInsert, + ) + + const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true) + + // paste into parent + const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( + parentTarget, + metadata, + pastedElementNames, + ) + if ( + MetadataUtils.targetSupportsChildren( + projectContents, + metadata, + parentTarget, + elementPathTree, + ) && + targetElementSupportsInsertedElement + ) { + return { type: 'parent', parentPath: childInsertionPath(parentTarget) } + } + + // paste into parent of parent + const parentOfSelected = EP.parentPath(parentTarget) + if ( + MetadataUtils.targetSupportsChildren( + projectContents, + metadata, + parentOfSelected, + elementPathTree, + ) + ) { + return { type: 'parent', parentPath: childInsertionPath(parentOfSelected) } + } + return null +} + +export function getTargetParentForOneShotInsertion( + storyboardPath: ElementPath, + projectContents: ProjectContentTreeRoot, + selectedViews: Array, + metadata: ElementInstanceMetadataMap, + elementsToInsert: JSXElementChild[], + elementPathTree: ElementPathTrees, +): Either { + if (!isNonEmptyArray(selectedViews)) { + return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) + } + + if (checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, elementsToInsert)) { + return left('Cannot insert component instance into component definition') + } + + const insertIntoSlotResult = insertIntoSlot( + selectedViews, + metadata, + projectContents, + elementPathTree, + elementsToInsert.length, + ) + if (insertIntoSlotResult != null) { + return right(insertIntoSlotResult) + } + + const pasteIntoParentOrGrandparentResult = pasteIntoParentOrGrandparent( + elementsToInsert, + projectContents, + selectedViews, + metadata, + elementPathTree, + ) + if (pasteIntoParentOrGrandparentResult != null) { + return right(pasteIntoParentOrGrandparentResult) + } + return left('Cannot find a suitable parent') +} + +export function getTargetParentForPaste( + storyboardPath: ElementPath, + projectContents: ProjectContentTreeRoot, + selectedViews: Array, + metadata: ElementInstanceMetadataMap, + copyData: ParsedCopyData, + elementPathTree: ElementPathTrees, +): Either { + if (!isNonEmptyArray(selectedViews)) { + return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) + } + const pastedJSXElements = mapDropNulls( + (p) => + MetadataUtils.getJSXElementFromMetadata( + copyData.originalContextMetadata, + p.originalElementPath, + ), + copyData.elementPaste, + ) + if (checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, pastedJSXElements)) { + return left('Cannot insert component instance into component definition') + } + + const insertIntoSlotResult = insertIntoSlot( + selectedViews, + metadata, + projectContents, + elementPathTree, + copyData.elementPaste.length, + ) + if (insertIntoSlotResult != null) { + return right(insertIntoSlotResult) + } + + const pasteNextToSameSizedElementResult = pasteNextToSameSizedElement( + copyData, + selectedViews, + metadata, + ) + if (pasteNextToSameSizedElementResult != null) { + return right(pasteNextToSameSizedElementResult) + } + + const pasteIntoParentOrGrandparentResult = pasteIntoParentOrGrandparent( + copyData.elementPaste.map((e) => e.element), + projectContents, + selectedViews, + metadata, + elementPathTree, + ) + if (pasteIntoParentOrGrandparentResult != null) { + return right(pasteIntoParentOrGrandparentResult) + } + return left('Cannot find a suitable parent') +} diff --git a/editor/src/components/editor/insert-callbacks.ts b/editor/src/components/editor/insert-callbacks.ts index 4c8140b62f55..654727f42fcf 100644 --- a/editor/src/components/editor/insert-callbacks.ts +++ b/editor/src/components/editor/insert-callbacks.ts @@ -29,7 +29,10 @@ import type { InsertionSubject } from './editor-modes' import { useDispatch } from './store/dispatch-context' import { Substores, useEditorState, useRefEditorState } from './store/store-hook' import type { InsertMenuItem } from '../canvas/ui/floating-insert-menu' -import { elementToReparent } from '../canvas/canvas-strategies/strategies/reparent-utils' +import { + elementToReparent, + getTargetParentForOneShotInsertion, +} from '../canvas/canvas-strategies/strategies/reparent-utils' import { fixUtopiaElement, generateConsistentUID } from '../../core/shared/uid-utils' import { getAllUniqueUids } from '../../core/model/get-unique-ids' import { assertNever } from '../../core/shared/utils' @@ -42,7 +45,6 @@ import { executeFirstApplicableStrategy } from '../inspector/inspector-strategie import { insertAsAbsoluteStrategy } from './one-shot-insertion-strategies/insert-as-absolute-strategy' import { insertAsStaticStrategy } from './one-shot-insertion-strategies/insert-as-static-strategy' import { getStoryboardElementPath } from '../../core/model/scene-utils' -import { getTargetParentForOneShotInsertion } from '../../utils/clipboard' function shouldSubjectBeWrappedWithConditional( subject: InsertionSubject, diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 237a0f562c1e..4f3d2db80eec 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -162,11 +162,7 @@ import type { GuidelineWithSnappingVectorAndPointsOfRelevance } from '../../canv import type { PersistenceMachine } from '../persistence/persistence' import type { ThemeSubstate } from './store-hook-substore-types' import type { ElementPathTrees } from '../../../core/shared/element-path-tree' -import type { - CopyData, - ElementPasteWithMetadata, - ReparentTargetForPaste, -} from '../../../utils/clipboard' +import type { CopyData, ElementPasteWithMetadata } from '../../../utils/clipboard' import type { InvalidGroupState } from '../../canvas/canvas-strategies/strategies/group-helpers' import { getGroupChildStateWithGroupMetadata, @@ -174,13 +170,10 @@ import { isInvalidGroupState, treatElementAsGroupLikeFromMetadata, } from '../../canvas/canvas-strategies/strategies/group-helpers' -import type { - RemixDerivedData, - RemixDerivedDataFactory, - RemixRoutingTable, -} from './remix-derived-data' +import type { RemixDerivedData, RemixDerivedDataFactory } from './remix-derived-data' import type { ProjectServerState } from './project-server-state' import { GridMenuWidth } from '../../canvas/grid-panels-state' +import type { ReparentTargetForPaste } from '../../canvas/canvas-strategies/strategies/reparent-utils' const ObjectPathImmutable: any = OPI diff --git a/editor/src/utils/clipboard.ts b/editor/src/utils/clipboard.ts index da38f84df3ce..3d8d11bc8a13 100644 --- a/editor/src/utils/clipboard.ts +++ b/editor/src/utils/clipboard.ts @@ -9,24 +9,18 @@ import type { import { getElementFromProjectContents, getOpenUIJSFileKey, - withUnderlyingTarget, } from '../components/editor/store/editor-state' import { getFrameAndMultiplier } from '../components/images' import * as EP from '../core/shared/element-path' import { MetadataUtils } from '../core/model/element-metadata-utils' -import { - isJSXConditionalExpression, - type ElementInstanceMetadataMap, - type JSXElementChild, - isJSXElement, -} from '../core/shared/element-template' +import { type ElementInstanceMetadataMap } from '../core/shared/element-template' import type { ElementPath } from '../core/shared/project-file-types' import { isParseSuccess, isTextFile } from '../core/shared/project-file-types' import type { PasteResult } from './clipboard-utils' import { extractFiles, extractUtopiaDataFromClipboardData } from './clipboard-utils' import Utils from './utils' import type { FileResult, ImageResult } from '../core/shared/file-utils' -import type { CanvasPoint, MaybeInfinityCanvasRectangle } from '../core/shared/math-utils' +import type { CanvasPoint } from '../core/shared/math-utils' import { isInfinityRectangle } from '../core/shared/math-utils' import { fastForEach } from '../core/shared/utils' import urljoin from 'url-join' @@ -36,29 +30,22 @@ import { normalisePathSuccessOrThrowError, normalisePathToUnderlyingTarget, } from '../components/custom-code/code-file' -import type { NonEmptyArray } from '../core/shared/array-utils' -import { mapDropNulls, isNonEmptyArray } from '../core/shared/array-utils' +import { mapDropNulls } from '../core/shared/array-utils' import ClipboardPolyfill from 'clipboard-polyfill' import { mapValues, pick } from '../core/shared/object-utils' import { getStoryboardElementPath } from '../core/model/scene-utils' import { getRequiredImportsForElement } from '../components/editor/import-utils' import type { BuiltInDependencies } from '../core/es-modules/package-manager/built-in-dependencies-list' import type { InsertionPath } from '../components/editor/store/insertion-path' -import { childInsertionPath, getInsertionPath } from '../components/editor/store/insertion-path' import type { ElementPathTrees } from '../core/shared/element-path-tree' -import { - isElementRenderedBySameComponent, - replaceJSXElementCopyData, -} from '../components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers' +import { replaceJSXElementCopyData } from '../components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-helpers' import { PropsPreservedPastePostActionChoice, PropsReplacedPastePostActionChoice, } from '../components/canvas/canvas-strategies/post-action-options/post-action-paste' -import type { Either } from '../core/shared/either' -import { isLeft, left, right } from '../core/shared/either' +import { isLeft } from '../core/shared/either' import { notice } from '../components/common/notice' -import { maybeBranchConditionalCase } from '../core/model/conditionals' -import { generateUidWithExistingComponents } from '../core/model/element-template-utils' +import { getTargetParentForPaste } from '../components/canvas/canvas-strategies/strategies/reparent-utils' export interface ElementPasteWithMetadata { elements: ElementPaste[] @@ -72,7 +59,7 @@ export interface CopyData { originalAllElementProps: AllElementProps } -interface ParsedCopyData { +export interface ParsedCopyData { elementPaste: ElementPaste[] originalContextMetadata: ElementInstanceMetadataMap originalContextElementPathTrees: ElementPathTrees @@ -422,281 +409,3 @@ export function filterMetadataForCopy( ) return filteredMetadataWithoutProps } - -function rectangleSizesEqual( - a: MaybeInfinityCanvasRectangle | null, - b: MaybeInfinityCanvasRectangle | null, -): boolean { - if (a == null || b == null || isInfinityRectangle(a) || isInfinityRectangle(b)) { - return false - } - - return a.height === b.height && a.width === b.width -} - -export type ReparentTargetForPaste = - | { - type: 'sibling' - siblingPath: ElementPath - parentPath: InsertionPath - } - | { type: 'parent'; parentPath: InsertionPath } - -type PasteParentNotFoundError = - | 'Cannot find a suitable parent' - | 'Cannot insert component instance into component definition' - -function checkComponentNotInsertedIntoOwnDefinition( - selectedViews: NonEmptyArray, - metadata: ElementInstanceMetadataMap, - elementsToInsert: JSXElementChild[], -): boolean { - const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true) - - const jsxElements = elementsToInsert.filter(isJSXElement) - - return jsxElements.some((element) => - isElementRenderedBySameComponent(metadata, parentTarget, element), - ) -} - -function insertIntoSlot( - selectedViews: NonEmptyArray, - metadata: ElementInstanceMetadataMap, - projectContents: ProjectContentTreeRoot, - elementPathTrees: ElementPathTrees, - numberOfElementsToInsert: number, -): ReparentTargetForPaste | null { - const targetPath = selectedViews[0] - const parentPath = EP.parentPath(targetPath) - const parentElement = withUnderlyingTarget(parentPath, projectContents, null, (_, element) => { - return element - }) - - if (parentElement == null || !isJSXConditionalExpression(parentElement)) { - return null - } - - const wrapperFragmentUID = generateUidWithExistingComponents(projectContents) - const conditionalCase = maybeBranchConditionalCase(parentPath, parentElement, targetPath) - if (conditionalCase == null) { - return null - } - - const parentInsertionPath = getInsertionPath( - targetPath, - projectContents, - metadata, - elementPathTrees, - wrapperFragmentUID, - numberOfElementsToInsert, - ) - - if (parentInsertionPath == null) { - return null - } - - return { type: 'parent', parentPath: parentInsertionPath } -} - -function pasteNextToSameSizedElement( - copyData: ParsedCopyData, - selectedViews: NonEmptyArray, - metadata: ElementInstanceMetadataMap, -): ReparentTargetForPaste | null { - const targetPath = selectedViews[0] - const elementPasteEntry = copyData.elementPaste.at(0) - if (elementPasteEntry == null) { - return null - } - - const selectedViewAABB = MetadataUtils.getFrameInCanvasCoords(targetPath, metadata) - // if the pasted item's BB is the same size as the selected item's BB - const pastedElementAABB = MetadataUtils.getFrameInCanvasCoords( - elementPasteEntry.originalElementPath, - copyData.originalContextMetadata, - ) - // if the selected item's parent is autolayouted - const parentInstance = MetadataUtils.findElementByElementPath(metadata, EP.parentPath(targetPath)) - - const isSelectedViewParentAutolayouted = MetadataUtils.isFlexLayoutedContainer(parentInstance) - - const pastingAbsoluteToAbsolute = - MetadataUtils.isPositionAbsolute( - MetadataUtils.findElementByElementPath(metadata, targetPath), - ) && - MetadataUtils.isPositionAbsolute( - MetadataUtils.findElementByElementPath( - copyData.originalContextMetadata, - elementPasteEntry.originalElementPath, - ), - ) - - const pastedElementNames = mapDropNulls( - (element) => MetadataUtils.getJSXElementName(element.element), - copyData.elementPaste, - ) - - const parentPath = EP.parentPath(targetPath) - const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( - parentPath, - metadata, - pastedElementNames, - ) - - if ( - rectangleSizesEqual(selectedViewAABB, pastedElementAABB) && - (isSelectedViewParentAutolayouted || pastingAbsoluteToAbsolute) && - targetElementSupportsInsertedElement - ) { - return { - type: 'sibling', - siblingPath: targetPath, - parentPath: childInsertionPath(EP.parentPath(targetPath)), - } - } - return null -} - -function pasteIntoParentOrGrandparent( - elementsToInsert: JSXElementChild[], - projectContents: ProjectContentTreeRoot, - selectedViews: NonEmptyArray, - metadata: ElementInstanceMetadataMap, - elementPathTree: ElementPathTrees, -): ReparentTargetForPaste | null { - const pastedElementNames = mapDropNulls( - (element) => (element.type === 'JSX_ELEMENT' ? element.name : null), - elementsToInsert, - ) - - const parentTarget = EP.getCommonParentOfNonemptyPathArray(selectedViews, true) - - // paste into parent - const targetElementSupportsInsertedElement = MetadataUtils.canInsertElementsToTargetText( - parentTarget, - metadata, - pastedElementNames, - ) - if ( - MetadataUtils.targetSupportsChildren( - projectContents, - metadata, - parentTarget, - elementPathTree, - ) && - targetElementSupportsInsertedElement - ) { - return { type: 'parent', parentPath: childInsertionPath(parentTarget) } - } - - // paste into parent of parent - const parentOfSelected = EP.parentPath(parentTarget) - if ( - MetadataUtils.targetSupportsChildren( - projectContents, - metadata, - parentOfSelected, - elementPathTree, - ) - ) { - return { type: 'parent', parentPath: childInsertionPath(parentOfSelected) } - } - return null -} - -export function getTargetParentForOneShotInsertion( - storyboardPath: ElementPath, - projectContents: ProjectContentTreeRoot, - selectedViews: Array, - metadata: ElementInstanceMetadataMap, - elementsToInsert: JSXElementChild[], - elementPathTree: ElementPathTrees, -): Either { - if (!isNonEmptyArray(selectedViews)) { - return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) - } - - if (checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, elementsToInsert)) { - return left('Cannot insert component instance into component definition') - } - - const insertIntoSlotResult = insertIntoSlot( - selectedViews, - metadata, - projectContents, - elementPathTree, - elementsToInsert.length, - ) - if (insertIntoSlotResult != null) { - return right(insertIntoSlotResult) - } - - const pasteIntoParentOrGrandparentResult = pasteIntoParentOrGrandparent( - elementsToInsert, - projectContents, - selectedViews, - metadata, - elementPathTree, - ) - if (pasteIntoParentOrGrandparentResult != null) { - return right(pasteIntoParentOrGrandparentResult) - } - return left('Cannot find a suitable parent') -} - -export function getTargetParentForPaste( - storyboardPath: ElementPath, - projectContents: ProjectContentTreeRoot, - selectedViews: Array, - metadata: ElementInstanceMetadataMap, - copyData: ParsedCopyData, - elementPathTree: ElementPathTrees, -): Either { - if (!isNonEmptyArray(selectedViews)) { - return right({ type: 'parent', parentPath: childInsertionPath(storyboardPath) }) - } - const pastedJSXElements = mapDropNulls( - (p) => - MetadataUtils.getJSXElementFromMetadata( - copyData.originalContextMetadata, - p.originalElementPath, - ), - copyData.elementPaste, - ) - if (checkComponentNotInsertedIntoOwnDefinition(selectedViews, metadata, pastedJSXElements)) { - return left('Cannot insert component instance into component definition') - } - - const insertIntoSlotResult = insertIntoSlot( - selectedViews, - metadata, - projectContents, - elementPathTree, - copyData.elementPaste.length, - ) - if (insertIntoSlotResult != null) { - return right(insertIntoSlotResult) - } - - const pasteNextToSameSizedElementResult = pasteNextToSameSizedElement( - copyData, - selectedViews, - metadata, - ) - if (pasteNextToSameSizedElementResult != null) { - return right(pasteNextToSameSizedElementResult) - } - - const pasteIntoParentOrGrandparentResult = pasteIntoParentOrGrandparent( - copyData.elementPaste.map((e) => e.element), - projectContents, - selectedViews, - metadata, - elementPathTree, - ) - if (pasteIntoParentOrGrandparentResult != null) { - return right(pasteIntoParentOrGrandparentResult) - } - return left('Cannot find a suitable parent') -}