Skip to content

Commit

Permalink
Wrap in List (#5722)
Browse files Browse the repository at this point in the history
**Problem:**
We cannot currently wrap with a List

**Fix:**
This PR fixes wrapping a single element with a list.

**Details:**
1. Adding a List as an option for "Wrap..."
2. Adjust the code so that wrapping in a map (which is currently two `INSERT`s in disguise) will actually `REPLACE` the map contents
3. NOTE - this PR supports a single element wrapping.

For multiple element wrapping see #5696

<video src="https://github.com/concrete-utopia/utopia/assets/7003853/0dfc0062-8c9a-4e19-9bfc-5a138f0a2df3"></video>

- [X] I opened a hydrogen project and it loaded
- [X] I could navigate to various routes in Preview mode

Fixes #5419
  • Loading branch information
liady authored May 23, 2024
1 parent 8101757 commit 17a8370
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 44 deletions.
3 changes: 2 additions & 1 deletion editor/src/components/editor/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
JSXFragment,
TopLevelElement,
JSExpressionOtherJavaScript,
JSXMapExpression,
} from '../../core/shared/element-template'
import type { KeysPressed, Key } from '../../utils/keyboard'
import type { IndexPosition } from '../../utils/utils'
Expand Down Expand Up @@ -526,7 +527,7 @@ export type ResetPins = {
}

export interface WrapInElementWith {
element: JSXElement | JSXConditionalExpression | JSXFragment
element: JSXElement | JSXConditionalExpression | JSXFragment | JSXMapExpression
importsToAdd: Imports
}

