From b56f85f36af879423cb5e6bbc5f6c0749073ae11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:04:52 +0100 Subject: [PATCH] Allow video, audio and iFrame in front components (#18200) Allow video, audio and iFrame in front components + fix css style properties starting with `--` --- .../host/generated/host-component-registry.ts | 10 ++ .../host/utils/createHtmlHostWrapper.ts | 20 ++- .../src/front-component-renderer/index.ts | 12 ++ .../remote/generated/remote-components.ts | 108 ++++++++++++++++ .../remote/generated/remote-elements.ts | 122 ++++++++++++++++++ .../constants/AllowedHtmlElements.ts | 52 ++++++++ 6 files changed, 319 insertions(+), 5 deletions(-) diff --git a/packages/twenty-sdk/src/front-component-renderer/host/generated/host-component-registry.ts b/packages/twenty-sdk/src/front-component-renderer/host/generated/host-component-registry.ts index ea4e485b16..dd7bf4dc1e 100644 --- a/packages/twenty-sdk/src/front-component-renderer/host/generated/host-component-registry.ts +++ b/packages/twenty-sdk/src/front-component-renderer/host/generated/host-component-registry.ts @@ -82,6 +82,16 @@ export const componentRegistry: Map = new Map([ ['html-td', createRemoteComponentRenderer(createHtmlHostWrapper('td'))], ['html-br', createRemoteComponentRenderer(createHtmlHostWrapper('br'))], ['html-hr', createRemoteComponentRenderer(createHtmlHostWrapper('hr'))], + [ + 'html-iframe', + createRemoteComponentRenderer(createHtmlHostWrapper('iframe')), + ], + ['html-video', createRemoteComponentRenderer(createHtmlHostWrapper('video'))], + ['html-audio', createRemoteComponentRenderer(createHtmlHostWrapper('audio'))], + [ + 'html-source', + createRemoteComponentRenderer(createHtmlHostWrapper('source')), + ], ['remote-style', createRemoteComponentRenderer(RemoteStyleRenderer)], ['remote-fragment', RemoteFragmentRenderer], ]); diff --git a/packages/twenty-sdk/src/front-component-renderer/host/utils/createHtmlHostWrapper.ts b/packages/twenty-sdk/src/front-component-renderer/host/utils/createHtmlHostWrapper.ts index 245a892bde..19436bfd53 100644 --- a/packages/twenty-sdk/src/front-component-renderer/host/utils/createHtmlHostWrapper.ts +++ b/packages/twenty-sdk/src/front-component-renderer/host/utils/createHtmlHostWrapper.ts @@ -45,10 +45,15 @@ const parseCssString = ( const property = declaration.slice(0, colonIndex).trim(); const value = declaration.slice(colonIndex + 1).trim(); - const camelProperty = property.replace(/-([a-z])/g, (_, letter: string) => - letter.toUpperCase(), - ); - style[camelProperty] = value; + const isCssCustomProperty = property.startsWith('--'); + + const key = isCssCustomProperty + ? property + : property.replace(/-([a-z])/g, (_, letter: string) => + letter.toUpperCase(), + ); + + style[key] = value; } return style; @@ -140,13 +145,18 @@ const filterProps = (props: T): T => { type WrapperProps = { children?: React.ReactNode } & Record; +const FORCED_PROPS_BY_TAG: Record> = { + iframe: { sandbox: '' }, +}; + export const createHtmlHostWrapper = (htmlTag: string) => { const isVoid = VOID_ELEMENTS.has(htmlTag); + const forcedProps = FORCED_PROPS_BY_TAG[htmlTag]; return ({ children, ...props }: WrapperProps) => React.createElement( htmlTag, - filterProps(props), + { ...filterProps(props), ...forcedProps }, isVoid ? undefined : children, ); }; diff --git a/packages/twenty-sdk/src/front-component-renderer/index.ts b/packages/twenty-sdk/src/front-component-renderer/index.ts index 744e41b8cc..0988d1fec5 100644 --- a/packages/twenty-sdk/src/front-component-renderer/index.ts +++ b/packages/twenty-sdk/src/front-component-renderer/index.ts @@ -24,7 +24,11 @@ export { HtmlH6, HtmlHeader, HtmlHr, + HtmlIframe, + HtmlAudio, HtmlImg, + HtmlSource, + HtmlVideo, HtmlInput, HtmlLabel, HtmlLi, @@ -69,7 +73,11 @@ export { HtmlH6Element, HtmlHeaderElement, HtmlHrElement, + HtmlIframeElement, + HtmlAudioElement, HtmlImgElement, + HtmlSourceElement, + HtmlVideoElement, HtmlInputElement, HtmlLabelElement, HtmlLiElement, @@ -102,7 +110,11 @@ export type { HtmlCommonEvents, HtmlCommonProperties, HtmlFormProperties, + HtmlIframeProperties, + HtmlAudioProperties, HtmlImgProperties, + HtmlSourceProperties, + HtmlVideoProperties, HtmlInputProperties, HtmlLabelProperties, HtmlOptionProperties, diff --git a/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-components.ts b/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-components.ts index 04b7d2c258..91a3bc4d53 100644 --- a/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-components.ts +++ b/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-components.ts @@ -43,6 +43,10 @@ import { HtmlTdElement, HtmlBrElement, HtmlHrElement, + HtmlIframeElement, + HtmlVideoElement, + HtmlAudioElement, + HtmlSourceElement, RemoteStyleElement, } from './remote-elements'; @@ -1118,6 +1122,110 @@ export const HtmlHr = createRemoteComponent('html-hr', HtmlHrElement, { onDrag: { event: 'drag' }, }, }); +export const HtmlIframe = createRemoteComponent( + 'html-iframe', + HtmlIframeElement, + { + eventProps: { + onClick: { event: 'click' }, + onDoubleClick: { event: 'dblclick' }, + onMouseDown: { event: 'mousedown' }, + onMouseUp: { event: 'mouseup' }, + onMouseOver: { event: 'mouseover' }, + onMouseOut: { event: 'mouseout' }, + onMouseEnter: { event: 'mouseenter' }, + onMouseLeave: { event: 'mouseleave' }, + onKeyDown: { event: 'keydown' }, + onKeyUp: { event: 'keyup' }, + onKeyPress: { event: 'keypress' }, + onFocus: { event: 'focus' }, + onBlur: { event: 'blur' }, + onChange: { event: 'change' }, + onInput: { event: 'input' }, + onSubmit: { event: 'submit' }, + onScroll: { event: 'scroll' }, + onWheel: { event: 'wheel' }, + onContextMenu: { event: 'contextmenu' }, + onDrag: { event: 'drag' }, + }, + }, +); +export const HtmlVideo = createRemoteComponent('html-video', HtmlVideoElement, { + eventProps: { + onClick: { event: 'click' }, + onDoubleClick: { event: 'dblclick' }, + onMouseDown: { event: 'mousedown' }, + onMouseUp: { event: 'mouseup' }, + onMouseOver: { event: 'mouseover' }, + onMouseOut: { event: 'mouseout' }, + onMouseEnter: { event: 'mouseenter' }, + onMouseLeave: { event: 'mouseleave' }, + onKeyDown: { event: 'keydown' }, + onKeyUp: { event: 'keyup' }, + onKeyPress: { event: 'keypress' }, + onFocus: { event: 'focus' }, + onBlur: { event: 'blur' }, + onChange: { event: 'change' }, + onInput: { event: 'input' }, + onSubmit: { event: 'submit' }, + onScroll: { event: 'scroll' }, + onWheel: { event: 'wheel' }, + onContextMenu: { event: 'contextmenu' }, + onDrag: { event: 'drag' }, + }, +}); +export const HtmlAudio = createRemoteComponent('html-audio', HtmlAudioElement, { + eventProps: { + onClick: { event: 'click' }, + onDoubleClick: { event: 'dblclick' }, + onMouseDown: { event: 'mousedown' }, + onMouseUp: { event: 'mouseup' }, + onMouseOver: { event: 'mouseover' }, + onMouseOut: { event: 'mouseout' }, + onMouseEnter: { event: 'mouseenter' }, + onMouseLeave: { event: 'mouseleave' }, + onKeyDown: { event: 'keydown' }, + onKeyUp: { event: 'keyup' }, + onKeyPress: { event: 'keypress' }, + onFocus: { event: 'focus' }, + onBlur: { event: 'blur' }, + onChange: { event: 'change' }, + onInput: { event: 'input' }, + onSubmit: { event: 'submit' }, + onScroll: { event: 'scroll' }, + onWheel: { event: 'wheel' }, + onContextMenu: { event: 'contextmenu' }, + onDrag: { event: 'drag' }, + }, +}); +export const HtmlSource = createRemoteComponent( + 'html-source', + HtmlSourceElement, + { + eventProps: { + onClick: { event: 'click' }, + onDoubleClick: { event: 'dblclick' }, + onMouseDown: { event: 'mousedown' }, + onMouseUp: { event: 'mouseup' }, + onMouseOver: { event: 'mouseover' }, + onMouseOut: { event: 'mouseout' }, + onMouseEnter: { event: 'mouseenter' }, + onMouseLeave: { event: 'mouseleave' }, + onKeyDown: { event: 'keydown' }, + onKeyUp: { event: 'keyup' }, + onKeyPress: { event: 'keypress' }, + onFocus: { event: 'focus' }, + onBlur: { event: 'blur' }, + onChange: { event: 'change' }, + onInput: { event: 'input' }, + onSubmit: { event: 'submit' }, + onScroll: { event: 'scroll' }, + onWheel: { event: 'wheel' }, + onContextMenu: { event: 'contextmenu' }, + onDrag: { event: 'drag' }, + }, + }, +); export const RemoteStyle = createRemoteComponent( 'remote-style', RemoteStyleElement, diff --git a/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-elements.ts b/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-elements.ts index bac1e29f98..3d448b6856 100644 --- a/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-elements.ts +++ b/packages/twenty-sdk/src/front-component-renderer/remote/generated/remote-elements.ts @@ -601,6 +601,120 @@ export const HtmlHrElement = createRemoteElement< events: [...HTML_COMMON_EVENTS_ARRAY], }); +export type HtmlIframeProperties = HtmlCommonProperties & { + src?: string; + width?: string; + height?: string; + allow?: string; + sandbox?: string; + allowFullScreen?: boolean; +}; + +export const HtmlIframeElement = createRemoteElement< + HtmlIframeProperties, + Record, + Record, + HtmlCommonEvents +>({ + properties: { + ...HTML_COMMON_PROPERTIES_CONFIG, + src: { type: String }, + width: { type: String }, + height: { type: String }, + allow: { type: String }, + sandbox: { type: String }, + allowFullScreen: { type: Boolean }, + }, + events: [...HTML_COMMON_EVENTS_ARRAY], +}); + +export type HtmlVideoProperties = HtmlCommonProperties & { + src?: string; + poster?: string; + controls?: boolean; + autoPlay?: boolean; + loop?: boolean; + muted?: boolean; + preload?: string; + width?: string; + height?: string; + crossOrigin?: string; + playsInline?: boolean; +}; + +export const HtmlVideoElement = createRemoteElement< + HtmlVideoProperties, + Record, + Record, + HtmlCommonEvents +>({ + properties: { + ...HTML_COMMON_PROPERTIES_CONFIG, + src: { type: String }, + poster: { type: String }, + controls: { type: Boolean }, + autoPlay: { type: Boolean }, + loop: { type: Boolean }, + muted: { type: Boolean }, + preload: { type: String }, + width: { type: String }, + height: { type: String }, + crossOrigin: { type: String }, + playsInline: { type: Boolean }, + }, + events: [...HTML_COMMON_EVENTS_ARRAY], +}); + +export type HtmlAudioProperties = HtmlCommonProperties & { + src?: string; + controls?: boolean; + autoPlay?: boolean; + loop?: boolean; + muted?: boolean; + preload?: string; + crossOrigin?: string; +}; + +export const HtmlAudioElement = createRemoteElement< + HtmlAudioProperties, + Record, + Record, + HtmlCommonEvents +>({ + properties: { + ...HTML_COMMON_PROPERTIES_CONFIG, + src: { type: String }, + controls: { type: Boolean }, + autoPlay: { type: Boolean }, + loop: { type: Boolean }, + muted: { type: Boolean }, + preload: { type: String }, + crossOrigin: { type: String }, + }, + events: [...HTML_COMMON_EVENTS_ARRAY], +}); + +export type HtmlSourceProperties = HtmlCommonProperties & { + src?: string; + type?: string; + media?: string; +}; + +export const HtmlSourceElement = createRemoteElement< + HtmlSourceProperties, + Record, + Record, + HtmlCommonEvents +>({ + properties: { + ...HTML_COMMON_PROPERTIES_CONFIG, + src: { type: String }, + type: { type: String }, + media: { type: String }, + }, + events: [...HTML_COMMON_EVENTS_ARRAY], +}); + export type RemoteStyleProperties = { cssText?: string; styleKey?: string; @@ -660,6 +774,10 @@ customElements.define('html-th', HtmlThElement); customElements.define('html-td', HtmlTdElement); customElements.define('html-br', HtmlBrElement); customElements.define('html-hr', HtmlHrElement); +customElements.define('html-iframe', HtmlIframeElement); +customElements.define('html-video', HtmlVideoElement); +customElements.define('html-audio', HtmlAudioElement); +customElements.define('html-source', HtmlSourceElement); customElements.define('remote-style', RemoteStyleElement); customElements.define('remote-root', RemoteRootElement); customElements.define('remote-fragment', RemoteFragmentElement); @@ -709,6 +827,10 @@ declare global { 'html-td': InstanceType; 'html-br': InstanceType; 'html-hr': InstanceType; + 'html-iframe': InstanceType; + 'html-video': InstanceType; + 'html-audio': InstanceType; + 'html-source': InstanceType; 'remote-style': InstanceType; 'remote-root': InstanceType; 'remote-fragment': InstanceType; diff --git a/packages/twenty-sdk/src/sdk/front-component-api/constants/AllowedHtmlElements.ts b/packages/twenty-sdk/src/sdk/front-component-api/constants/AllowedHtmlElements.ts index 46cb05e6f0..4c982cbf0b 100644 --- a/packages/twenty-sdk/src/sdk/front-component-api/constants/AllowedHtmlElements.ts +++ b/packages/twenty-sdk/src/sdk/front-component-api/constants/AllowedHtmlElements.ts @@ -145,4 +145,56 @@ export const ALLOWED_HTML_ELEMENTS: AllowedHtmlElement[] = [ }, { tag: 'html-br', name: 'HtmlBr', properties: {} }, { tag: 'html-hr', name: 'HtmlHr', properties: {} }, + { + tag: 'html-iframe', + name: 'HtmlIframe', + properties: { + src: { type: 'string', optional: true }, + title: { type: 'string', optional: true }, + width: { type: 'string', optional: true }, + height: { type: 'string', optional: true }, + allow: { type: 'string', optional: true }, + sandbox: { type: 'string', optional: true }, + allowFullScreen: { type: 'boolean', optional: true }, + }, + }, + { + tag: 'html-video', + name: 'HtmlVideo', + properties: { + src: { type: 'string', optional: true }, + poster: { type: 'string', optional: true }, + controls: { type: 'boolean', optional: true }, + autoPlay: { type: 'boolean', optional: true }, + loop: { type: 'boolean', optional: true }, + muted: { type: 'boolean', optional: true }, + preload: { type: 'string', optional: true }, + width: { type: 'string', optional: true }, + height: { type: 'string', optional: true }, + crossOrigin: { type: 'string', optional: true }, + playsInline: { type: 'boolean', optional: true }, + }, + }, + { + tag: 'html-audio', + name: 'HtmlAudio', + properties: { + src: { type: 'string', optional: true }, + controls: { type: 'boolean', optional: true }, + autoPlay: { type: 'boolean', optional: true }, + loop: { type: 'boolean', optional: true }, + muted: { type: 'boolean', optional: true }, + preload: { type: 'string', optional: true }, + crossOrigin: { type: 'string', optional: true }, + }, + }, + { + tag: 'html-source', + name: 'HtmlSource', + properties: { + src: { type: 'string', optional: true }, + type: { type: 'string', optional: true }, + media: { type: 'string', optional: true }, + }, + }, ];