Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recognize props passed to a component #4616

Merged
merged 14 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions editor/src/components/canvas/ui/floating-insert-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import type {
InsertableComponent,
InsertableComponentGroup,
InsertableComponentGroupType,
InsertableVariable,
} from '../../shared/project-components'
import {
getInsertableGroupLabel,
getNonEmptyComponentGroups,
isInsertableVariable,
} from '../../shared/project-components'
import { InspectorInputEmotionStyle } from '../../../uuiui/inputs/base-input'
import { optionalMap } from '../../../core/shared/optional-utils'
Expand Down Expand Up @@ -56,14 +58,20 @@ function convertInsertableComponentsToFlatList(
options: componentGroup.insertableComponents.map(
(componentToBeInserted, index): InsertMenuItem => {
const source = index === 0 ? componentGroup.source : null
let label = componentToBeInserted.name
// there is no indentation, so for inner props we want to show here the full path (i.e myObj.myProp)
if (
isInsertableVariable(componentToBeInserted) &&
componentToBeInserted.originalName != null
) {
label = componentToBeInserted.originalName
}
return {
label: componentToBeInserted.name,
label: label,
source: optionalMap(getInsertableGroupLabel, source),
value: {
...componentToBeInserted,
key: `${getInsertableGroupLabel(componentGroup.source)}-${
componentToBeInserted.name
}`,
key: `${getInsertableGroupLabel(componentGroup.source)}-${label}`,
source: source,
},
}
Expand Down Expand Up @@ -125,6 +133,7 @@ export function useGetInsertableComponents(
store.editor.selectedViews[0],
projectContents,
store.editor.variablesInScope,
store.editor.jsxMetadata,
),
'useGetInsertableComponents scopedVariables',
)
Expand Down
5 changes: 4 additions & 1 deletion editor/src/components/editor/canvas-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ export const CanvasToolbarSearch = React.memo((props: CanvasToolbarSearchProps)
onChange={props.actionWith}
options={options}
menuPortalTarget={menuPortalTarget}
filterOption={createFilter({ ignoreAccents: true })}
filterOption={createFilter({
ignoreAccents: true,
stringify: (c) => c.data.source + c.data.label,
})}
styles={{
...componentSelectorStyles,
menuPortal: (styles: CSSObject): CSSObject => {
Expand Down
10 changes: 6 additions & 4 deletions editor/src/components/editor/insertmenu.spec.browser2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { act, fireEvent, screen } from '@testing-library/react'
import { FOR_TESTS_setNextGeneratedUid } from '../../core/model/element-template-utils.test-utils'
import { forceNotNull } from '../../core/shared/optional-utils'
import { CanvasControlsContainerID } from '../canvas/controls/new-canvas-controls'
import { mouseDragFromPointToPoint, mouseMoveToPoint } from '../canvas/event-helpers.test-utils'
import {
mouseDragFromPointToPoint,
mouseMoveToPoint,
pressKey,
} from '../canvas/event-helpers.test-utils'
import type { EditorRenderResult } from '../canvas/ui-jsx.test-utils'
import {
getPrintedUiJsCode,
Expand Down Expand Up @@ -122,9 +126,7 @@ describe('insert menu', () => {
const filterBox = await screen.findByTestId(InsertMenuFilterTestId)
forceNotNull('the filter box must not be null', filterBox)

await act(async () => {
fireEvent.keyDown(filterBox, { key: 'Enter', keycode: 13 })
})
await pressKey('Enter', { targetElement: filterBox })

const targetElement = renderResult.renderedDOM.getByTestId('root')
const targetElementBounds = targetElement.getBoundingClientRect()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ export const canvasSubstateKeys = Object.keys(emptyCanvasSubstate.editor.canvas)
>

// VariablesInScopeSubstate
export const variablesInScopeSubstateKeys = ['variablesInScope', 'selectedViews'] as const
export const variablesInScopeSubstateKeys = [
'variablesInScope',
'selectedViews',
'jsxMetadata',
] as const
const emptyVariablesInScopeSubstate = {
editor: pick(variablesInScopeSubstateKeys, EmptyEditorStateForKeysOnly),
} as const
Expand Down
147 changes: 139 additions & 8 deletions editor/src/components/editor/variablesmenu.spec.browser2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import {
makeTestProjectCodeWithComponentInnards,
makeTestProjectCodeWithSnippet,
renderTestEditorWithCode,
renderTestEditorWithProjectContent,
} from '../canvas/ui-jsx.test-utils'
import * as Prettier from 'prettier'
import * as EP from '../../core/shared/element-path'
import { setPanelVisibility, setRightMenuTab } from './actions/action-creators'
import { RightMenuTab } from './store/editor-state'
import { selectComponentsForTest } from '../../utils/utils.test-utils'
import { BakedInStoryboardUID } from '../../core/model/scene-utils'
import { forceNotNull } from '../../core/shared/optional-utils'
import { InsertMenuFilterTestId } from './insertmenu'
import { codeFile } from '../../core/shared/project-file-types'
import type { ProjectContentTreeRoot } from '../assets'
import { contentsToTree } from '../assets'
import { PrettierConfig } from 'utopia-vscode-common'
import { pressKey } from '../canvas/event-helpers.test-utils'

function getInsertItems() {
return screen.queryAllByTestId(/^insert-item-/gi)
Expand Down Expand Up @@ -57,12 +64,12 @@ describe('variables menu', () => {

await openVariablesMenu(editor)

expect(getInsertItems().length).toEqual(3)
expect(getInsertItems().length).toEqual(4)

document.execCommand('insertText', false, 'myObj.im')

expect(getInsertItems().length).toEqual(1)
expect(getInsertItems()[0].innerText).toEqual('myObj.image')
expect(getInsertItems()[0].innerText).toEqual('image')
})

describe('no match', () => {
Expand Down Expand Up @@ -106,6 +113,57 @@ describe('variables menu', () => {
})
})

describe('props', () => {
it('shows and inserts props in scope', async () => {
const editor = await renderTestEditorWithProjectContent(
makeTestProjectContents(),
'await-first-dom-report',
)

await selectComponentsForTest(editor, [
EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:container`),
])

await openVariablesMenu(editor)

document.execCommand('insertText', false, 'imgProp')
expect(getInsertItems().length).toEqual(1)
expect(getInsertItems()[0].innerText).toEqual('imgProp')

const filterBox = await screen.findByTestId(InsertMenuFilterTestId)
forceNotNull('the filter box must not be null', filterBox)

await pressKey('Enter', { targetElement: filterBox })

expect(getPrintedUiJsCode(editor.getEditorState(), '/src/app.js')).toEqual(
Prettier.format(
`
import * as React from 'react'
export function App({objProp, imgProp, unusedProp}) {
return (
<div
style={{
backgroundColor: '#aaaaaa33',
position: 'absolute',
left: 57,
top: 168,
width: 247,
height: 402,
}}
data-uid='container'
>
<div data-uid='a3d' />
<img src={imgProp} style={{width: 100, height: 100, top:0, left: 0, position: 'absolute'}} data-uid='ele'/>
</div>
)
}
`,
PrettierConfig,
),
)
})
})

describe('insertion', () => {
it('inserts an image from within an object', async () => {
const editor = await renderTestEditorWithCode(
Expand Down Expand Up @@ -140,9 +198,7 @@ describe('variables menu', () => {
const filterBox = await screen.findByTestId(InsertMenuFilterTestId)
forceNotNull('the filter box must not be null', filterBox)

await act(async () => {
fireEvent.keyDown(filterBox, { key: 'Enter', keycode: 13 })
})
await pressKey('Enter', { targetElement: filterBox })

expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(
makeTestProjectCodeWithComponentInnards(`
Expand Down Expand Up @@ -198,9 +254,7 @@ describe('variables menu', () => {
const filterBox = await screen.findByTestId(InsertMenuFilterTestId)
forceNotNull('the filter box must not be null', filterBox)

await act(async () => {
fireEvent.keyDown(filterBox, { key: 'Enter', keycode: 13 })
})
await pressKey('Enter', { targetElement: filterBox })

expect(getPrintedUiJsCode(editor.getEditorState())).toEqual(
makeTestProjectCodeWithComponentInnards(`
Expand All @@ -225,3 +279,80 @@ describe('variables menu', () => {
})
})
})

function makeTestProjectContents(): ProjectContentTreeRoot {
return contentsToTree({
['/package.json']: codeFile(
`
{
"name": "Utopia Project",
"version": "0.1.0",
"utopia": {
"main-ui": "utopia/storyboard.js",
"html": "public/index.html",
"js": "src/index.js"
},
"dependencies": {
"react": "16.13.1",
"react-dom": "16.13.1",
"utopia-api": "0.4.1",
"non-existant-dummy-library": "8.0.27",
"@heroicons/react": "1.0.1",
"@emotion/react": "11.9.3"
}
}`,
null,
),
['/src/app.js']: codeFile(
`
import * as React from 'react'
export function App({objProp, imgProp, unusedProp}) {
return (
<div
style={{
backgroundColor: '#aaaaaa33',
position: 'absolute',
left: 57,
top: 168,
width: 247,
height: 402,
}}
data-uid='container'
>
<div data-uid='a3d' />
</div>
)
}
`,
null,
),
['/utopia/storyboard.js']: codeFile(
`
import * as React from 'react'
import { Scene, Storyboard } from 'utopia-api'
import { App } from '/src/app.js'

export var storyboard = (
<Storyboard data-uid='utopia-storyboard-uid' data-testid='utopia-storyboard-uid'>
<Scene
style={{
width: 744,
height: 1133,
display: 'flex',
flexDirection: 'column',
left: -52,
top: 9,
position: 'absolute',
}}
data-label='Main Scene'
data-uid='scene-aaa'
data-testid='scene-aaa'
>
<App objProp={{key1: 'key1'}} imgProp='test.png' nonExistentProp='non' data-uid='app-entity' data-testid='app-entity' />
</Scene>
</Storyboard>
)`,
null,
),
})
}
47 changes: 40 additions & 7 deletions editor/src/components/editor/variablesmenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ import { Icn, UIRow, UtopiaTheme, useColorTheme } from '../../uuiui'
import { getControlStyles } from '../../uuiui-deps'
import { InspectorInputEmotionStyle } from '../../uuiui/inputs/base-input'
import type { ProjectContentTreeRoot } from '../assets'
import type { InsertableComponent, InsertableComponentGroup } from '../shared/project-components'
import type {
InsertableComponent,
InsertableComponentGroup,
InsertableComponentGroupType,
InsertableVariable,
} from '../shared/project-components'
import { getInsertableGroupLabel } from '../shared/project-components'
import { setRightMenuTab } from './actions/action-creators'
import type { Mode } from './editor-modes'
Expand All @@ -30,6 +35,17 @@ import { optionalMap } from '../../core/shared/optional-utils'

export const VariablesMenuFilterTestId = 'insert-menu-filter'

type InsertMenuVariableItemValue = InsertableVariable & {
source: InsertableComponentGroupType | null
key: string
}

type InsertMenuVariableItem = {
label: string
source: string | null
value: InsertMenuVariableItemValue
}

interface VariablesMenuProps {
mode: Mode
currentlyOpenFilename: string | null
Expand Down Expand Up @@ -74,7 +90,12 @@ export const VariablesMenu = React.memo(() => {
const scopedVariables = useEditorState(
Substores.variablesInScope,
(store) =>
getVariablesInScope(selectedViews[0], projectContents, store.editor.variablesInScope),
getVariablesInScope(
selectedViews[0],
projectContents,
store.editor.variablesInScope,
store.editor.jsxMetadata,
),
'VariablesMenu scopedVariables',
)

Expand All @@ -92,8 +113,19 @@ const Input = (props: InputProps) => {
return <components.Input {...props} data-testid={VariablesMenuFilterTestId} />
}

const iconByType = (insertMenuItem: InsertMenuItem): string => {
const variableType = insertMenuItem.value.metadata?.variableType as string
const BASE_PADDING = 4
const DEPTH_PADDING = 16
function paddingByDepth(insertMenuItem: InsertMenuVariableItem) {
const depth = insertMenuItem.value.depth
const depthValue = depth == null ? 0 : depth
liady marked this conversation as resolved.
Show resolved Hide resolved
return {
padding: BASE_PADDING,
paddingLeft: BASE_PADDING + depthValue * DEPTH_PADDING,
}
}

const iconByType = (insertMenuItem: InsertMenuVariableItem): string => {
const variableType = insertMenuItem.value.variableType

const iconsByType: Record<string, string> = {
string: 'text',
Expand Down Expand Up @@ -123,19 +155,19 @@ const Option = React.memo((props: OptionProps<ComponentOptionItem, false>) => {
rowHeight={'smaller'}
css={{
borderRadius: 2,
padding: 4,
color: isHovered ? colorTheme.dynamicBlue.value : colorTheme.fg1.value,
background: undefined,
gap: 4,
border: '1px solid transparent',
...paddingByDepth(props.data as InsertMenuVariableItem),
liady marked this conversation as resolved.
Show resolved Hide resolved
}}
onMouseEnter={setIsHoveredTrue}
onMouseLeave={setIsHoveredFalse}
data-testid={`insert-item-${props.label}`}
>
<Icn
category='element'
type={iconByType(props.data as InsertMenuItem)}
type={iconByType(props.data as InsertMenuVariableItem)}
color={isHovered ? 'dynamic' : 'main'}
width={18}
height={18}
Expand Down Expand Up @@ -306,7 +338,8 @@ const VariablesMenuInner = React.memo((props: VariablesMenuProps) => {

const filterOption = createFilter({
ignoreAccents: true,
stringify: (c) => c.data.source + c.data.label,
stringify: (c: { data: InsertMenuVariableItem }) =>
c.data.source + c.data.label + c.data.value.originalName,
ignoreCase: true,
trim: true,
matchFrom: 'any',
Expand Down
Loading
Loading