diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx index 63787cf8f744..51a981bd1c45 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.tsx @@ -1,11 +1,15 @@ +import type { BuiltInDependencies } from '../../../../core/es-modules/package-manager/built-in-dependencies-list' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import { mapDropNulls } from '../../../../core/shared/array-utils' import * as EP from '../../../../core/shared/element-path' import type { ElementPathTrees } from '../../../../core/shared/element-path-tree' import type { ElementInstanceMetadataMap } from '../../../../core/shared/element-template' -import type { ElementPath } from '../../../../core/shared/project-file-types' +import type { ElementPath, NodeModules } from '../../../../core/shared/project-file-types' import * as PP from '../../../../core/shared/property-path' +import type { IndexPosition } from '../../../../utils/utils' +import type { ProjectContentTreeRoot } from '../../../assets' import type { AllElementProps } from '../../../editor/store/editor-state' +import type { InsertionPath } from '../../../editor/store/insertion-path' import { CSSCursor } from '../../canvas-types' import type { CanvasCommand } from '../../commands/commands' import { setCursorCommand } from '../../commands/set-cursor-command' @@ -36,6 +40,7 @@ import { treatElementAsFragmentLike, } from './fragment-like-helpers' import { ifAllowedToReparent, isAllowedToReparent } from './reparent-helpers/reparent-helpers' +import type { ForcePins } from './reparent-helpers/reparent-property-changes' import { getAbsoluteReparentPropertyChanges } from './reparent-helpers/reparent-property-changes' import type { ReparentTarget } from './reparent-helpers/reparent-strategy-helpers' import { getReparentOutcome, pathToReparent } from './reparent-utils' @@ -136,46 +141,22 @@ export function baseAbsoluteReparentStrategy( }) if (reparentTarget.shouldReparent && allowedToReparent) { - const commands = mapDropNulls((selectedElement) => { - const reparentResult = getReparentOutcome( - canvasState.startingMetadata, - canvasState.startingElementPathTree, - canvasState.startingAllElementProps, - canvasState.builtInDependencies, - projectContents, - nodeModules, - pathToReparent(selectedElement), - newParent, - 'always', - null, - ) - - if (reparentResult == null) { - return null - } else { - const offsetCommands = replaceFragmentLikePathsWithTheirChildrenRecursive( + const commands = mapDropNulls( + (selectedElement) => + createAbsoluteReparentAndOffsetCommands( + selectedElement, + newParent, + null, canvasState.startingMetadata, - canvasState.startingAllElementProps, canvasState.startingElementPathTree, - [selectedElement], - ).flatMap((target) => { - return getAbsoluteReparentPropertyChanges( - target, - newParent.intendedParentPath, - canvasState.startingMetadata, - canvasState.startingMetadata, - canvasState.projectContents, - ) - }) - - const { commands: reparentCommands, newPath } = reparentResult - return { - oldPath: selectedElement, - newPath: newPath, - commands: [...offsetCommands, ...reparentCommands], - } - } - }, filteredSelectedElements) + canvasState.startingAllElementProps, + canvasState.builtInDependencies, + projectContents, + nodeModules, + 'force-pins', + ), + selectedElements, + ) let newPaths: Array = [] let updatedTargetPaths: UpdatedPathMap = {} @@ -235,6 +216,59 @@ export function baseAbsoluteReparentStrategy( } } +export function createAbsoluteReparentAndOffsetCommands( + target: ElementPath, + newParent: InsertionPath, + indexPosition: IndexPosition | null, + metadata: ElementInstanceMetadataMap, + pathTree: ElementPathTrees, + elementProps: AllElementProps, + builtInDependencies: BuiltInDependencies, + projectContents: ProjectContentTreeRoot, + nodeModules: NodeModules, + forcePins: ForcePins, +) { + const reparentResult = getReparentOutcome( + metadata, + pathTree, + elementProps, + builtInDependencies, + projectContents, + nodeModules, + pathToReparent(target), + newParent, + 'always', + indexPosition, + ) + + if (reparentResult == null) { + return null + } else { + const offsetCommands = replaceFragmentLikePathsWithTheirChildrenRecursive( + metadata, + elementProps, + pathTree, + [target], + ).flatMap((p) => { + return getAbsoluteReparentPropertyChanges( + p, + newParent.intendedParentPath, + metadata, + metadata, + projectContents, + forcePins, + ) + }) + + const { commands: reparentCommands, newPath } = reparentResult + return { + oldPath: target, + newPath: newPath, + commands: [...offsetCommands, ...reparentCommands], + } + } +} + function maybeAddContainLayout( metadata: ElementInstanceMetadataMap, allElementProps: AllElementProps, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts index 7aa8ad89e515..3922ab30a29d 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-changes.ts @@ -71,12 +71,15 @@ const propertiesToRemove: Array = [ PP.create('style', 'bottom'), ] +export type ForcePins = 'force-pins' | 'do-not-force-pins' + export function getAbsoluteReparentPropertyChanges( target: ElementPath, newParent: ElementPath, targetStartingMetadata: ElementInstanceMetadataMap, newParentStartingMetadata: ElementInstanceMetadataMap, projectContents: ProjectContentTreeRoot, + forcePins: ForcePins, ): Array { const element: JSXElement | null = getJSXElementFromProjectContents(target, projectContents) @@ -146,8 +149,8 @@ export function getAbsoluteReparentPropertyChanges( return isRight(rawPin) && rawPin.value != null } - const needsLeftPin = !hasPin('left') && !hasPin('right') - const needsTopPin = !hasPin('top') && !hasPin('bottom') + const needsLeftPin = !hasPin('left') && !hasPin('right') && forcePins === 'force-pins' + const needsTopPin = !hasPin('top') && !hasPin('bottom') && forcePins === 'force-pins' let edgePropertiesToAdjust: Array = [] @@ -335,6 +338,7 @@ export function getReparentPropertyChanges( originalContextMetadata, currentMetadata, projectContents, + 'force-pins', ) const strategyCommands = runReparentPropertyStrategies([ diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-strategies.ts b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-strategies.ts index 535f780b5fe0..39c06c04f062 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-strategies.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reparent-helpers/reparent-property-strategies.ts @@ -312,6 +312,7 @@ export const convertFragmentLikeChildrenToVisualSize = metadata.originalTargetMetadata, metadata.currentMetadata, projectContents, + 'force-pins', ) } else { const directions = singleAxisAutoLayoutContainerDirections( diff --git a/editor/src/components/canvas/canvas-utils.ts b/editor/src/components/canvas/canvas-utils.ts index 92374c9f77e6..c6bf9cdc93b8 100644 --- a/editor/src/components/canvas/canvas-utils.ts +++ b/editor/src/components/canvas/canvas-utils.ts @@ -7,14 +7,12 @@ import { HorizontalLayoutPinnedProps, } from '../../core/layout/layout-helpers-new' import { - maybeSwitchLayoutProps, PinningAndFlexPoints, PinningAndFlexPointsExceptSize, roundJSXElementLayoutValues, } from '../../core/layout/layout-utils' import { findElementAtPath, - findJSXElementAtPath, getSimpleAttributeAtPath, MetadataUtils, } from '../../core/model/element-metadata-utils' @@ -25,7 +23,6 @@ import type { ElementInstanceMetadataMap, ArbitraryJSBlock, TopLevelElement, - SettableLayoutSystem, } from '../../core/shared/element-template' import { isJSXElement, @@ -99,7 +96,6 @@ import type { } from '../editor/store/editor-state' import { removeElementAtPath, - modifyParseSuccessAtPath, withUnderlyingTargetFromEditorState, modifyUnderlyingElementForOpenFile, isSyntheticNavigatorEntry, @@ -129,7 +125,7 @@ import type { import { cornerGuideline, Guidelines, xAxisGuideline, yAxisGuideline } from './guideline' import { getLayoutProperty } from '../../core/layout/getLayoutProperty' import { getStoryboardUID } from '../../core/model/scene-utils' -import { forceNotNull, optionalMap } from '../../core/shared/optional-utils' +import { optionalMap } from '../../core/shared/optional-utils' import { assertNever, fastForEach } from '../../core/shared/utils' import type { ProjectContentTreeRoot } from '../assets' import { getProjectFileByFilePath } from '../assets' @@ -146,7 +142,6 @@ import { mergeImports } from '../../core/workers/common/project-file-utils' import { childInsertionPath, conditionalClauseInsertionPath, - getInsertionPath, wrapInFragmentAndAppendElements, } from '../editor/store/insertion-path' import { getConditionalCaseCorrespondingToBranchPath } from '../../core/model/conditionals' @@ -1303,13 +1298,6 @@ export function isTargetPropertyHorizontal(edgePosition: EdgePosition): boolean return edgePosition.x !== 0.5 } -export const SkipFrameChange = 'skipFrameChange' - -export interface MoveTemplateResult { - updatedEditorState: EditorState - newPath: ElementPath | null -} - export function getFrameChange( target: ElementPath, newFrame: CanvasRectangle, @@ -1322,241 +1310,6 @@ export function getFrameChange( } } -export function moveTemplate( - target: ElementPath, - originalPath: ElementPath, - newFrame: CanvasRectangle | typeof SkipFrameChange | null, - indexPosition: IndexPosition, - newParentPath: ElementPath | null, - parentFrame: CanvasRectangle | null, - editorState: EditorState, - componentMetadata: ElementInstanceMetadataMap, - selectedViews: Array, - highlightedViews: Array, - newParentLayoutSystem: SettableLayoutSystem | null, - newParentMainAxis: 'horizontal' | 'vertical' | null, -): MoveTemplateResult { - function noChanges(): MoveTemplateResult { - return { - updatedEditorState: editorState, - newPath: target, - } - } - let newIndex: number = 0 - let newPath: ElementPath | null = null - let flexContextChanged: boolean = false - - const targetID = EP.toUid(target) - if (newParentPath == null) { - // TODO Scene Implementation - return noChanges() - } else { - return withUnderlyingTargetFromEditorState( - target, - editorState, - noChanges(), - (underlyingElementSuccess, underlyingElement, underlyingTarget, underlyingFilePath) => { - return withUnderlyingTargetFromEditorState( - newParentPath, - editorState, - noChanges(), - ( - newParentSuccess, - underlyingNewParentElement, - underlyingNewParentPath, - underlyingNewParentFilePath, - ) => { - const utopiaComponentsIncludingScenes = - getUtopiaJSXComponentsFromSuccess(newParentSuccess) - const { - components: withLayoutUpdatedForNewContext, - componentMetadata: withMetadataUpdatedForNewContext, - didSwitch, - toast, - } = maybeSwitchLayoutProps( - target, - originalPath, - newParentPath, - componentMetadata, - componentMetadata, - utopiaComponentsIncludingScenes, - parentFrame, - newParentLayoutSystem, - newParentMainAxis, - styleStringInArray, - editorState.allElementProps, - editorState.elementPathTree, - ) - const updatedUnderlyingElement = findElementAtPath( - underlyingTarget, - withLayoutUpdatedForNewContext, - ) - if (updatedUnderlyingElement == null) { - return noChanges() - } else { - let workingEditorState: EditorState = editorState - - let updatedUtopiaComponents: Array = - withLayoutUpdatedForNewContext - - flexContextChanged = flexContextChanged || didSwitch - - // Remove and then insert again at the new location. - let detailsOfUpdate: string | null = null - workingEditorState = modifyParseSuccessAtPath( - underlyingNewParentFilePath, - workingEditorState, - (workingSuccess) => { - updatedUtopiaComponents = removeElementAtPath( - underlyingTarget, - updatedUtopiaComponents, - ) - - const wrapperUID = generateUidWithExistingComponents( - workingEditorState.projectContents, - ) - const insertionPath = getInsertionPath( - newParentPath, - workingEditorState.projectContents, - workingEditorState.jsxMetadata, - workingEditorState.elementPathTree, - wrapperUID, - 1, - ) - - if (insertionPath == null) { - return workingSuccess - } - - const insertResult = insertJSXElementChildren( - insertionPath, - [updatedUnderlyingElement], - updatedUtopiaComponents, - indexPosition, - ) - updatedUtopiaComponents = insertResult.components - detailsOfUpdate = insertResult.insertionDetails - - return { - ...workingSuccess, - imports: mergeImports( - underlyingFilePath, - underlyingElementSuccess.imports, - insertResult.importsToAdd, - ), - topLevelElements: applyUtopiaJSXComponentsChanges( - workingSuccess.topLevelElements, - updatedUtopiaComponents, - ), - } - }, - ) - workingEditorState = includeToast(detailsOfUpdate, workingEditorState) - - // Validate the result of the re-insertion. - if (newParentPath == null) { - newIndex = updatedUtopiaComponents.findIndex( - (exported) => exported.rootElement === updatedUnderlyingElement, - ) - if (newIndex === -1) { - throw new Error('Invalid root element index.') - } - } else { - // Can't rely on underlyingNewParentElement as that will now be out of date. - const updatedUnderlyingNewParentElement = forceNotNull( - 'Element should exist', - findJSXElementAtPath(underlyingNewParentPath, updatedUtopiaComponents), - ) - newIndex = - updatedUnderlyingNewParentElement.children.indexOf(updatedUnderlyingElement) - if (newIndex === -1) { - throw new Error('Invalid child element index.') - } - } - - newPath = EP.appendToPath(newParentPath, targetID) - - let updatedComponentMetadata: ElementInstanceMetadataMap = - withMetadataUpdatedForNewContext - // Need to make these changes ahead of updating the frame. - const elementMetadata = MetadataUtils.findElementByElementPath( - updatedComponentMetadata, - target, - ) - - if (elementMetadata != null) { - const elementMetadataWithNewPath: ElementInstanceMetadata = { - ...elementMetadata, - elementPath: newPath, - } - - updatedComponentMetadata = MetadataUtils.removeElementMetadataChild( - target, - updatedComponentMetadata, - ) - - updatedComponentMetadata = MetadataUtils.insertElementMetadataChild( - newParentPath, - elementMetadataWithNewPath, - updatedComponentMetadata, - ) - } - workingEditorState.jsxMetadata = updatedComponentMetadata - - if ( - newFrame !== SkipFrameChange && - newFrame != null && - newPath != null && - !flexContextChanged - ) { - const isParentFlex = - MetadataUtils.isParentYogaLayoutedContainerAndElementParticipatesInLayout( - originalPath, - componentMetadata, - ) - const frameChanges: Array = [ - getFrameChange(newPath, newFrame, isParentFlex), - ] - - workingEditorState = updateFramesOfScenesAndComponents( - workingEditorState, - frameChanges, - parentFrame, - ) - } - - const newSelectedViews = selectedViews.map((v) => { - if (EP.pathsEqual(v, target)) { - return newPath - } else { - return v - } - }) - - const newHighlightedViews = - newParentPath == null - ? highlightedViews.map((t) => (EP.pathsEqual(t, target) ? newPath : t)) - : [newParentPath] - - const updatedEditorState: EditorState = { - ...workingEditorState, - selectedViews: Utils.stripNulls(newSelectedViews), - highlightedViews: Utils.stripNulls(newHighlightedViews), - toasts: uniqToasts([...workingEditorState.toasts, ...toast]), - } - - return { - updatedEditorState: updatedEditorState, - newPath: newPath, - } - } - }, - ) - }, - ) - } -} - export function getCanvasOffset( previousOffset: CanvasPoint, previousScale: number, diff --git a/editor/src/components/editor/actions/actions.spec.browser2.tsx b/editor/src/components/editor/actions/actions.spec.browser2.tsx index 760373c3c972..63eb891ae72b 100644 --- a/editor/src/components/editor/actions/actions.spec.browser2.tsx +++ b/editor/src/components/editor/actions/actions.spec.browser2.tsx @@ -5820,7 +5820,7 @@ export var storyboard = ( ), ) }) - it(`Unwraps an flex element`, async () => { + it(`Unwraps a flex element`, async () => { const testCode = `
-
+
`, ), ) @@ -6001,16 +6001,11 @@ export var storyboard = ( backgroundColor: 'orange', width: 30, height: 30, - top: 140, }} data-uid='qux' />
@@ -6579,9 +6574,9 @@ export var storyboard = ( backgroundColor: 'orange', width: 50, height: 50, + position: 'absolute', left: 20, top: 50, - position: 'absolute', }} data-uid='foo' /> @@ -6590,9 +6585,9 @@ export var storyboard = ( backgroundColor: 'orange', width: 30, height: 30, + position: 'absolute', left: 20, top: 102, - position: 'absolute', }} data-uid='bar' /> @@ -6600,10 +6595,10 @@ export var storyboard = ( style={{ backgroundColor: 'orange', height: 60, - left: 20, - top: 134, width: 100, position: 'absolute', + left: 20, + top: 134, }} data-uid='baz' /> diff --git a/editor/src/components/editor/actions/actions.spec.tsx b/editor/src/components/editor/actions/actions.spec.tsx index 9a02b7fdb7b1..957033b30a5a 100644 --- a/editor/src/components/editor/actions/actions.spec.tsx +++ b/editor/src/components/editor/actions/actions.spec.tsx @@ -108,7 +108,7 @@ import { updateFromWorker, workerCodeAndParsedUpdate, } from './action-creators' -import { UPDATE_FNS, editorMoveTemplate } from './actions' +import { UPDATE_FNS } from './actions' import { CURRENT_PROJECT_VERSION } from './migrations/migrations' const chaiExpect = Chai.expect @@ -319,434 +319,6 @@ describe('SET_CANVAS_FRAMES', () => { }) }) -xdescribe('moveTemplate', () => { - function fileModel(rootElements: Array): Readonly { - return deepFreeze( - parseSuccess( - sampleImportsForTests, - [ - storyboardComponent(rootElements.length), - ...rootElements.map((element, index) => { - const componentName = `MyView${index + 1}` - return utopiaJSXComponent( - componentName, - true, - 'var', - 'block', - defaultPropsParam, - [], - element, - null, - false, - emptyComments, - ) - }), - ], - {}, - null, - null, - [exportFunction('whatever')], - {}, - ), - ) - } - - function view( - uid: string, - children: Array = [], - x: FramePin = 0, - y: FramePin = 0, - width: FramePin = 0, - height: FramePin = 0, - name: string = 'View1', - ): JSXElement { - return jsxElement( - jsxElementName(name, []), - uid, - jsxAttributesFromMap({ - style: jsxAttributeNestedObjectSimple( - jsxAttributesFromMap({ - left: jsExpressionValue(x, emptyComments), - top: jsExpressionValue(y, emptyComments), - width: jsExpressionValue(width, emptyComments), - height: jsExpressionValue(height, emptyComments), - }), - emptyComments, - ), - 'data-uid': jsExpressionValue(uid, emptyComments), - }), - children, - ) - } - - function testEditorFromParseSuccess(uiFile: Readonly): EditorState { - let editor: EditorState = { - ...createEditorState(NO_OP), - projectContents: contentsToTree({ - [StoryboardFilePath]: textFile( - textFileContents('', uiFile, RevisionsState.ParsedAhead), - null, - uiFile, - 0, - ), - }), - } - editor.jsxMetadata = createFakeMetadataForComponents(uiFile.topLevelElements) - - return deepFreeze(editor) - } - - it('reparents a simple child', () => { - const view1 = view('bbb') - const view2 = view('ccc') - const root = view('aaa', [view1, view2]) - const editor = testEditorFromParseSuccess(fileModel([root])) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'ccc']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'ccc']), - 'skipFrameChange', - { type: 'front' }, - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb']), - null, - editor, - null, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath( - newEditor.projectContents, - StoryboardFilePath, - ) as TextFile - expect(isTextFile(newUiJsFile)).toBeTruthy() - expect(isParseSuccess(newUiJsFile.fileContents.parsed)).toBeTruthy() - const newComponents = getUtopiaJSXComponentsFromSuccess( - newUiJsFile.fileContents.parsed as ParseSuccess, - ) - const updatedRoot = newComponents[1] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedRoot)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot)).toHaveLength(1) - const expectedView2 = Utils.path(['rootElement', 'children', 0, 'children', 0], updatedRoot) - chaiExpect(expectedView2).to.deep.equal(view2) - }) - - // TODO BALAZS FIX THIS BY MARCH 10 2020 - xit('does update the frame', () => { - const view1 = view('bbb', [], 5, 5, 100, 100) - const view2 = view('ccc', [], 15, 15, 100, 100) - const root = view('aaa', [view1, view2], 10, 10, 100, 100) - const editor = testEditorFromParseSuccess(fileModel([root])) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'ccc']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'ccc']), - { - x: 25, - y: 25, - width: 100, - height: 100, - } as CanvasRectangle, - { type: 'front' }, - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb']), - { - x: 15, - y: 15, - width: 100, - height: 100, - } as CanvasRectangle, - editor, - null, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath(newEditor.projectContents, StoryboardFilePath) - if (newUiJsFile != null && isTextFile(newUiJsFile)) { - if (isParseSuccess(newUiJsFile.fileContents.parsed)) { - const newTopLevelElements = newUiJsFile.fileContents.parsed.topLevelElements - const updatedRoot = newTopLevelElements[0] - if (isUtopiaJSXComponent(updatedRoot)) { - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot)).toHaveLength(1) - const movedView = Utils.path(['rootElement', 'children', 0, 'children', 0], updatedRoot) - expect(movedView).toEqual(view('ccc', [], 10, 10, 100, 100)) - } else { - throw new Error('First top level element is not a component.') - } - } else { - throw new Error('File does not contain parse success.') - } - } else { - throw new Error('src/app.js is not a UI JS file.') - } - }) - - // TODO BALAZS FIX THIS BY MARCH 10 2020 - xit('does update a relative frame too', () => { - const view1 = view('bbb', [], '5%', '5%', 100, 100, 'BBB') - const view2 = view('ccc', [], '15%', '15%', 100, 100, 'CCC') - const root = view('aaa', [view1, view2], 10, 10, 100, 100, 'AAA') - const editor = testEditorFromParseSuccess(fileModel([root])) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'ccc']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'ccc']), - { - x: 25, - y: 25, - width: 100, - height: 100, - } as CanvasRectangle, - { type: 'front' }, - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb']), - { - x: 15, - y: 15, - width: 100, - height: 100, - } as CanvasRectangle, - editor, - null, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath( - newEditor.projectContents, - StoryboardFilePath, - ) as TextFile - expect(isTextFile(newUiJsFile)).toBeTruthy() - expect(isParseSuccess(newUiJsFile.fileContents.parsed)).toBeTruthy() - const newComponents = getUtopiaJSXComponentsFromSuccess( - newUiJsFile.fileContents.parsed as ParseSuccess, - ) - const updatedRoot = newComponents[1] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedRoot)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot)).toHaveLength(1) - const actual = Utils.path(['rootElement', 'children', 0, 'children', 0], updatedRoot) - const expected = view('ccc', [], '10%', '10%', 100, 100, 'CCC') - expect(actual).toEqual(expected) - }) - - it('reparents into a child arrays end', () => { - const view2 = view('ccc') - const view1 = view('bbb', [view2]) - const root = view('aaa', [view1]) - const editor = testEditorFromParseSuccess(fileModel([root])) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb', 'ccc']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb', 'ccc']), - 'skipFrameChange', - { type: 'front' }, - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa']), - null, - editor, - null, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath( - newEditor.projectContents, - StoryboardFilePath, - ) as TextFile - expect(isTextFile(newUiJsFile)).toBeTruthy() - expect(isParseSuccess(newUiJsFile.fileContents.parsed)).toBeTruthy() - const newComponents = getUtopiaJSXComponentsFromSuccess( - newUiJsFile.fileContents.parsed as ParseSuccess, - ) - const updatedRoot = newComponents[1] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedRoot)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot)).toHaveLength(2) - const actual = Utils.path(['rootElement', 'children', 1], updatedRoot) - chaiExpect(actual).to.deep.equal(view2) - }) - - it('reparents into a child arrays beginning', () => { - const view2 = view('ccc') - const view1 = view('bbb', [view2]) - const root = view('aaa', [view1]) - const editor = testEditorFromParseSuccess(fileModel([root])) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb', 'ccc']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb', 'ccc']), - 'skipFrameChange', - { type: 'back' }, - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa']), - null, - editor, - null, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath( - newEditor.projectContents, - StoryboardFilePath, - ) as TextFile - expect(isTextFile(newUiJsFile)).toBeTruthy() - expect(isParseSuccess(newUiJsFile.fileContents.parsed)).toBeTruthy() - const newComponents = getUtopiaJSXComponentsFromSuccess( - newUiJsFile.fileContents.parsed as ParseSuccess, - ) - const updatedRoot = newComponents[1] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedRoot)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot)).toHaveLength(2) - const actual = Utils.path(['rootElement', 'children', 0], updatedRoot) - chaiExpect(actual).to.deep.equal(view2) - }) - - it('reparents into a child arrays index', () => { - const view2 = view('ccc') - const view1 = view('bbb', [view2]) - const view3 = view('ddd') - const root = view('aaa', [view1, view3]) - const editor = testEditorFromParseSuccess(fileModel([root])) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb', 'ccc']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb', 'ccc']), - 'skipFrameChange', - { type: 'absolute', index: 1 }, - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa']), - null, - editor, - null, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath( - newEditor.projectContents, - StoryboardFilePath, - ) as TextFile - expect(isTextFile(newUiJsFile)).toBeTruthy() - expect(isParseSuccess(newUiJsFile.fileContents.parsed)).toBeTruthy() - const newComponents = getUtopiaJSXComponentsFromSuccess( - newUiJsFile.fileContents.parsed as ParseSuccess, - ) - const updatedRoot = newComponents[1] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedRoot)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot)).toHaveLength(3) - const actual = Utils.path(['rootElement', 'children', 1], updatedRoot) - chaiExpect(actual).to.deep.equal(view2) - }) - - it('reparents across components', () => { - const view1 = view('bbb') - const root1 = view('aaa', [view1]) - const root2 = view('ccc', []) - const editor = testEditorFromParseSuccess(fileModel([root1, root2])) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb']), - 'skipFrameChange', - { type: 'front' }, - EP.appendNewElementPath(ScenePath1ForTestUiJsFile, ['ccc']), - null, - editor, - null, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath( - newEditor.projectContents, - StoryboardFilePath, - ) as TextFile - expect(isTextFile(newUiJsFile)).toBeTruthy() - expect(isParseSuccess(newUiJsFile.fileContents.parsed)).toBeTruthy() - const newComponents = getUtopiaJSXComponentsFromSuccess( - newUiJsFile.fileContents.parsed as ParseSuccess, - ) - const updatedRoot1 = newComponents[1] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedRoot1)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot1)).toHaveLength(0) - const updatedRoot2 = newComponents[2] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedRoot2)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedRoot2)).toHaveLength(1) - const actual = Utils.path(['rootElement', 'children', 0], updatedRoot2) - chaiExpect(actual).to.deep.equal(view1) - }) - - it('reparents from pinned to group with frame props updated', () => { - const view1 = jsxElement( - jsxElementName('bbb', []), - 'bbb', - jsxAttributesFromMap({ - style: jsxAttributeNestedObjectSimple( - jsxAttributesFromMap({ - bottom: jsExpressionValue(50, emptyComments), - right: jsExpressionValue(50, emptyComments), - width: jsExpressionValue(100, emptyComments), - height: jsExpressionValue(100, emptyComments), - }), - emptyComments, - ), - 'data-uid': jsExpressionValue('bbb', emptyComments), - }), - [], - ) - const root2 = view('ddd', [], -10, -10, 100, 100, 'Group') - const root1 = view('aaa', [view1]) - const editor = testEditorFromParseSuccess(fileModel([root1, root2])) - const groupFrame = canvasRectangle({ x: -10, y: -10, width: 100, height: 100 }) - - const newEditor = editorMoveTemplate( - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb']), - EP.appendNewElementPath(ScenePathForTestUiJsFile, ['aaa', 'bbb']), - canvasRectangle({ x: 10, y: 10, width: 100, height: 100 }), - { type: 'front' }, - EP.appendNewElementPath(ScenePath1ForTestUiJsFile, ['ddd']), - groupFrame, - editor, - LayoutSystem.Group, - null, - ).editor - - const newUiJsFile = getProjectFileByFilePath( - newEditor.projectContents, - StoryboardFilePath, - ) as TextFile - expect(isTextFile(newUiJsFile)).toBeTruthy() - expect(isParseSuccess(newUiJsFile.fileContents.parsed)).toBeTruthy() - const newComponents = getUtopiaJSXComponentsFromSuccess( - newUiJsFile.fileContents.parsed as ParseSuccess, - ) - const updatedGroup = newComponents[2] as UtopiaJSXComponent - expect(isUtopiaJSXComponent(updatedGroup)).toBeTruthy() - expect(Utils.pathOr([], ['rootElement', 'children'], updatedGroup)).toHaveLength(1) - const actual: any = Utils.path(['rootElement', 'children', 0], updatedGroup) - expect( - getLayoutPropertyOr(undefined, 'left', right(actual.props), styleStringInArray), - ).toBeDefined() - expect( - getLayoutPropertyOr(undefined, 'top', right(actual.props), styleStringInArray), - ).toBeDefined() - expect( - getLayoutPropertyOr(undefined, 'width', right(actual.props), styleStringInArray), - ).toBeDefined() - expect( - getLayoutPropertyOr(undefined, 'height', right(actual.props), styleStringInArray), - ).toBeDefined() - expect( - getLayoutPropertyOr(undefined, 'right', right(actual.props), styleStringInArray), - ).not.toBeDefined() - expect( - getLayoutPropertyOr(undefined, 'bottom', right(actual.props), styleStringInArray), - ).not.toBeDefined() - }) -}) - -function getOpenFileComponents(editor: EditorState): Array { - const openFile = getOpenUIJSFile(editor) - if (openFile == null) { - return [] - } else { - if (isParseSuccess(openFile.fileContents.parsed)) { - return getUtopiaJSXComponentsFromSuccess(openFile.fileContents.parsed) - } else { - return [] - } - } -} - describe('LOAD', () => { it('Parses all UIJS files and bins any previously stored parsed model data', () => { const firstUIJSFile = StoryboardFilePath diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 1304268040f9..0e32a346f127 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -154,12 +154,10 @@ import { } from '../../assets' import type { CanvasFrameAndTarget, PinOrFlexFrameChange } from '../../canvas/canvas-types' import { pinSizeChange } from '../../canvas/canvas-types' -import type { SkipFrameChange } from '../../canvas/canvas-utils' import { canvasPanelOffsets, duplicate, getFrameChange, - moveTemplate, updateFramesOfScenesAndComponents, } from '../../canvas/canvas-utils' import type { SetFocus } from '../../common/actions' @@ -483,7 +481,10 @@ import { } from '../store/insertion-path' import { getConditionalCaseCorrespondingToBranchPath } from '../../../core/model/conditionals' import { deleteProperties } from '../../canvas/commands/delete-properties-command' -import { treatElementAsFragmentLike } from '../../canvas/canvas-strategies/strategies/fragment-like-helpers' +import { + replaceFragmentLikePathsWithTheirChildrenRecursive, + treatElementAsFragmentLike, +} from '../../canvas/canvas-strategies/strategies/fragment-like-helpers' import { fixParentContainingBlockSettings, isTextContainingConditional, @@ -522,6 +523,9 @@ import { deleteElement } from '../../canvas/commands/delete-element-command' import { queueGroupTrueUp } from '../../canvas/commands/queue-group-true-up-command' import { processWorkerUpdates } from '../../../core/shared/parser-projectcontents-utils' import { getAllUniqueUids } from '../../../core/model/get-unique-ids' +import { resultForFirstApplicableStrategy } from '../../inspector/inspector-strategies/inspector-strategy' +import { reparentToUnwrapAsAbsoluteStrategy } from '../one-shot-unwrap-strategies/reparent-to-unwrap-as-absolute-strategy' +import { convertToAbsoluteAndReparentToUnwrapStrategy } from '../one-shot-unwrap-strategies/convert-to-absolute-and-reparent-to-unwrap' export const MIN_CODE_PANE_REOPEN_WIDTH = 100 @@ -700,37 +704,47 @@ export function insertIntoWrapper( } } -export function editorMoveTemplate( +export function reparentElementToUnwrap( target: ElementPath, - originalPath: ElementPath, - newFrame: CanvasRectangle | typeof SkipFrameChange | null, + insertionPath: InsertionPath, indexPosition: IndexPosition, - newParentPath: ElementPath | null, - parentFrame: CanvasRectangle | null, editor: EditorModel, - newParentLayoutSystem: SettableLayoutSystem | null, - newParentMainAxis: 'horizontal' | 'vertical' | null, -): { - editor: EditorModel - newPath: ElementPath | null -} { - const moveResult = moveTemplate( - target, - originalPath, - newFrame, - indexPosition, - newParentPath, - parentFrame, - editor, + builtInDependencies: BuiltInDependencies, +): { editor: EditorModel; newPath: ElementPath | null } { + const result = resultForFirstApplicableStrategy( editor.jsxMetadata, editor.selectedViews, - editor.highlightedViews, - newParentLayoutSystem, - newParentMainAxis, + editor.elementPathTree, + editor.allElementProps, + [ + reparentToUnwrapAsAbsoluteStrategy( + pathToReparent(target), + insertionPath, + indexPosition, + builtInDependencies, + editor.projectContents, + editor.nodeModules.files, + ), + convertToAbsoluteAndReparentToUnwrapStrategy( + pathToReparent(target), + insertionPath, + indexPosition, + builtInDependencies, + editor.projectContents, + editor.nodeModules.files, + ), + ], ) + + if (result == null) { + return { editor: editor, newPath: null } + } + + const updatedEditor = foldAndApplyCommandsSimple(editor, result.commands) + return { - newPath: moveResult.newPath, - editor: moveResult.updatedEditorState, + editor: updatedEditor, + newPath: result.data.newPath, } } @@ -2285,38 +2299,35 @@ export const UPDATE_FNS = { ) const withChildrenMoved = children.reduce((working, child) => { - const childFrame = MetadataUtils.getFrameOrZeroRectInCanvasCoords( - child.elementPath, - workingEditor.jsxMetadata, - ) - const result = editorMoveTemplate( - child.elementPath, + if (parentPath == null) { + return working + } + const result = reparentElementToUnwrap( child.elementPath, - childFrame, + parentPath, indexPosition, - parentPath?.intendedParentPath ?? null, - parentFrame, working, - null, - null, + builtInDependencies, ) if (result.newPath != null) { - newSelection.push(result.newPath) + const newPath = result.newPath + newSelection.push(newPath) if (isGroupChild) { - groupTrueUps.push(result.newPath) + groupTrueUps.push(newPath) return foldAndApplyCommandsSimple( result.editor, createPinChangeCommandsForElementBecomingGroupChild( workingEditor.jsxMetadata, child, - result.newPath, + newPath, parentFrame, localRectangle(parentFrame), ), ) } + return result.editor } - return result.editor + return working }, workingEditor) return { diff --git a/editor/src/components/editor/one-shot-insertion-strategies/insert-as-absolute-strategy.tsx b/editor/src/components/editor/one-shot-insertion-strategies/insert-as-absolute-strategy.tsx index c44b95db3886..69873e0696f0 100644 --- a/editor/src/components/editor/one-shot-insertion-strategies/insert-as-absolute-strategy.tsx +++ b/editor/src/components/editor/one-shot-insertion-strategies/insert-as-absolute-strategy.tsx @@ -73,6 +73,7 @@ export const insertAsAbsoluteStrategy = ( metadata, metadata, state.projectContents, + 'force-pins', ), setProperty('always', result.newPath, PP.create('style', 'position'), 'absolute'), ], diff --git a/editor/src/components/editor/one-shot-unwrap-strategies/convert-to-absolute-and-reparent-to-unwrap.tsx b/editor/src/components/editor/one-shot-unwrap-strategies/convert-to-absolute-and-reparent-to-unwrap.tsx new file mode 100644 index 000000000000..2c4b7f977ae9 --- /dev/null +++ b/editor/src/components/editor/one-shot-unwrap-strategies/convert-to-absolute-and-reparent-to-unwrap.tsx @@ -0,0 +1,101 @@ +import type { BuiltInDependencies } from '../../../core/es-modules/package-manager/built-in-dependencies-list' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import * as EP from '../../../core/shared/element-path' +import type { ElementPathTrees } from '../../../core/shared/element-path-tree' +import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' +import { + getLocalRectangleInNewParentContext, + isFiniteRectangle, +} from '../../../core/shared/math-utils' +import type { NodeModules, ElementPath } from '../../../core/shared/project-file-types' +import type { IndexPosition } from '../../../utils/utils' +import { front } from '../../../utils/utils' +import type { ProjectContentTreeRoot } from '../../assets' +import { autoLayoutParentAbsoluteOrStatic } from '../../canvas/canvas-strategies/strategies/reparent-helpers/reparent-strategy-parent-lookup' +import type { PathToReparent } from '../../canvas/canvas-strategies/strategies/reparent-utils' +import { getReparentOutcome } from '../../canvas/canvas-strategies/strategies/reparent-utils' +import { reorderElement } from '../../canvas/commands/reorder-element-command' +import { getConvertIndividualElementToAbsoluteCommands } from '../../inspector/inspector-common' +import type { AllElementProps } from '../store/editor-state' +import type { InsertionPath } from '../store/insertion-path' +import type { UnwrapInspectorStrategy } from './unwrap-strategies-common' + +export const convertToAbsoluteAndReparentToUnwrapStrategy = ( + element: PathToReparent, + parentInsertionPath: InsertionPath, + indexPosition: IndexPosition, + builtInDependencies: BuiltInDependencies, + projectContents: ProjectContentTreeRoot, + nodeModules: NodeModules, +): UnwrapInspectorStrategy => ({ + name: 'Convert to absolute and reparent to unwrap', + strategy: ( + metadata: ElementInstanceMetadataMap, + _: Array, + elementPathTree: ElementPathTrees, + allElementProps: AllElementProps, + ) => { + const shouldReparentAsAbsoluteOrStatic = autoLayoutParentAbsoluteOrStatic( + metadata, + allElementProps, + elementPathTree, + EP.parentPath(element.target), + 'prefer-absolute', + ) + + if (shouldReparentAsAbsoluteOrStatic !== 'REPARENT_AS_STATIC') { + return null + } + + const result = getReparentOutcome( + metadata, + elementPathTree, + allElementProps, + builtInDependencies, + projectContents, + nodeModules, + element, + parentInsertionPath, + 'always', + front(), + ) + + if (result == null) { + return null + } + + const targetMetadata = MetadataUtils.findElementByElementPath(metadata, element.target) + const parentMetadata = MetadataUtils.findElementByElementPath( + metadata, + parentInsertionPath.intendedParentPath, + ) + + const escapeHatchCommands = + parentMetadata?.globalFrame != null && + targetMetadata?.globalFrame != null && + isFiniteRectangle(targetMetadata.globalFrame) && + isFiniteRectangle(parentMetadata.globalFrame) + ? getConvertIndividualElementToAbsoluteCommands( + element.target, + metadata, + elementPathTree, + getLocalRectangleInNewParentContext( + parentMetadata.globalFrame, + targetMetadata.globalFrame, + ), + null, + ) + : [] + + return { + commands: [ + ...escapeHatchCommands, + ...result.commands, + reorderElement('on-complete', result.newPath, indexPosition), + ], + data: { + newPath: result.newPath, + }, + } + }, +}) diff --git a/editor/src/components/editor/one-shot-unwrap-strategies/reparent-to-unwrap-as-absolute-strategy.tsx b/editor/src/components/editor/one-shot-unwrap-strategies/reparent-to-unwrap-as-absolute-strategy.tsx new file mode 100644 index 000000000000..472708c53dd4 --- /dev/null +++ b/editor/src/components/editor/one-shot-unwrap-strategies/reparent-to-unwrap-as-absolute-strategy.tsx @@ -0,0 +1,66 @@ +import type { BuiltInDependencies } from '../../../core/es-modules/package-manager/built-in-dependencies-list' +import * as EP from '../../../core/shared/element-path' +import type { ElementPathTrees } from '../../../core/shared/element-path-tree' +import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template' +import type { NodeModules, ElementPath } from '../../../core/shared/project-file-types' +import type { IndexPosition } from '../../../utils/utils' +import type { ProjectContentTreeRoot } from '../../assets' +import { autoLayoutParentAbsoluteOrStatic } from '../../canvas/canvas-strategies/strategies/reparent-helpers/reparent-strategy-parent-lookup' +import type { PathToReparent } from '../../canvas/canvas-strategies/strategies/reparent-utils' +import type { AllElementProps } from '../store/editor-state' +import type { InsertionPath } from '../store/insertion-path' +import { createAbsoluteReparentAndOffsetCommands } from '../../canvas/canvas-strategies/strategies/absolute-reparent-strategy' +import type { UnwrapInspectorStrategy } from './unwrap-strategies-common' + +export const reparentToUnwrapAsAbsoluteStrategy = ( + element: PathToReparent, + parentInsertionPath: InsertionPath, + indexPosition: IndexPosition, + builtInDependencies: BuiltInDependencies, + projectContents: ProjectContentTreeRoot, + nodeModules: NodeModules, +): UnwrapInspectorStrategy => ({ + name: 'Reparent to unwrap as absolute', + strategy: ( + metadata: ElementInstanceMetadataMap, + _: Array, + elementPathTree: ElementPathTrees, + allElementProps: AllElementProps, + ) => { + const shouldReparentAsAbsoluteOrStatic = autoLayoutParentAbsoluteOrStatic( + metadata, + allElementProps, + elementPathTree, + EP.parentPath(element.target), + 'prefer-absolute', + ) + + if (shouldReparentAsAbsoluteOrStatic !== 'REPARENT_AS_ABSOLUTE') { + return null + } + + const result = createAbsoluteReparentAndOffsetCommands( + element.target, + parentInsertionPath, + indexPosition, + metadata, + elementPathTree, + allElementProps, + builtInDependencies, + projectContents, + nodeModules, + 'do-not-force-pins', + ) + + if (result == null) { + return null + } + + return { + commands: result.commands, + data: { + newPath: result.newPath, + }, + } + }, +}) diff --git a/editor/src/components/editor/one-shot-unwrap-strategies/unwrap-strategies-common.ts b/editor/src/components/editor/one-shot-unwrap-strategies/unwrap-strategies-common.ts new file mode 100644 index 000000000000..4f4fa8ad739a --- /dev/null +++ b/editor/src/components/editor/one-shot-unwrap-strategies/unwrap-strategies-common.ts @@ -0,0 +1,4 @@ +import type { ElementPath } from '../../../core/shared/project-file-types' +import type { CustomInspectorStrategy } from '../../inspector/inspector-strategies/inspector-strategy' + +export type UnwrapInspectorStrategy = CustomInspectorStrategy<{ newPath: ElementPath }> diff --git a/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts b/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts index 5f95bb2e6b6b..a7d9fbfb0ea2 100644 --- a/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts +++ b/editor/src/components/inspector/inspector-strategies/inspector-strategy.ts @@ -6,6 +6,25 @@ import type { EditorDispatch } from '../../editor/action-types' import { applyCommandsAction } from '../../editor/actions/action-creators' import type { AllElementProps } from '../../editor/store/editor-state' +interface CustomInspectorStrategyResultBase { + commands: Array +} + +export type CustomInspectorStrategyResult> = + T extends undefined + ? CustomInspectorStrategyResultBase + : CustomInspectorStrategyResultBase & { data: T } + +export interface CustomInspectorStrategy> { + name: string + strategy: ( + metadata: ElementInstanceMetadataMap, + selectedElementPaths: Array, + elementPathTree: ElementPathTrees, + allElementProps: AllElementProps, + ) => CustomInspectorStrategyResult | null +} + export interface InspectorStrategy { name: string strategy: ( @@ -16,6 +35,22 @@ export interface InspectorStrategy { ) => Array | null } +export function resultForFirstApplicableStrategy>( + metadata: ElementInstanceMetadataMap, + selectedViews: Array, + elementPathTree: ElementPathTrees, + allElementProps: AllElementProps, + strategies: Array>, +): CustomInspectorStrategyResult | null { + for (const strategy of strategies) { + const result = strategy.strategy(metadata, selectedViews, elementPathTree, allElementProps) + if (result != null) { + return result + } + } + return null +} + export function commandsForFirstApplicableStrategy( metadata: ElementInstanceMetadataMap, selectedViews: Array, diff --git a/editor/src/core/layout/layout-utils.ts b/editor/src/core/layout/layout-utils.ts index 81991a77f3e6..e317042d5c41 100644 --- a/editor/src/core/layout/layout-utils.ts +++ b/editor/src/core/layout/layout-utils.ts @@ -65,59 +65,6 @@ interface LayoutPropChangeResult { toast: Array } -export function maybeSwitchChildrenLayoutProps( - target: ElementPath, - targetOriginalContextMetadata: ElementInstanceMetadataMap, - currentContextMetadata: ElementInstanceMetadataMap, - components: UtopiaJSXComponent[], - propertyTarget: ReadonlyArray, - allElementProps: AllElementProps, - pathTrees: ElementPathTrees, -): LayoutPropChangeResult { - const children = MetadataUtils.getChildrenOrdered( - targetOriginalContextMetadata, - pathTrees, - target, - ) - const result = children.reduce( - (working, next) => { - const { components: workingComponents, didSwitch: workingDidSwitch } = working - const { - components: nextComponents, - componentMetadata: nextMetadata, - didSwitch: nextDidSwitch, - toast: nextToast, - } = maybeSwitchLayoutProps( - next.elementPath, - next.elementPath, - target, - targetOriginalContextMetadata, - currentContextMetadata, - workingComponents, - null, - null, - null, - propertyTarget, - allElementProps, - pathTrees, - ) - return { - components: nextComponents, - componentMetadata: nextMetadata, - didSwitch: workingDidSwitch || nextDidSwitch, - toast: [...working.toast, ...nextToast], - } - }, - { - components: components, - componentMetadata: currentContextMetadata, - didSwitch: false, - toast: [], - }, - ) - return result -} - function getParentAxisFromElement( element: ElementInstanceMetadata | null, propertyTarget: ReadonlyArray, @@ -134,178 +81,6 @@ function getParentAxisFromElement( ) } -export function maybeSwitchLayoutProps( - target: ElementPath, - originalPath: ElementPath, - newParentPath: ElementPath, - targetOriginalContextMetadata: ElementInstanceMetadataMap, - currentContextMetadata: ElementInstanceMetadataMap, - components: UtopiaJSXComponent[], - parentFrame: CanvasRectangle | null, - parentLayoutSystem: SettableLayoutSystem | null, - newParentMainAxis: 'horizontal' | 'vertical' | null, - propertyTarget: ReadonlyArray, - allElementProps: AllElementProps, - pathTrees: ElementPathTrees, -): LayoutPropChangeResult { - const originalParentPath = EP.parentPath(originalPath) - const originalParent = MetadataUtils.findElementByElementPath( - targetOriginalContextMetadata, - originalParentPath, - ) - const newParent = MetadataUtils.findElementByElementPath(currentContextMetadata, newParentPath) - - let wasFlexContainer = MetadataUtils.isFlexLayoutedContainer(originalParent) - const oldParentMainAxis: 'horizontal' | 'vertical' | null = getParentAxisFromElement( - originalParent, - propertyTarget, - ) - - let isFlexContainer = - parentLayoutSystem === 'flex' || MetadataUtils.isFlexLayoutedContainer(newParent) - - const parentMainAxis: 'horizontal' | 'vertical' | null = - newParentMainAxis ?? getParentAxisFromElement(newParent, propertyTarget) // if no newParentMainAxis is provided, let's try to find one - - // When wrapping elements in view/group the element is not available from the componentMetadata but we know the frame already. - // BALAZS I added a clause !isFlexContainer here but I think this whole IF should go to the bin - if (!isFlexContainer && newParent == null && parentFrame != null) { - // FIXME wrapping in a view now always switches to pinned props. maybe the user wants to keep the parent layoutsystem? - const switchLayoutFunction = - parentLayoutSystem === LayoutSystem.Group - ? switchChildToGroupWithParentFrame - : switchChildToPinnedWithParentFrame - const { updatedComponents, updatedMetadata } = switchLayoutFunction( - target, - originalPath, - targetOriginalContextMetadata, - components, - parentFrame, - propertyTarget, - allElementProps, - ) - - const staticTarget = EP.dynamicPathToStaticPath(target) - const originalElement = findJSXElementAtStaticPath(components, staticTarget) - const originalPropertyPaths = getAllPathsFromAttributes(originalElement?.props ?? []) - const updatedelement = findJSXElementAtStaticPath(updatedComponents, staticTarget) - const updatedPropertyPaths = getAllPathsFromAttributes(updatedelement?.props ?? []) - - return { - components: updatedComponents, - componentMetadata: updatedMetadata, - didSwitch: true, - toast: createStylePostActionToast( - MetadataUtils.getElementLabel( - allElementProps, - target, - pathTrees, - targetOriginalContextMetadata, - ), - originalPropertyPaths, - updatedPropertyPaths, - ), - } - } else { - const switchLayoutFunction = getLayoutFunction( - wasFlexContainer, - oldParentMainAxis, - isFlexContainer, - parentMainAxis, - ) - const { updatedComponents, updatedMetadata } = switchLayoutFunction.layoutFn( - target, - newParentPath, - targetOriginalContextMetadata, - currentContextMetadata, - components, - parentMainAxis, - propertyTarget, - allElementProps, - ) - const staticTarget = EP.dynamicPathToStaticPath(target) - const originalElement = findJSXElementAtStaticPath(components, staticTarget) - const originalPropertyPaths = getAllPathsFromAttributes(originalElement?.props ?? []) - const updatedelement = findJSXElementAtStaticPath(updatedComponents, staticTarget) - const updatedPropertyPaths = getAllPathsFromAttributes(updatedelement?.props ?? []) - - return { - components: updatedComponents, - componentMetadata: updatedMetadata, - didSwitch: switchLayoutFunction.didSwitch, - toast: switchLayoutFunction.didSwitch - ? createStylePostActionToast( - MetadataUtils.getElementLabel( - allElementProps, - target, - pathTrees, - targetOriginalContextMetadata, - ), - originalPropertyPaths, - updatedPropertyPaths, - ) - : [], - } - } -} - -function getLayoutFunction( - wasFlexContainer: boolean, - oldMainAxis: 'horizontal' | 'vertical' | null, - isFlexContainer: boolean, - newMainAxis: 'horizontal' | 'vertical' | null, -): { - layoutFn: ( - target: ElementPath, - newParentPath: ElementPath, - targetOriginalContextMetadata: ElementInstanceMetadataMap, - currentContextMetadata: ElementInstanceMetadataMap, - components: UtopiaJSXComponent[], - newParentMainAxis: 'horizontal' | 'vertical' | null, - propertyTarget: ReadonlyArray, - allElementProps: AllElementProps, - ) => SwitchLayoutTypeResult - didSwitch: boolean -} { - if (wasFlexContainer) { - if (isFlexContainer) { - // From flex to flex - if (oldMainAxis === newMainAxis) { - return { - layoutFn: keepLayoutProps, - didSwitch: false, - } - } else { - return { - layoutFn: switchFlexToFlexDifferentAxis, - didSwitch: true, - } - } - } else { - // From flex to pinned - return { - layoutFn: switchFlexChildToPinned, - didSwitch: true, - } - } - } else { - // wasPinned - if (isFlexContainer) { - // From pinned to flex - return { - layoutFn: switchPinnedChildToFlex, - didSwitch: true, - } - } else { - // From pinned to pinned - return { - layoutFn: keepLayoutProps, - didSwitch: false, - } - } - } -} - export const PinningAndFlexPoints: Array = [ ...LayoutPinnedProps, 'flexBasis', diff --git a/editor/src/core/model/element-metadata-utils.ts b/editor/src/core/model/element-metadata-utils.ts index 3da79ce1d22b..356d56686478 100644 --- a/editor/src/core/model/element-metadata-utils.ts +++ b/editor/src/core/model/element-metadata-utils.ts @@ -1703,39 +1703,6 @@ export const MetadataUtils = { createElementPathTreeFromMetadata(metadata: ElementInstanceMetadataMap): ElementPathTrees { return buildTree(metadata) }, - removeElementMetadataChild( - target: ElementPath, - metadata: ElementInstanceMetadataMap, - ): ElementInstanceMetadataMap { - // Note this only removes the child element from the metadata, but keeps grandchildren in there (inaccessible). Is this a memory leak? - let remainingElements: ElementInstanceMetadataMap = omit([EP.toString(target)], metadata) - if (Object.keys(remainingElements).length === Object.keys(metadata).length) { - // Nothing was removed - return metadata - } else { - return remainingElements - } - }, - insertElementMetadataChild( - targetParent: ElementPath | null, - elementToInsert: ElementInstanceMetadata, - metadata: ElementInstanceMetadataMap, - ): ElementInstanceMetadataMap { - // Insert into the map - if (!EP.pathsEqual(EP.parentPath(elementToInsert.elementPath), targetParent)) { - throw new Error( - `insertElementMetadataChild: trying to insert child metadata with incorrect parent path prefix. - Target parent: ${EP.toString(targetParent!)}, - child path: ${EP.toString(elementToInsert.elementPath)}`, - ) - } - - const withNewElement: ElementInstanceMetadataMap = { - ...metadata, - [EP.toString(elementToInsert.elementPath)]: elementToInsert, - } - return withNewElement - }, duplicateElementMetadataAtPath( oldPath: ElementPath, newPath: ElementPath,