From 7893ec4cf75b8c7d6632f890c787e524db97246c Mon Sep 17 00:00:00 2001
From: Balint Gabor <127662+gbalint@users.noreply.github.com>
Date: Thu, 15 Feb 2024 15:42:00 +0100
Subject: [PATCH] Html control with preview (#4908)
* Html control with preview
* Add back commented out test code
---
.../component-section.spec.browser2.tsx | 163 +++++++++++++++++-
.../component-section/component-section.tsx | 3 +
.../property-content-preview.tsx | 18 +-
.../property-control-controls.tsx | 39 ++++-
.../property-control-values.ts | 6 +
.../property-controls-parser.ts | 26 +++
utopia-api/src/property-controls/factories.ts | 15 +-
.../property-controls/property-controls.ts | 11 ++
8 files changed, 255 insertions(+), 26 deletions(-)
diff --git a/editor/src/components/inspector/sections/component-section/component-section.spec.browser2.tsx b/editor/src/components/inspector/sections/component-section/component-section.spec.browser2.tsx
index 9450def833c5..d8fc3bb3fa4b 100644
--- a/editor/src/components/inspector/sections/component-section/component-section.spec.browser2.tsx
+++ b/editor/src/components/inspector/sections/component-section/component-section.spec.browser2.tsx
@@ -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 {
@@ -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 () => {
@@ -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,
@@ -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
+}
+
+var Playground = ({ style }) => {
+ return (
+
+
+
+ )
+}
+
+export var storyboard = (
+
+
+
+
+
+)
+
+registerInternalComponent(Image, {
+ supportsChildren: false,
+ properties: {
+ url: {
+ control: 'string-input',
+ },
+ },
+ variants: [
+ {
+ code: '',
+ },
+ ],
+})
+
+`
+
+const registerInternalComponentProjectWithHtmlProp = `import * as React from 'react'
+import {
+ Storyboard,
+ Scene,
+ registerInternalComponent,
+} from 'utopia-api'
+
+function Title({ text }) {
+ return {text}
+}
+
+var Playground = ({ style }) => {
+ return (
+
+
+
+ )
+}
+
+export var storyboard = (
+
+
+
+
+
+)
+
+registerInternalComponent(Title, {
+ supportsChildren: false,
+ properties: {
+ text: {
+ control: 'html-input',
+ },
+ },
+ variants: [
+ {
+ code: '',
+ },
+ ],
+})
+
+`
+
// const projectWithImage = (imageUrl: string) => `import * as React from 'react'
// import {
// Storyboard,
@@ -838,5 +987,3 @@ export var storyboard = (
// },
// ],
// })
-
-// `
diff --git a/editor/src/components/inspector/sections/component-section/component-section.tsx b/editor/src/components/inspector/sections/component-section/component-section.tsx
index e9c0cfa14120..57f27f4c63cc 100644
--- a/editor/src/components/inspector/sections/component-section/component-section.tsx
+++ b/editor/src/components/inspector/sections/component-section/component-section.tsx
@@ -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'
@@ -147,6 +148,8 @@ const ControlForProp = React.memo((props: ControlForPropProps
case 'string-input':
return
+ case 'html-input':
+ return
case 'style-controls':
return null
case 'vector2':
diff --git a/editor/src/components/inspector/sections/component-section/property-content-preview.tsx b/editor/src/components/inspector/sections/component-section/property-content-preview.tsx
index 9aaa499ddd46..e68d0604b2d9 100644
--- a/editor/src/components/inspector/sections/component-section/property-content-preview.tsx
+++ b/editor/src/components/inspector/sections/component-section/property-content-preview.tsx
@@ -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
- }
- // maybe later we can check more about whether this is an html or not
- return
-})
-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(url)
@@ -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 (
{
propPath: PropertyPath
@@ -466,7 +467,41 @@ export const StringInputPropertyControl = React.memo(
controlStyles={propMetadata.controlStyles}
focus={props.focusOnMount}
/>
-
+
+
+ )
+ },
+)
+
+export const HtmlInputPropertyControl = React.memo(
+ (props: ControlForPropProps) => {
+ const { propName, propMetadata, controlDescription } = props
+
+ const controlId = `${propName}-string-input-property-control`
+ const value = propMetadata.propertyStatus.set ? propMetadata.value : undefined
+
+ const safeValue = value ?? ''
+
+ return (
+
+
+
)
},
diff --git a/editor/src/core/property-controls/property-control-values.ts b/editor/src/core/property-controls/property-control-values.ts
index 7c45de050e5d..6bd22d34dca7 100644
--- a/editor/src/core/property-controls/property-control-values.ts
+++ b/editor/src/core/property-controls/property-control-values.ts
@@ -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':
@@ -330,6 +332,7 @@ export function unwrapperAndParserForPropertyControl(
case 'popuplist':
case 'radio':
case 'string-input':
+ case 'html-input':
case 'style-controls':
case 'vector2':
case 'vector3':
@@ -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':
@@ -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':
diff --git a/editor/src/core/property-controls/property-controls-parser.ts b/editor/src/core/property-controls/property-controls-parser.ts
index a4fd92c9b6c1..85ec7b8b20b8 100644
--- a/editor/src/core/property-controls/property-controls-parser.ts
+++ b/editor/src/core/property-controls/property-controls-parser.ts
@@ -28,6 +28,7 @@ import type {
BasicControlOptions,
ExpressionControlOption,
TupleControlDescription,
+ HtmlInputControlDescription,
} from 'utopia-api/core'
import { parseColor } from '../../components/inspector/common/css-utils'
import type { Parser, ParseResult } from '../../utils/value-parser-utils'
@@ -257,6 +258,29 @@ export function parseStringInputControlDescription(
)
}
+export function parseHtmlInputControlDescription(
+ value: unknown,
+): ParseResult {
+ return applicative5Either(
+ (label, control, placeholder, obscured, visibleByDefault) => {
+ let htmlInputControlDescription: HtmlInputControlDescription = {
+ control: control,
+ }
+ setOptionalProp(htmlInputControlDescription, 'label', label)
+ setOptionalProp(htmlInputControlDescription, 'placeholder', placeholder)
+ setOptionalProp(htmlInputControlDescription, 'obscured', obscured)
+ setOptionalProp(htmlInputControlDescription, 'visibleByDefault', visibleByDefault)
+
+ return htmlInputControlDescription
+ },
+ optionalObjectKeyParser(parseString, 'label')(value),
+ objectKeyParser(parseConstant('html-input'), 'control')(value),
+ optionalObjectKeyParser(parseString, 'placeholder')(value),
+ optionalObjectKeyParser(parseBoolean, 'obscured')(value),
+ optionalObjectKeyParser(parseBoolean, 'visibleByDefault')(value),
+ )
+}
+
export function parseRadioControlDescription(value: unknown): ParseResult {
return applicative4Either(
(label, control, options, visibleByDefault) => {
@@ -657,6 +681,8 @@ function parseRegularControlDescription(value: unknown): ParseResult