-
Notifications
You must be signed in to change notification settings - Fork 172
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Children row in inspector props (#5839)
**Problem:** The children prop for components is not shown in the inspector. **Fix:** - Show the `children` prop in the inspector and allow changing it, via either a cartouche or number/string inputs - Allow setting children elements when asking for a prop update - For number and string children, use string/number inputs - or the default JSX component (or cartouche for data references) otherwise - If the selected component does not support children, don't show it https://github.com/concrete-utopia/utopia/assets/1081051/a66df108-2812-4c7d-9d65-aa83f8641bc6 **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Preview mode Fixes #5686
- Loading branch information
Showing
10 changed files
with
526 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import type { ElementPath } from 'utopia-shared/src/types' | ||
import { MetadataUtils } from '../../core/model/element-metadata-utils' | ||
import { getRegisteredComponent } from '../../core/property-controls/property-controls-utils' | ||
import { intrinsicHTMLElementNamesThatSupportChildren } from '../../core/shared/dom-utils' | ||
import type { | ||
ElementInstanceMetadataMap, | ||
JSXElementChild, | ||
} from '../../core/shared/element-template' | ||
import { | ||
isIntrinsicHTMLElement, | ||
getJSXElementNameAsString, | ||
} from '../../core/shared/element-template' | ||
import type { PropertyControlsInfo } from '../custom-code/code-file' | ||
import type { EditorAction } from './action-types' | ||
import { deleteView, replaceElementInScope } from './actions/action-creators' | ||
import * as EP from '../../core/shared/element-path' | ||
|
||
export function elementSupportsChildrenFromPropertyControls( | ||
metadata: ElementInstanceMetadataMap, | ||
propertyControlsInfo: PropertyControlsInfo, | ||
target: ElementPath, | ||
) { | ||
const targetElement = MetadataUtils.findElementByElementPath(metadata, target) | ||
|
||
const targetJSXElement = MetadataUtils.getJSXElementFromElementInstanceMetadata(targetElement) | ||
if (targetJSXElement == null) { | ||
// this should not happen, erring on the side of true | ||
return true | ||
} | ||
if (isIntrinsicHTMLElement(targetJSXElement.name)) { | ||
// when it is an intrinsic html element, we check if it supports children from our list | ||
return intrinsicHTMLElementNamesThatSupportChildren.includes(targetJSXElement.name.baseVariable) | ||
} | ||
|
||
const elementImportInfo = targetElement?.importInfo | ||
if (elementImportInfo == null) { | ||
// erring on the side of true | ||
return true | ||
} | ||
|
||
const targetName = getJSXElementNameAsString(targetJSXElement.name) | ||
const registeredComponent = getRegisteredComponent( | ||
targetName, | ||
elementImportInfo.filePath, | ||
propertyControlsInfo, | ||
) | ||
if (registeredComponent == null) { | ||
// when there is no component annotation default is supporting children | ||
return true | ||
} | ||
|
||
return registeredComponent.supportsChildren | ||
} | ||
|
||
export function replaceFirstChildAndDeleteSiblings( | ||
target: ElementPath, | ||
children: JSXElementChild[], | ||
replaceWith: JSXElementChild, | ||
): EditorAction[] { | ||
return [ | ||
// replace the first child | ||
replaceElementInScope(target, { | ||
type: 'replace-child-with-uid', | ||
uid: children[0].uid, | ||
replaceWith: replaceWith, | ||
}), | ||
// get rid of all the others | ||
...children.slice(1).map((child) => deleteView(EP.appendToPath(target, child.uid))), | ||
] | ||
} | ||
|
||
const reNumericComponents = /[0-9\._]/g | ||
|
||
export function childrenAreProbablyNumericExpression(children: JSXElementChild[]) { | ||
return children.some( | ||
(c) => | ||
c.type === 'ATTRIBUTE_OTHER_JAVASCRIPT' && | ||
c.originalJavascript.trim().replace(reNumericComponents, '').length > 0, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
editor/src/components/inspector/sections/component-section/component-section-children.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import React from 'react' | ||
import type { | ||
ControlDescription, | ||
RegularControlDescription, | ||
} from '../../../custom-code/internal-property-controls' | ||
import type { ElementPath } from '../../../../core/shared/project-file-types' | ||
import * as PP from '../../../../core/shared/property-path' | ||
import { iconForControlType } from '../../../../uuiui' | ||
import type { ControlForPropProps } from './property-control-controls' | ||
import { StringInputPropertyControl } from './property-control-controls' | ||
import { Substores, useEditorState } from '../../../editor/store/store-hook' | ||
import { MetadataUtils } from '../../../../core/model/element-metadata-utils' | ||
import type { | ||
ElementInstanceMetadataMap, | ||
JSXElementChild, | ||
} from '../../../../core/shared/element-template' | ||
import { isJSXElement, isJSXTextBlock } from '../../../../core/shared/element-template' | ||
import { IdentifierExpressionCartoucheControl } from './cartouche-control' | ||
import { renderedAtChildNode } from '../../../editor/store/editor-state' | ||
import { isRight } from '../../../../core/shared/either' | ||
import { DataReferenceCartoucheControl } from './data-reference-cartouche' | ||
import { assertNever } from '../../../../core/shared/utils' | ||
import { childrenAreProbablyNumericExpression } from '../../../editor/element-children' | ||
import type { CartoucheDataType } from './cartouche-ui' | ||
|
||
export function useChildrenPropOverride( | ||
props: ControlForPropProps<RegularControlDescription> & { | ||
onDeleteCartouche: () => void | ||
safeToDelete: boolean | ||
dataTypeForExpression: CartoucheDataType | ||
}, | ||
) { | ||
const childrenContent = useEditorState( | ||
Substores.metadata, | ||
(store) => { | ||
return getMaybeChildrenContent( | ||
store.editor.jsxMetadata, | ||
props.elementPath, | ||
props.propName, | ||
props.controlDescription, | ||
) | ||
}, | ||
'useGetMaybeChildrenOverride maybeChildrenDataRefElement', | ||
) | ||
|
||
if (childrenContent == null) { | ||
return null | ||
} | ||
|
||
switch (childrenContent.type) { | ||
case 'cartouche': | ||
return ( | ||
<DataReferenceCartoucheControl | ||
elementPath={props.elementPath} | ||
renderedAt={renderedAtChildNode(props.elementPath, childrenContent.value.uid)} | ||
surroundingScope={props.elementPath} | ||
childOrAttribute={childrenContent.value} | ||
selected={false} | ||
/> | ||
) | ||
case 'expression': | ||
return ( | ||
<IdentifierExpressionCartoucheControl | ||
contents={'Expression'} | ||
icon={React.createElement(iconForControlType('none'))} | ||
matchType='partial' | ||
onOpenDataPicker={props.onOpenDataPicker} | ||
onDeleteCartouche={props.onDeleteCartouche} | ||
testId={`cartouche-${PP.toString(props.propPath)}`} | ||
propertyPath={props.propPath} | ||
safeToDelete={props.safeToDelete} | ||
elementPath={props.elementPath} | ||
datatype={props.dataTypeForExpression} | ||
/> | ||
) | ||
case 'text': | ||
return ( | ||
<StringInputPropertyControl | ||
{...props} | ||
controlDescription={ | ||
props.controlDescription.control === 'string-input' | ||
? props.controlDescription | ||
: { | ||
control: 'string-input', | ||
label: 'hey', | ||
} | ||
} | ||
/> | ||
) | ||
default: | ||
assertNever(childrenContent) | ||
} | ||
} | ||
|
||
function getMaybeChildrenContent( | ||
jsxMetadata: ElementInstanceMetadataMap, | ||
elementPath: ElementPath, | ||
propName: string, | ||
controlDescription: ControlDescription, | ||
): | ||
| { type: 'cartouche'; value: JSXElementChild } | ||
| { type: 'text' } | ||
| { type: 'expression' } | ||
| null { | ||
if (propName !== 'children') { | ||
return null | ||
} | ||
|
||
const element = MetadataUtils.findElementByElementPath(jsxMetadata, elementPath) | ||
if (element != null && isRight(element.element) && isJSXElement(element.element.value)) { | ||
const { children } = element.element.value | ||
if (children.every((child) => isJSXTextBlock(child))) { | ||
return { type: 'text' } | ||
} else if ( | ||
controlDescription.control === 'number-input' && | ||
childrenAreProbablyNumericExpression(children) | ||
) { | ||
return { type: 'expression' } | ||
} else if ( | ||
children.length === 1 && | ||
MetadataUtils.isElementDataReference(children[0]) && | ||
children[0].type !== 'ATTRIBUTE_OTHER_JAVASCRIPT' | ||
) { | ||
return { type: 'cartouche', value: children[0] } | ||
} | ||
} | ||
|
||
return null | ||
} |
Oops, something went wrong.