Skip to content

Commit

Permalink
Html control with preview (#4908)
Browse files Browse the repository at this point in the history
* Html control with preview

* Add back commented out test code
  • Loading branch information
gbalint authored Feb 15, 2024
1 parent 7d4cdf1 commit 7893ec4
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { within } from '@testing-library/react'
import * as EP from '../../../../core/shared/element-path'
import { selectComponentsForTest } from '../../../../utils/utils.test-utils'
import { selectComponentsForTest, wait } from '../../../../utils/utils.test-utils'
import { mouseClickAtPoint, pressKey } from '../../../canvas/event-helpers.test-utils'
import { renderTestEditorWithCode } from '../../../canvas/ui-jsx.test-utils'
import {
Expand Down Expand Up @@ -43,31 +43,31 @@ describe('Set element prop via the data picker', () => {
let currentOption = editor.renderedDOM.getByTestId(VariableFromScopeOptionTestId(0))
await mouseClickAtPoint(currentOption, { x: 2, y: 2 })
expect(within(theScene).queryByText('Title too')).not.toBeNull()
expect(within(theInspector).queryAllByText('Title too')).toHaveLength(2)
expect(within(theInspector).queryByText('Title too')).not.toBeNull()

// choose another string-valued variable
currentOption = editor.renderedDOM.getByTestId(VariableFromScopeOptionTestId(1))
await mouseClickAtPoint(currentOption, { x: 2, y: 2 })
expect(within(theScene).queryByText('Alternate title')).not.toBeNull()
expect(within(theInspector).queryAllByText('Alternate title')).toHaveLength(2)
expect(within(theInspector).queryByText('Alternate title')).not.toBeNull()

// choose an object prop
currentOption = editor.renderedDOM.getByTestId(VariableFromScopeOptionTestId(3))
await mouseClickAtPoint(currentOption, { x: 2, y: 2 })
expect(within(theScene).queryByText('The First Title')).not.toBeNull()
expect(within(theInspector).queryAllByText('The First Title')).toHaveLength(2)
expect(within(theInspector).queryByText('The First Title')).not.toBeNull()

// choose an object prop
currentOption = editor.renderedDOM.getByTestId(VariableFromScopeOptionTestId(4))
await mouseClickAtPoint(currentOption, { x: 2, y: 2 })
expect(within(theScene).queryByText('Sweet')).not.toBeNull()
expect(within(theInspector).queryAllByText('Sweet')).toHaveLength(2)
expect(within(theInspector).queryByText('Sweet')).not.toBeNull()

// choose an array element
currentOption = editor.renderedDOM.getByTestId(VariableFromScopeOptionTestId(6))
await mouseClickAtPoint(currentOption, { x: 2, y: 2 })
expect(within(theScene).queryByText('Chapter One')).not.toBeNull()
expect(within(theInspector).queryAllByText('Chapter One')).toHaveLength(2)
expect(within(theInspector).queryByText('Chapter One')).not.toBeNull()
})

it('with number input control descriptor present', async () => {
Expand Down Expand Up @@ -442,6 +442,17 @@ describe('Controls from registering components', () => {
expect(within(theScene).queryByText('New title')).not.toBeNull()
})

it('registering internal component with html prop shows preview', async () => {
const editor = await renderTestEditorWithCode(
registerInternalComponentProjectWithHtmlProp,
'await-first-dom-report',
)
await selectComponentsForTest(editor, [EP.fromString('sb/scene/pg:root/title')])

const theInspector = editor.renderedDOM.getByTestId('inspector-sections-container')
expect(within(theInspector).queryByText('Hello Utopia')).not.toBeNull()
})

it('registering external component', async () => {
const editor = await renderTestEditorWithCode(
registerExternalComponentProject,
Expand Down Expand Up @@ -772,6 +783,144 @@ export var storyboard = (
)`
}

const projectWithHtmlProp = (imageUrl: string) => `import * as React from 'react'
import {
Storyboard,
Scene,
registerInternalComponent,
} from 'utopia-api'
function Image({ url }) {
return <img src={url} />
}
var Playground = ({ style }) => {
return (
<div style={style} data-uid='root'>
<Image url='${imageUrl}' data-uid='image' />
</div>
)
}
export var storyboard = (
<Storyboard data-uid='sb'>
<Scene
style={{
width: 521,
height: 266,
position: 'absolute',
left: 554,
top: 247,
backgroundColor: 'white',
}}
data-uid='scene'
data-testid='scene'
commentId='120'
>
<Playground
style={{
width: 454,
height: 177,
position: 'absolute',
left: 34,
top: 44,
backgroundColor: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title='Hello Utopia'
data-uid='pg'
/>
</Scene>
</Storyboard>
)
registerInternalComponent(Image, {
supportsChildren: false,
properties: {
url: {
control: 'string-input',
},
},
variants: [
{
code: '<Image />',
},
],
})
`

const registerInternalComponentProjectWithHtmlProp = `import * as React from 'react'
import {
Storyboard,
Scene,
registerInternalComponent,
} from 'utopia-api'
function Title({ text }) {
return <h2 data-uid='0cd'>{text}</h2>
}
var Playground = ({ style }) => {
return (
<div style={style} data-uid='root'>
<Title text='<p>Hello Utopia</p>' data-uid='title' />
</div>
)
}
export var storyboard = (
<Storyboard data-uid='sb'>
<Scene
style={{
width: 521,
height: 266,
position: 'absolute',
left: 554,
top: 247,
backgroundColor: 'white',
}}
data-uid='scene'
data-testid='scene'
commentId='120'
>
<Playground
style={{
width: 454,
height: 177,
position: 'absolute',
left: 34,
top: 44,
backgroundColor: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title='Hello Utopia'
data-uid='pg'
/>
</Scene>
</Storyboard>
)
registerInternalComponent(Title, {
supportsChildren: false,
properties: {
text: {
control: 'html-input',
},
},
variants: [
{
code: '<Title />',
},
],
})
`

// const projectWithImage = (imageUrl: string) => `import * as React from 'react'
// import {
// Storyboard,
Expand Down Expand Up @@ -838,5 +987,3 @@ export var storyboard = (
// },
// ],
// })

// `
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
ExpressionInputPropertyControl,
StringInputPropertyControl,
VectorPropertyControl,
HtmlInputPropertyControl,
} from './property-control-controls'
import { ExpandableIndicator } from '../../../navigator/navigator-item/expandable-indicator'
import { unless, when } from '../../../../utils/react-conditionals'
Expand Down Expand Up @@ -147,6 +148,8 @@ const ControlForProp = React.memo((props: ControlForPropProps<BaseControlDescrip
return <RadioPropertyControl {...props} controlDescription={controlDescription} />
case 'string-input':
return <StringInputPropertyControl {...props} controlDescription={controlDescription} />
case 'html-input':
return <HtmlInputPropertyControl {...props} controlDescription={controlDescription} />
case 'style-controls':
return null
case 'vector2':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,11 @@ import sanitizeHtml from 'sanitize-html'
import { isImage } from '../../../../core/shared/utils'
export const ImagePreviewTestId = 'image-preview'

interface ContentPreviewProps {
text: string
}
export const ContentPreview = React.memo(({ text }: ContentPreviewProps) => {
if (isImage(text)) {
return <ImagePreview url={text} />
}
// maybe later we can check more about whether this is an html or not
return <HtmlPreview html={text} />
})
ContentPreview.displayName = 'ContentPreview'

interface ImagePreviewProps {
url: string
}
const ImagePreview = React.memo(({ url }: ImagePreviewProps) => {
const [imageCanBeLoaded, setImageCanBeLoaded] = React.useState(true)
export const ImagePreview = React.memo(({ url }: ImagePreviewProps) => {
const [imageCanBeLoaded, setImageCanBeLoaded] = React.useState(isImage(url))

// we need to track if the url has changed so we retry loading the image even if it failed before
const urlRef = React.useRef<string>(url)
Expand Down Expand Up @@ -52,7 +40,7 @@ interface HtmlPreviewProps {
html: string
}

const HtmlPreview = React.memo(({ html }: HtmlPreviewProps) => {
export const HtmlPreview = React.memo(({ html }: HtmlPreviewProps) => {
const sanitizedHtml = sanitizeHtml(html)
return (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
EulerControlDescription,
ExpressionInputControlDescription,
ExpressionPopUpListControlDescription,
HtmlInputControlDescription,
Matrix3ControlDescription,
Matrix4ControlDescription,
NumberInputControlDescription,
Expand Down Expand Up @@ -55,7 +56,7 @@ import {
normalisePathToUnderlyingTarget,
} from '../../../custom-code/code-file'
import { useDispatch } from '../../../editor/store/dispatch-context'
import { ContentPreview } from './property-content-preview'
import { HtmlPreview, ImagePreview } from './property-content-preview'

export interface ControlForPropProps<T extends BaseControlDescription> {
propPath: PropertyPath
Expand Down Expand Up @@ -466,7 +467,41 @@ export const StringInputPropertyControl = React.memo(
controlStyles={propMetadata.controlStyles}
focus={props.focusOnMount}
/>
<ContentPreview text={safeValue} />
<ImagePreview url={safeValue} />
</div>
)
},
)

export const HtmlInputPropertyControl = React.memo(
(props: ControlForPropProps<HtmlInputControlDescription>) => {
const { propName, propMetadata, controlDescription } = props

const controlId = `${propName}-string-input-property-control`
const value = propMetadata.propertyStatus.set ? propMetadata.value : undefined

const safeValue = value ?? ''

return (
<div
style={{
display: 'flex',
flexDirection: 'column',
flexBasis: 0,
gap: 5,
}}
>
<StringControl
key={controlId}
id={controlId}
testId={controlId}
value={safeValue}
onSubmitValue={propMetadata.onSubmitValue}
controlStatus={propMetadata.controlStatus}
controlStyles={propMetadata.controlStyles}
focus={props.focusOnMount}
/>
<HtmlPreview html={safeValue} />
</div>
)
},
Expand Down
6 changes: 6 additions & 0 deletions editor/src/core/property-controls/property-control-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ export function unwrapperAndParserForBaseControl(
return defaultUnwrapFirst(parseAny)
case 'string-input':
return defaultUnwrapFirst(parseString)
case 'html-input':
return defaultUnwrapFirst(parseString)
case 'style-controls':
return defaultUnwrapFirst(parseAny)
case 'vector2':
Expand Down Expand Up @@ -330,6 +332,7 @@ export function unwrapperAndParserForPropertyControl(
case 'popuplist':
case 'radio':
case 'string-input':
case 'html-input':
case 'style-controls':
case 'vector2':
case 'vector3':
Expand Down Expand Up @@ -454,6 +457,8 @@ export function printerForBasePropertyControl(control: BaseControlDescription):
return printSimple
case 'string-input':
return printSimple
case 'html-input':
return printSimple
case 'style-controls':
return printSimple
case 'vector2':
Expand Down Expand Up @@ -535,6 +540,7 @@ export function printerForPropertyControl(control: RegularControlDescription): P
case 'popuplist':
case 'radio':
case 'string-input':
case 'html-input':
case 'style-controls':
case 'vector2':
case 'vector3':
Expand Down
Loading

0 comments on commit 7893ec4

Please sign in to comment.