@@ -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 })
+ })
+}