diff --git a/editor/src/components/editor/actions/actions.spec.browser2.tsx b/editor/src/components/editor/actions/actions.spec.browser2.tsx index 595029f72907..2378d2f08f73 100644 --- a/editor/src/components/editor/actions/actions.spec.browser2.tsx +++ b/editor/src/components/editor/actions/actions.spec.browser2.tsx @@ -38,6 +38,7 @@ import { getElementFromRenderResult } from './actions.test-utils' import { expectNoAction, expectSingleUndoNSaves, + searchInComponentPicker, searchInFloatingMenu, selectComponentsForTest, setFeatureForBrowserTestsUseInDescribeBlockOnly, @@ -7408,7 +7409,7 @@ export var storyboard = ( ) }) - it('can group conditionals', async () => { + it.skip('can group conditionals', async () => { const testCode = `
@@ -7493,10 +7494,10 @@ export var storyboard = ( ), ) - expect(renderResult.getEditorState().editor.toasts.length).toEqual(1) - expect(renderResult.getEditorState().editor.toasts[0].message).toEqual( - 'Not all targets can be wrapped into a Group', - ) + // expect(renderResult.getEditorState().editor.toasts.length).toEqual(1) + // expect(renderResult.getEditorState().editor.toasts[0].message).toEqual( + // 'Not all targets can be wrapped into a Group', + // ) }) it('cannot group conditionals with active branch that cannot be a group child', async () => { @@ -7535,13 +7536,13 @@ export var storyboard = ( ), ) - expect(renderResult.getEditorState().editor.toasts.length).toEqual(1) - expect(renderResult.getEditorState().editor.toasts[0].message).toEqual( - 'Not all targets can be wrapped into a Group', - ) + // expect(renderResult.getEditorState().editor.toasts.length).toEqual(1) + // expect(renderResult.getEditorState().editor.toasts[0].message).toEqual( + // 'Not all targets can be wrapped into a Group', + // ) }) - it('can wrap nested conditionals', async () => { + it.skip('can wrap nested conditionals', async () => { const testCode = `
@@ -7646,10 +7647,10 @@ export var storyboard = ( ), ) - expect(renderResult.getEditorState().editor.toasts.length).toEqual(1) - expect(renderResult.getEditorState().editor.toasts[0].message).toEqual( - 'Not all targets can be wrapped into a Group', - ) + // expect(renderResult.getEditorState().editor.toasts.length).toEqual(1) + // expect(renderResult.getEditorState().editor.toasts[0].message).toEqual( + // 'Not all targets can be wrapped into a Group', + // ) }) }) }) @@ -7841,5 +7842,5 @@ async function wrapInElement( await selectComponentsForTest(renderResult, pathsToWrap) await pressKey('w') // open the wrap menu FOR_TESTS_setNextGeneratedUid(uid) - await searchInFloatingMenu(renderResult, query) + await searchInComponentPicker(renderResult, query) } diff --git a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx index 0dab577ef27a..c385b144bd85 100644 --- a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx +++ b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx @@ -5,6 +5,7 @@ import { selectComponentsForTest, expectSingleUndo2Saves, searchInFloatingMenu, + searchInComponentPicker, } from '../../utils/utils.test-utils' import { FOR_TESTS_setNextGeneratedUid, @@ -1389,11 +1390,11 @@ export var storyboard = ( return editor } - it.skip('when wrapping into fragment', async () => { + it('when wrapping into fragment', async () => { const editor = await setup() await pressKey('w') - await searchInFloatingMenu(editor, 'fragm') + await searchInComponentPicker(editor, 'fragm') expect(getPrintedUiJsCode(editor.getEditorState(), PlaygroundFilePath)) .toEqual(`import * as React from 'react' diff --git a/editor/src/components/editor/conditionals.spec.browser2.tsx b/editor/src/components/editor/conditionals.spec.browser2.tsx index 0f446980685a..c4e7b8a5d633 100644 --- a/editor/src/components/editor/conditionals.spec.browser2.tsx +++ b/editor/src/components/editor/conditionals.spec.browser2.tsx @@ -10,7 +10,11 @@ import { unsafeGet } from '../../core/shared/optics/optic-utilities' import type { Optic } from '../../core/shared/optics/optics' import { forceNotNull } from '../../core/shared/optional-utils' import type { ElementPath } from '../../core/shared/project-file-types' -import { searchInFloatingMenu, selectComponentsForTest } from '../../utils/utils.test-utils' +import { + searchInComponentPicker, + searchInFloatingMenu, + selectComponentsForTest, +} from '../../utils/utils.test-utils' import type { EditorRenderResult } from '../canvas/ui-jsx.test-utils' import { TestScenePath, @@ -385,7 +389,7 @@ describe('conditionals', () => { }) }) describe('wrap', () => { - it.skip('can wrap a single element in a conditional', async () => { + it('can wrap a single element in a conditional', async () => { const startSnippet = `
hello there
@@ -448,7 +452,7 @@ describe('conditionals', () => { `), ) }) - it.skip('can wrap a conditional clause element in a conditional', async () => { + it('can wrap a conditional clause element in a conditional', async () => { const targetUID = 'bbb' const startSnippet = `
@@ -1114,5 +1118,5 @@ describe('conditionals', () => { async function wrapInConditional(renderResult: EditorRenderResult) { await pressKey('w') // open the wrap menu - await searchInFloatingMenu(renderResult, 'Condition') + await searchInComponentPicker(renderResult, 'Condition') } diff --git a/editor/src/components/editor/global-shortcuts.tsx b/editor/src/components/editor/global-shortcuts.tsx index 13e349628e42..a13abc200ceb 100644 --- a/editor/src/components/editor/global-shortcuts.tsx +++ b/editor/src/components/editor/global-shortcuts.tsx @@ -591,16 +591,11 @@ export function handleKeyDown( [WRAP_ELEMENT_PICKER_SHORTCUT]: () => { if (allowedToEdit) { if (isSelectMode(editor.mode)) { - // for multiple selection, we show the old picker - if (editor.selectedViews.length > 1) { - return [EditorActions.openFloatingInsertMenu({ insertMenuMode: 'wrap' })] - } else { - const mousePoint = WindowMousePositionRaw ?? zeroCanvasPoint - showComponentPicker(editor.selectedViews, EditorActions.wrapTarget)(event, { - position: mousePoint, - }) - return [] - } + const mousePoint = WindowMousePositionRaw ?? zeroCanvasPoint + showComponentPicker(editor.selectedViews, EditorActions.wrapTarget)(event, { + position: mousePoint, + }) + return [] } } return [] diff --git a/editor/src/components/navigator/navigator-item/component-picker-context-menu.tsx b/editor/src/components/navigator/navigator-item/component-picker-context-menu.tsx index ba94d2f4761c..380a54f88f6e 100644 --- a/editor/src/components/navigator/navigator-item/component-picker-context-menu.tsx +++ b/editor/src/components/navigator/navigator-item/component-picker-context-menu.tsx @@ -24,6 +24,7 @@ import type { ElementPath, Imports } from '../../../core/shared/project-file-typ import { useDispatch } from '../../editor/store/dispatch-context' import { Substores, useEditorState, useRefEditorState } from '../../editor/store/store-hook' import { + applyCommandsAction, deleteView, insertAsChildTarget, insertInsertable, @@ -82,7 +83,9 @@ import { generateUidWithExistingComponents } from '../../../core/model/element-t import { emptyComments } from 'utopia-shared/src/types' import { intrinsicHTMLElementNamesThatSupportChildren } from '../../../core/shared/dom-utils' import { emptyImports } from '../../../core/workers/common/project-file-utils' -import { forceNotNull } from '../../../core/shared/optional-utils' +import { commandsForFirstApplicableStrategy } from '../../../components/inspector/inspector-strategies/inspector-strategy' +import { wrapInDivStrategy } from '../../../components/editor/wrap-in-callbacks' +import type { AllElementProps } from '../../../components/editor/store/editor-state' type RenderPropTarget = { type: 'render-prop'; prop: string } type ConditionalTarget = { type: 'conditional'; conditionalCase: ConditionalCase } @@ -469,19 +472,38 @@ function insertComponentPickerItem( toInsert: InsertableComponent, targets: ElementPath[], projectContents: ProjectContentTreeRoot, + allElementProps: AllElementProps, + propertyControlsInfo: PropertyControlsInfo, metadata: ElementInstanceMetadataMap, pathTrees: ElementPathTrees, dispatch: EditorDispatch, insertionTarget: InsertionTarget, ) { const uniqueIds = new Set(getAllUniqueUids(projectContents).uniqueIDs) - const uid = generateConsistentUID('prop', uniqueIds) const elementWithoutUID = toInsert.element() // TODO: for most of the operations we still only support one target const firstTarget = targets[0] const actions = ((): Array => { if (elementWithoutUID.type === 'JSX_ELEMENT') { + if (isWrapTarget(insertionTarget) && elementWithoutUID?.name?.baseVariable === 'div') { + const commands = commandsForFirstApplicableStrategy([ + wrapInDivStrategy( + metadata, + targets, + pathTrees, + allElementProps, + projectContents, + propertyControlsInfo, + ), + ]) + + if (commands != null) { + return [applyCommandsAction(commands)] + } + } + + const uid = generateConsistentUID('prop', uniqueIds) const element = jsxElementFromJSXElementWithoutUID(elementWithoutUID, uid) const fixedElement = fixUtopiaElement(element, uniqueIds).value @@ -509,27 +531,6 @@ function insertComponentPickerItem( return [replaceMappedElement(fixedElement, firstTarget, toInsert.importsToAdd)] } - if (isWrapTarget(insertionTarget)) { - const newUID = generateUidWithExistingComponents(projectContents) - - const newElement = jsxElement( - element.name, - newUID, - setJSXAttributesAttribute( - element.props, - 'data-uid', - jsExpressionValue(newUID, emptyComments), - ), - element.children, - ) - return [ - wrapInElement(targets, { - element: newElement, - importsToAdd: toInsert.importsToAdd, - }), - ] - } - if ( isReplaceTarget(insertionTarget) || isReplaceKeepChildrenAndStyleTarget(insertionTarget) @@ -539,6 +540,15 @@ function insertComponentPickerItem( ] } + if (isWrapTarget(insertionTarget)) { + return [ + wrapInElement(targets, { + element: fixedElement, + importsToAdd: toInsert.importsToAdd, + }), + ] + } + if (!isConditionalTarget(insertionTarget)) { return [insertJSXElement(fixedElement, firstTarget, toInsert.importsToAdd ?? undefined)] } @@ -586,7 +596,7 @@ function insertComponentPickerItem( ...elementToInsert, uid: generateUidWithExistingComponents(projectContents), }, - importsToAdd: emptyImports(), + importsToAdd: toInsert.importsToAdd, }), ] } @@ -657,6 +667,8 @@ function insertPreferredChild( preferredChildToInsert: ElementToInsert, targets: ElementPath[], projectContents: ProjectContentTreeRoot, + allElementProps: AllElementProps, + propertyControlsInfo: PropertyControlsInfo, metadata: ElementInstanceMetadataMap, pathTrees: ElementPathTrees, dispatch: EditorDispatch, @@ -677,6 +689,8 @@ function insertPreferredChild( toInsert, targets, projectContents, + allElementProps, + propertyControlsInfo, metadata, pathTrees, dispatch, @@ -750,6 +764,8 @@ const ComponentPickerContextMenuSimple = React.memo state.editor.projectContents) + const allElementPropsRef = useRefEditorState((state) => state.editor.allElementProps) + const propertyControlsInfoRef = useRefEditorState((state) => state.editor.propertyControlsInfo) const metadataRef = useRefEditorState((state) => state.editor.jsxMetadata) const elementPathTreesRef = useRefEditorState((state) => state.editor.elementPathTree) @@ -759,12 +775,23 @@ const ComponentPickerContextMenuSimple = React.memo(null) @@ -855,6 +882,8 @@ const ComponentPickerContextMenuFull = React.memo state.editor.projectContents) + const allElementPropsRef = useRefEditorState((state) => state.editor.allElementProps) + const propertyControlsInfoRef = useRefEditorState((state) => state.editor.propertyControlsInfo) const metadataRef = useRefEditorState((state) => state.editor.jsxMetadata) const elementPathTreesRef = useRefEditorState((state) => state.editor.elementPathTree) @@ -871,6 +900,8 @@ const ComponentPickerContextMenuFull = React.memo { const PreferredChildComponents = [ @@ -1418,6 +1422,7 @@ export const Column = () => ( const expectedOutput = formatTestProjectCode(` import * as React from 'react' import { Storyboard } from 'utopia-api' + import { Placeholder } from 'utopia-api' export const Card = (props) => { return ( @@ -1464,39 +1469,273 @@ export const Column = () => ( it('Works when using the context menu', async () => { const editor = await renderTestEditorWithModel(TestProject, 'await-first-dom-report') await selectComponentsForTest(editor, [target]) - - const navigatorRow = editor.renderedDOM.getByTestId(NavigatorContainerId) - - await act(async () => { - fireEvent( - navigatorRow, - new MouseEvent('contextmenu', { - bubbles: true, - cancelable: true, - clientX: 3, - clientY: 3, - buttons: 0, - button: 2, - }), - ) - }) - - await editor.getDispatchFollowUpActionsFinished() - - const replaceButton = await waitFor(() => editor.renderedDOM.getByText('Wrap in…')) - await mouseClickAtPoint(replaceButton, { x: 3, y: 3 }) - - const menuButton = await waitFor(() => editor.renderedDOM.getByText('List')) - await mouseClickAtPoint(menuButton, { x: 3, y: 3 }) - - await editor.getDispatchFollowUpActionsFinished() - + await openContextMenuAndClick(editor, [{ text: 'Wrap in…' }, { text: 'List' }]) expect(getPrintedUiJsCodeWithoutUIDs(editor.getEditorState(), StoryboardFilePath)).toEqual( expectedOutput, ) }) }) + describe('wrap in div', () => { + const entryPoints = { + 'choose div from dropdown': async (renderResult: EditorRenderResult): Promise => { + await openContextMenuAndClick(renderResult, [ + { text: 'Wrap in…' }, + { text: 'div', testId: 'Div-div' }, + ]) + }, + 'cmd + enter': (): Promise => pressKey('Enter', { modifiers: cmdModifier }), + } + + Object.entries(entryPoints).forEach(([entrypoint, trigger]) => { + describe(`${entrypoint}`, () => { + it('wrap absolute elements', async () => { + const renderResult = await renderTestEditorWithCode( + makeTestProjectCodeWithSnippet(`
+
+
+
`), + 'await-first-dom-report', + ) + await selectComponentsForTest(renderResult, [ + EP.appendNewElementPath(TestScenePath, ['root', 'one']), + EP.appendNewElementPath(TestScenePath, ['root', 'two']), + ]) + await trigger(renderResult) + + expect( + renderResult.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:root', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/wra', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/wra/one', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/wra/two', + ]) + + expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual( + makeTestProjectCodeWithSnippet(`
+
+
+
+
+
`), + ) + }) + it('wrap flex elements', async () => { + const renderResult = await renderTestEditorWithCode( + makeTestProjectCodeWithSnippet(`
+
+
+
+
+
+
`), + 'await-first-dom-report', + ) + await selectComponentsForTest(renderResult, [ + EP.appendNewElementPath(TestScenePath, ['root', 'container', 'one']), + EP.appendNewElementPath(TestScenePath, ['root', 'container', 'two']), + ]) + await trigger(renderResult) + + expect( + renderResult.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:root', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/container', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/container/wra', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/container/wra/one', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/container/wra/two', + 'regular-utopia-storyboard-uid/scene-aaa/app-entity:root/container/three', + ]) + + expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual( + makeTestProjectCodeWithSnippet(`
+
+
+
+
+
+
+
+
`), + ) + }) + }) + }) + }) + describe('Replacing a regular element', () => { const target = EP.fromString('sb/card-with-title') @@ -1905,3 +2144,41 @@ export var storyboard = ( ) ` + +type Selector = { + text: string + testId?: string +} +async function openContextMenuAndClick(editor: EditorRenderResult, selectors: Selector[]) { + const navigatorRow = editor.renderedDOM.getByTestId(NavigatorContainerId) + + await act(async () => { + fireEvent( + navigatorRow, + new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + clientX: 3, + clientY: 3, + buttons: 0, + button: 2, + }), + ) + }) + + await editor.getDispatchFollowUpActionsFinished() + + for (const selector of selectors) { + // we actually need the await here: https://eslint.org/docs/latest/rules/no-await-in-loop#when-not-to-use-it + // eslint-disable-next-line no-await-in-loop + const button = await waitFor(() => + selector.testId != null + ? editor.renderedDOM.getByTestId(selector.testId) + : editor.renderedDOM.getByText(selector.text), + ) + // eslint-disable-next-line no-await-in-loop + await mouseClickAtPoint(button, { x: 3, y: 3 }) + } + + await editor.getDispatchFollowUpActionsFinished() +} diff --git a/editor/src/utils/utils.test-utils.tsx b/editor/src/utils/utils.test-utils.tsx index ff73788ba5f2..2ffa82aed7fa 100644 --- a/editor/src/utils/utils.test-utils.tsx +++ b/editor/src/utils/utils.test-utils.tsx @@ -86,6 +86,7 @@ import { editorStateToElementChildOptic } from '../core/model/common-optics' import { toFirst } from '../core/shared/optics/optic-utilities' import { emptyUiJsxCanvasContextData } from '../components/canvas/ui-jsx-canvas' import type { RenderContext } from '../components/canvas/ui-jsx-canvas-renderer/ui-jsx-canvas-element-renderer-utils' +import { componentPickerFilterInputTestId } from '../components/navigator/navigator-item/component-picker' export const testRenderContext: RenderContext = { rootScope: {}, @@ -639,3 +640,14 @@ export async function searchInFloatingMenu(editor: EditorRenderResult, query: st fireEvent.keyDown(searchBox, { key: 'Enter', keyCode: 13, metaKey: true }) }) } + +export async function searchInComponentPicker(editor: EditorRenderResult, query: string) { + const searchBox = editor.renderedDOM.getByTestId(componentPickerFilterInputTestId) + + await act(() => { + fireEvent.focus(searchBox) + fireEvent.change(searchBox, { target: { value: query } }) + fireEvent.blur(searchBox) + fireEvent.keyDown(searchBox, { key: 'Enter', keyCode: 13, metaKey: true }) + }) +}