Skip to content

Commit

Permalink
Collapse / Expand objects in data picker (#4917)
Browse files Browse the repository at this point in the history
Add an object collapse arrow to long or deeply nested objects in data picker.
Currently all objects are collapsed by default, unless they are under two levels of indentation, or have less than four keys.
(in order to keep small/shallow objects visible) 

<video src="https://github.com/concrete-utopia/utopia/assets/7003853/e87508ac-19f0-4f30-87a7-df9f77de6fd6" />
  • Loading branch information
liady authored Feb 19, 2024
1 parent 9333cf6 commit f817830
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { within } from '@testing-library/react'
import * as EP from '../../../../core/shared/element-path'
import { selectComponentsForTest, wait } from '../../../../utils/utils.test-utils'
import { mouseClickAtPoint, pressKey } from '../../../canvas/event-helpers.test-utils'
import type { EditorRenderResult } from '../../../canvas/ui-jsx.test-utils'
import { renderTestEditorWithCode } from '../../../canvas/ui-jsx.test-utils'
import {
DataPickerPopupButtonTestId,
Expand All @@ -23,9 +24,7 @@ describe('Set element prop via the data picker', () => {
const theScene = editor.renderedDOM.getByTestId('scene')
const theInspector = editor.renderedDOM.getByTestId('inspector-sections-container')

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

// the items from the data picker are expected here, so that the numbers in `VariableFromScopeOptionTestId`
// below are put in context
Expand Down Expand Up @@ -82,9 +81,7 @@ describe('Set element prop via the data picker', () => {
const dataPickerPopup = editor.renderedDOM.queryByTestId(DataPickerPopupTestId)
expect(dataPickerPopup).not.toBeNull()

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

expect(options).toEqual([
'currentCount', // at the top because the number input control descriptor is present
Expand All @@ -107,9 +104,7 @@ describe('Set element prop via the data picker', () => {
const dataPickerPopup = editor.renderedDOM.queryByTestId(DataPickerPopupTestId)
expect(dataPickerPopup).not.toBeNull()

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

expect(options).toEqual([
'titleIdeas', // the array is at the top because of the array descriptor
Expand Down Expand Up @@ -137,9 +132,7 @@ describe('Set element prop via the data picker', () => {
const dataPickerPopup = editor.renderedDOM.queryByTestId(DataPickerPopupTestId)
expect(dataPickerPopup).not.toBeNull()

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

expect(options).toEqual([
'bookInfo', // object is at the top because of the object descriptor
Expand Down Expand Up @@ -191,9 +184,7 @@ describe('Set element prop via the data picker', () => {
const dataPickerPopup = editor.renderedDOM.queryByTestId(DataPickerPopupTestId)
expect(dataPickerPopup).not.toBeNull()

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

expect(options).toEqual([
'authors', // at the top because it's an array of string, same as titleIdeas
Expand Down Expand Up @@ -255,9 +246,7 @@ describe('Set element prop via the data picker', () => {
const dataPickerPopup = editor.renderedDOM.queryByTestId(DataPickerPopupTestId)
expect(dataPickerPopup).not.toBeNull()

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

expect(options).toEqual([
'bookInfo', // object with matching shape
Expand Down Expand Up @@ -314,9 +303,7 @@ describe('Set element prop via the data picker', () => {
const dataPickerPopup = editor.renderedDOM.queryByTestId(DataPickerPopupTestId)
expect(dataPickerPopup).not.toBeNull()

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

expect(options).toEqual([
'authors',
Expand Down Expand Up @@ -366,9 +353,7 @@ describe('Set element prop via the data picker', () => {
const dataPickerPopup = editor.renderedDOM.queryByTestId(DataPickerPopupTestId)
expect(dataPickerPopup).not.toBeNull()

const options = [
...editor.renderedDOM.baseElement.querySelectorAll(`[data-testid^="variable-from-scope"]`),
].map((node) => node.firstChild!.firstChild!.textContent)
const options = getRenderedOptions(editor)

expect(options).toEqual([
'authors',
Expand Down Expand Up @@ -978,3 +963,11 @@ registerInternalComponent(Title, {
// },
// ],
// })

function getRenderedOptions(editor: EditorRenderResult) {
return [
...editor.renderedDOM.baseElement.querySelectorAll(
`[data-testid^="variable-from-scope"] [data-testid="variable-name"]`,
),
].map((node) => node.textContent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,14 @@ interface ValueRowProps {
function ValueRow({ variableOption, idx, onTweakProperty }: ValueRowProps) {
const colorTheme = useColorTheme()
const [selectedIndex, setSelectedIndex] = React.useState<number>(0)
const childrenLength = variableOption.variableChildren?.length ?? 0
const [childrenOpen, setChildrenOpen] = React.useState<boolean>(
variableOption.depth < 2 || childrenLength < 4,
)
const totalChildCount = variableOption.variableChildren?.length ?? 0
const toggleChildrenOpen = useCallback(() => {
setChildrenOpen(!childrenOpen)
}, [childrenOpen, setChildrenOpen])

const {
variableName,
Expand All @@ -138,6 +145,7 @@ function ValueRow({ variableOption, idx, onTweakProperty }: ValueRowProps) {
const stopPropagation = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
}, [])
const hasObjectChildren = variableChildren != null && variableChildren.length > 0 && !isArray
const shouldDim = depth > 0 || !valueMatchesPropType
return (
<>
Expand Down Expand Up @@ -169,25 +177,14 @@ function ValueRow({ variableOption, idx, onTweakProperty }: ValueRowProps) {
maxWidth: '100%',
}}
>
{depth > 0 ? (
<span
style={{
borderLeft: `1px solid ${colorTheme.neutralBorder.value}`,
borderBottom: `1px solid ${colorTheme.neutralBorder.value}`,
width: 5,
display: 'inline-block',
height: 10,
marginRight: 4,
position: 'relative',
top: 0,
marginLeft: (depth - 1) * 8,
flex: 'none',
textOverflow: 'ellipsis',
overflow: 'hidden',
}}
></span>
) : null}
<PrefixIcon
depth={depth}
hasObjectChildren={hasObjectChildren}
onIconClick={toggleChildrenOpen}
open={childrenOpen}
/>
<span
data-testid='variable-name'
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
Expand Down Expand Up @@ -237,7 +234,7 @@ function ValueRow({ variableOption, idx, onTweakProperty }: ValueRowProps) {
idx={`${idx}-${selectedIndex}`}
onTweakProperty={onTweakProperty}
/>
) : (
) : childrenOpen ? (
variableChildren.map((child, index) => {
return (
<ValueRow
Expand All @@ -248,12 +245,65 @@ function ValueRow({ variableOption, idx, onTweakProperty }: ValueRowProps) {
/>
)
})
)
) : null
) : null}
</>
)
}

function PrefixIcon({
depth,
hasObjectChildren,
onIconClick,
open,
}: {
depth: number
hasObjectChildren: boolean
onIconClick: () => void
open: boolean
}) {
const colorTheme = useColorTheme()
const style = {
width: 5,
display: 'inline-block',
height: 10,
marginRight: 4,
position: 'relative',
top: 0,
marginLeft: (depth - 1) * 8,
flex: 'none',
} as const
const onClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
onIconClick()
},
[onIconClick],
)
if (hasObjectChildren) {
return (
<span
style={{ color: colorTheme.neutralBorder.value, fontSize: 6, ...style }}
onClick={onClick}
>
{open ? '▼' : '▶'}
</span>
)
}
if (depth > 0) {
return (
<span
style={{
borderLeft: `1px solid ${colorTheme.neutralBorder.value}`,
borderBottom: `1px solid ${colorTheme.neutralBorder.value}`,
...style,
}}
></span>
)
}
return null
}

function ArrayPaginator({
selectedIndex,
totalChildCount,
Expand Down

0 comments on commit f817830

Please sign in to comment.