From 650ff2008d4a55291236be26acbe7d65ea7e3a78 Mon Sep 17 00:00:00 2001
From: Balint Gabor <127662+gbalint@users.noreply.github.com>
Date: Thu, 15 Feb 2024 13:22:51 +0100
Subject: [PATCH 1/2] Html control with preview
---
.../component-section.spec.browser2.tsx | 226 ++++++++++++------
.../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, 253 insertions(+), 91 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 52388326b4e0..7065353d4551 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 {
@@ -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()
// 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 () => {
@@ -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,
@@ -673,71 +684,140 @@ export var storyboard = (
)`
}
-// const projectWithImage = (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 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: '',
+ },
+ ],
+})
+
+`
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
Date: Thu, 15 Feb 2024 13:25:17 +0100
Subject: [PATCH 2/2] Add back commented out test code
---
.../component-section.spec.browser2.tsx | 67 +++++++++++++++++++
1 file changed, 67 insertions(+)
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 7065353d4551..659fd0e1eaad 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
@@ -821,3 +821,70 @@ registerInternalComponent(Title, {
})
`
+
+// const projectWithImage = (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: '',
+// },
+// ],
+// })