Expand Down
111 changes: 89 additions & 22 deletions editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../../../core/model/element-metadata-utils'
import type { InsertChildAndDetails } from '../../../core/model/element-template-utils'
import {
elementPathForNonChildInsertions,
elementPathFromInsertionPath,
findJSXElementChildAtPath,
generateUidWithExistingComponents,
Expand Down Expand Up @@ -152,7 +153,7 @@ import {
} from '../../../core/shared/project-file-types'
import * as PP from '../../../core/shared/property-path'
import { assertNever, fastForEach, getProjectLockedKey, identity } from '../../../core/shared/utils'
import { mergeImports } from '../../../core/workers/common/project-file-utils'
import { emptyImports, mergeImports } from '../../../core/workers/common/project-file-utils'
import type { UtopiaTsWorkers } from '../../../core/workers/common/worker-types'
import type { IndexPosition } from '../../../utils/utils'
import Utils, { absolute } from '../../../utils/utils'
Expand Down Expand Up @@ -495,6 +496,7 @@ import {
insertAsChildTarget,
insertJSXElement,
openCodeEditorFile,
replaceMappedElement,
scrollToPosition,
selectComponents,
setCodeEditorBuildErrors,
Expand Down Expand Up @@ -522,7 +524,7 @@ import { LayoutPropertyList, StyleProperties } from '../../inspector/common/css-
import { isUtopiaCommentFlag, makeUtopiaFlagComment } from '../../../core/shared/comment-flags'
import { modify, toArrayOf } from '../../../core/shared/optics/optic-utilities'
import { fromField, traverseArray } from '../../../core/shared/optics/optic-creators'
import type { InsertionPath } from '../store/insertion-path'
import type { ConditionalClauseInsertBehavior, InsertionPath } from '../store/insertion-path'
import {
commonInsertionPathFromArray,
getElementPathFromInsertionPath,
Expand Down Expand Up @@ -754,6 +756,52 @@ export function editorMoveMultiSelectedTemplates(
}
}

export function replaceInsideMap(
targets: ElementPath[],
intendedParentPath: StaticElementPath,
insertBehavior: ConditionalClauseInsertBehavior,
editor: EditorModel,
): {
editor: EditorModel
newPaths: Array<ElementPath>
} {
const elements: Array<JSXElementChild> = mapDropNulls((path) => {
const instance = MetadataUtils.findElementByElementPath(editor.jsxMetadata, path)
if (instance == null || isLeft(instance.element)) {
return null
}

return instance.element.value
}, targets)

let newPaths: Array<ElementPath> = targets.map((target) =>
elementPathForNonChildInsertions(insertBehavior, intendedParentPath, EP.toUid(target)),
)

// TODO: handle multiple elements - currently we're taking the first one
const elementToReplace = elements.find((element) => isJSXElement(element))

if (elementToReplace != null && isJSXElement(elementToReplace)) {
const editorAfterReplace = UPDATE_FNS.REPLACE_MAPPED_ELEMENT(
replaceMappedElement(elementToReplace, intendedParentPath, emptyImports()),
editor,
)
const updatedEditor = foldAndApplyCommandsSimple(editorAfterReplace, [
...targets.map((path) => deleteElement('always', path)),
])
return {
editor: updatedEditor,
newPaths: newPaths,
}
}

// if we couldn't find the JSXElement to replace, we just return the editor as is
return {
editor: editor,
newPaths: newPaths,
}
}

export function insertIntoWrapper(
targets: ElementPath[],
newParent: InsertionPath,
Expand Down Expand Up @@ -2370,7 +2418,9 @@ export const UPDATE_FNS = {
'found no element path for the storyboard root',
getStoryboardElementPath(editor.projectContents, editor.canvas.openFile?.filename),
)
: EP.parentPath(action.target)
: EP.isIndexedElement(action.target)
? EP.parentPath(action.target)
: action.target

const withNewElement = modifyUnderlyingTarget(
parentPath,
Expand Down Expand Up @@ -2598,30 +2648,47 @@ export const UPDATE_FNS = {
}

const wrapperUID = generateUidWithExistingComponents(editor.projectContents)

const insertionPath = () => {
if (isJSXConditionalExpression(action.whatToWrapWith.element)) {
const behaviour =
action.targets.length === 1
? replaceWithSingleElement()
: replaceWithElementsWrappedInFragmentBehaviour(wrapperUID)

return conditionalClauseInsertionPath(newPath, 'true-case', behaviour)
}
return childInsertionPath(newPath)
const intendedParentPath = EP.dynamicPathToStaticPath(newPath)
const insertionBehavior =
action.targets.length === 1
? replaceWithSingleElement()
: replaceWithElementsWrappedInFragmentBehaviour(wrapperUID)

let insertionResult: {
editor: EditorModel
newPaths: Array<ElementPath>
}

if (isJSXMapExpression(action.whatToWrapWith.element)) {
// in maps we do not insert directly, but replace contents
insertionResult = replaceInsideMap(
orderedActionTargets,
intendedParentPath,
insertionBehavior,
includeToast(detailsOfUpdate, withWrapperViewAdded),
)
} else if (isJSXConditionalExpression(action.whatToWrapWith.element)) {
// for conditionals we're inserting into the true-case according to behavior
insertionResult = insertIntoWrapper(
orderedActionTargets,
conditionalClauseInsertionPath(intendedParentPath, 'true-case', insertionBehavior),
includeToast(detailsOfUpdate, withWrapperViewAdded),
)
} else {
// otherwise we fall back to standard child insertion
insertionResult = insertIntoWrapper(
orderedActionTargets,
childInsertionPath(intendedParentPath),
includeToast(detailsOfUpdate, withWrapperViewAdded),
)
}

const actualInsertionPath = insertionPath()

const { editor: editorWithElementsInserted, newPaths } = insertIntoWrapper(
orderedActionTargets,
actualInsertionPath,
includeToast(detailsOfUpdate, withWrapperViewAdded),
)
const editorWithElementsInserted = insertionResult.editor
const newPaths = insertionResult.newPaths

return {
...editorWithElementsInserted,
selectedViews: [actualInsertionPath.intendedParentPath],
selectedViews: [intendedParentPath],
leftMenu: { visible: editor.leftMenu.visible, selectedTab: LeftMenuTab.Navigator },
highlightedViews: [],
trueUpElementsAfterDomWalkerRuns: [
Expand Down
25 changes: 24 additions & 1 deletion editor/src/components/editor/actions/wrap-unwrap-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
JSXConditionalExpression,
JSXElement,
JSXFragment,
JSXMapExpression,
} from '../../../core/shared/element-template'
import {
JSXElementChild,
Expand Down Expand Up @@ -334,7 +335,7 @@ export function wrapElementInsertions(
editor: EditorState,
targets: Array<ElementPath>,
parentPath: InsertionPath,
rawElementToInsert: JSXElement | JSXFragment | JSXConditionalExpression,
rawElementToInsert: JSXElement | JSXFragment | JSXConditionalExpression | JSXMapExpression,
importsToAdd: Imports,
anyTargetIsARootElement: boolean,
targetThatIsRootElementOfCommonParent: ElementPath | undefined,
Expand Down Expand Up @@ -447,6 +448,28 @@ export function wrapElementInsertions(
return { updatedEditor: editor, newPath: null }
}
}
case 'JSX_MAP_EXPRESSION': {
switch (staticTarget.type) {
case 'CHILD_INSERTION':
return {
updatedEditor: foldAndApplyCommandsSimple(editor, [
addElement('always', staticTarget, elementToInsert, { importsToAdd, indexPosition }),
]),
newPath: newPath,
}
case 'CONDITIONAL_CLAUSE_INSERTION':
const withTargetAdded = insertElementIntoJSXConditional(
editor,
staticTarget,
jsxFragment(elementToInsert.uid, [elementToInsert], false),
importsToAdd,
)
return { updatedEditor: withTargetAdded, newPath: newPath }
default:
// const _exhaustiveCheck: never = staticTarget
return { updatedEditor: editor, newPath: null }
}
}
default:
const _exhaustiveCheck: never = elementToInsert
return { updatedEditor: editor, newPath: null }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ import type { PreferredChildComponentDescriptor } from '../../custom-code/intern
import { fixUtopiaElement, generateConsistentUID } from '../../../core/shared/uid-utils'
import { getAllUniqueUids } from '../../../core/model/get-unique-ids'
import { elementFromInsertMenuItem } from '../../editor/insert-callbacks'
import { ContextMenuWrapper, ContextMenu } from '../../context-menu-wrapper'
import { BodyMenuOpenClass, NO_OP, assertNever } from '../../../core/shared/utils'
import { ContextMenuWrapper } from '../../context-menu-wrapper'
import { BodyMenuOpenClass, assertNever } from '../../../core/shared/utils'
import { type ContextMenuItem } from '../../context-menu-items'
import { FlexRow, Icn, type IcnProps } from '../../../uuiui'
import type {
Expand Down Expand Up @@ -81,6 +81,7 @@ import { notice } from '../../common/notice'
import { generateUidWithExistingComponents } from '../../../core/model/element-template-utils'
import { emptyComments } from 'utopia-shared/src/types'
import { intrinsicHTMLElementNamesThatSupportChildren } from '../../../core/shared/dom-utils'
import { emptyImports } from '../../../core/workers/common/project-file-utils'

type RenderPropTarget = { type: 'render-prop'; prop: string }
type ConditionalTarget = { type: 'conditional'; conditionalCase: ConditionalCase }
Expand Down Expand Up @@ -552,6 +553,34 @@ function insertComponentPickerItem(
]
}

if (isWrapTarget(insertionTarget)) {
const elementToInsert = toInsert.element()
if (
elementToInsert.type === 'JSX_MAP_EXPRESSION' &&
!MetadataUtils.isJSXElement(target, metadata)
) {
return [
showToast(
notice(
'We are working on support to insert Lists, Conditionals and Fragments into Lists',
'INFO',
false,
'wrap-component-picker-item-nested-map',
),
),
]
}
return [
wrapInElement([target], {
element: {
...elementToInsert,
uid: generateUidWithExistingComponents(projectContents),
},
importsToAdd: emptyImports(),
}),
]
}

if (isReplaceTarget(insertionTarget)) {
if (
MetadataUtils.isJSXMapExpression(EP.parentPath(target), metadata) &&
Expand Down Expand Up @@ -767,6 +796,12 @@ const ComponentPickerContextMenuFull = React.memo<ComponentPickerContextMenuProp
'usePreferredChildrenForTarget targetChildren',
)

const isJsxElement = useEditorState(
Substores.metadata,
(store) => MetadataUtils.isJSXElement(target, store.editor.jsxMetadata),
'isJsxElement targetElement',
)

const allInsertableComponents = useGetInsertableComponents('insert').flatMap((group) => {
return {
label: group.label,
Expand All @@ -783,13 +818,18 @@ const ComponentPickerContextMenuFull = React.memo<ComponentPickerContextMenuProp
// If we want to keep the children of this element when it has some, don't include replacements that have children.
return targetChildren.length === 0 || !componentElementToInsertHasChildren(element)
}
if (isWrapTarget(insertionTarget) && element.type === 'JSX_ELEMENT') {
if (isIntrinsicHTMLElement(element.name)) {
if (isWrapTarget(insertionTarget)) {
if (element.type === 'JSX_ELEMENT' && isIntrinsicHTMLElement(element.name)) {
// when it is an intrinsic html element, we check if it supports children from our list
return intrinsicHTMLElementNamesThatSupportChildren.includes(
element.name.baseVariable,
)
}
if (element.type === 'JSX_MAP_EXPRESSION') {
// we cannot currently wrap in List a conditional, fragment or map expression
return isJsxElement
}
return true
}
// Right now we only support inserting JSX elements when we insert into a render prop or when replacing elements
return element.type === 'JSX_ELEMENT'
Expand Down
Loading

0 comments on commit 17a8370

Please sign in to comment.