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

Html control with preview #4908

Merged
merged 2 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@ -53,31 +53,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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's slowly getting worse, this test :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as if to prove my point that testing what is in the UI is not the way to go


// 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(13))
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(14))
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(16))
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 @@ -345,6 +345,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 @@ -673,6 +684,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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't name this Hello Utopia, just to avoid the test text matcher being confused in the future

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 @@ -739,5 +888,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
Loading