Skip to content
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions assets/docs/blog/2025-10-06-html-flow/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: HTML Flow Component
date: 2025-10-06
author: Mathieu Ledru
cover: images/cover.png
coverSeo: images/cover.png
coverAuthor: Html
coverOriginalUrl: https://www.w3schools.com/html
tags: ["blog", "components", "html"]
---

Introducing HTML Flow: Safe HTML Rendering in Your Automation Flows! 🎨
We're excited to announce the new HTML Flow component for Uniflow. This powerful addition allows you to safely render HTML content stored in variables directly within your automation flows.

The HTML Flow component provides secure HTML rendering with built-in sanitization, making it perfect for displaying dynamic content, embedded media, or custom layouts in your automation interfaces.

### Key Features

- **Safe HTML Rendering**: The component automatically sanitizes HTML content, removing potentially dangerous elements like scripts, forms, and malicious attributes while preserving safe content like iframes with proper restrictions.
- **Variable Integration**: Simply specify a variable name that contains your HTML content, and the component will automatically fetch and display it from the runner context.
- **Real-time Preview**: See your HTML content rendered live as you build your flows, with immediate visual feedback.
- **Iframe Support**: Safely embed external content through iframes with automatic URL validation and attribute filtering.

### How It Works

The HTML Flow component takes a single input - the name of a variable containing HTML content. It then:

1. Fetches the HTML from the runner context
2. Sanitizes the content using custom DOM filtering
3. Renders the safe HTML directly in the preview pane
4. Updates automatically when the variable content changes

Perfect for creating rich, interactive automation interfaces that can display dynamic content, embedded media, or custom layouts while maintaining security and performance.

Let's make your flows not just functional, but visually engaging! 🚀
4 changes: 4 additions & 0 deletions assets/docs/changelog.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
- tag: "v1.1.18"
label: |-
Add HTML Flow Component
date: "2025-10-06"
- tag: "v1.1.17"
label: |-
Migrate to Symfony UX
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, { useImperativeHandle } from 'react'
import FlowHeader from '../flow/header.jsx'
import FlowHeader from './header.jsx'
import FormInput, { FormInputType } from '../form-input.jsx'
import { flow } from '../flow/flow.jsx'
import { flow } from './flow.jsx'
import LZString from 'lz-string'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDownload, faTimes } from '@fortawesome/free-solid-svg-icons'
import { ClientType } from '../../models/client-type';

const AssetsFlow = flow((props, ref) => {
const { onPop, onUpdate, onPlay, onStop, isPlaying, data, clients } = props
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { useImperativeHandle } from 'react'
import FlowHeader from '../flow/header.jsx'
import FlowHeader from './header.jsx'
import FormInput, { FormInputType } from '../form-input.jsx'
import { flow } from '../flow/flow.jsx'
import { flow } from './flow.jsx'
import { useRef } from 'react'
import { ClientType } from '../../models/client-type';

// Canvas flow data shape:
// {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useImperativeHandle } from 'react'
import FlowHeader from '../flow/header.jsx'
import FlowHeader from './header.jsx'
import FormInput, { FormInputType } from '../form-input.jsx'
import { flow } from '../flow/flow.jsx'
import { ClientType } from '../../models/client-type';
import { flow } from './flow.jsx'

/**
* @typedef {Object} FunctionFlowData
Expand Down
120 changes: 120 additions & 0 deletions assets/shop/components/flow/html-flow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useImperativeHandle, useMemo } from 'react'
import FlowHeader from './header.jsx'
import FormInput, { FormInputType } from '../form-input.jsx'
import { flow } from './flow.jsx'

/**
* HtmlFlow — safely render HTML stored in a variable
*
* Single input: `variable` (name of runner context variable holding HTML).
* The variable's value is sanitized using custom DOM filtering and rendered directly
* in the preview (not inside an iframe).
*/

const HtmlFlow = flow((props, ref) => {
const { onPop, onUpdate, onPlay, onStop, isPlaying, data, clients } = props

useImperativeHandle(ref, () => ({
onSerialize: () => {
return JSON.stringify([data?.variable, data?.html])
},
onDeserialize: (data) => {
const [variable, html] = data ? JSON.parse(data) : [undefined, undefined]
return { variable, html }
},
onCompile: () => {
if (!data || !data.variable) {
return ''
}

let html = data.html || ''
html = JSON.stringify(html)

return data.variable + ' = ' + html
},
onExecute: async (runner) => {
if (data && data.variable) {
let context = runner.getContext()
if (context[data.variable]) {
onUpdate({
...data,
html: context[data.variable]
})
} else {
return runner.run()
}
}
}
}), [data])

const onChangeVariable = (variable) => onUpdate({ ...data, variable })

const sanitizedHtml = (() => {
const dirty = String(data?.html || '')
if (!dirty) return ''

// Custom sanitization for iframes - more permissive than DOMPurify
const tempDiv = document.createElement('div')
tempDiv.innerHTML = dirty

// Remove potentially dangerous elements
const dangerousElements = tempDiv.querySelectorAll('script, object, embed, form, input, button')
dangerousElements.forEach(el => el.remove())

// Clean up iframe attributes - only keep safe ones
const iframes = tempDiv.querySelectorAll('iframe')
iframes.forEach(iframe => {
// Remove potentially dangerous attributes
const allowedAttrs = ['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'allow', 'sandbox']
const attrsToRemove = []

for (let attr of iframe.attributes) {
if (!allowedAttrs.includes(attr.name)) {
attrsToRemove.push(attr.name)
}
}

attrsToRemove.forEach(attr => iframe.removeAttribute(attr))

// Ensure src starts with https: or is a relative URL
const src = iframe.getAttribute('src')
if (src && !src.startsWith('https:') && !src.startsWith('/') && !src.startsWith('./')) {
iframe.removeAttribute('src')
}
})

return tempDiv.innerHTML
})()

return (
<>
<FlowHeader
title="HTML"
clients={clients}
isPlaying={isPlaying}
onPlay={onPlay}
onStop={onStop}
onPop={onPop}
/>

<form className="form-sm-horizontal">
<FormInput
id="variable"
type={FormInputType.TEXT}
label="Variable"
value={data?.variable}
onChange={onChangeVariable}
help="Context variable containing HTML to display (sanitized)."
/>

<div className="row mb-3">
<label className="col-sm-2 col-form-label">Preview</label>
<div className="col-sm-10" dangerouslySetInnerHTML={{ __html: sanitizedHtml }}>
</div>
</div>
</form>
</>
)
})

export default HtmlFlow
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, { useImperativeHandle } from 'react'
import FlowHeader from '../flow/header.jsx'
import FlowHeader from './header.jsx'
import FormInput, { FormInputType } from '../form-input.jsx'
import { flow } from '../flow/flow.jsx'
import { flow } from './flow.jsx'
import PropertyAccessor from 'property-accessor'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { ClientType } from '../../models/client-type';

const ObjectFlow = flow((props, ref) => {
const { onPop, onUpdate, onPlay, onStop, isPlaying, data, clients } = props
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useImperativeHandle, useRef, useState } from 'react'
import FlowHeader from '../flow/header.jsx'
import FlowHeader from './header.jsx'
import FormInput, { FormInputType } from '../form-input.jsx'
import { flow } from '../flow/flow.jsx'
import { flow } from './flow.jsx'
import { useStateRef } from '../../hooks/use-state-ref'
import { ClientType } from '../../models/client-type';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useImperativeHandle } from 'react'
import FlowHeader from '../flow/header.jsx'
import FlowHeader from './header.jsx'
import FormInput, { FormInputType } from '../form-input.jsx'
import { flow } from '../flow/flow.jsx'
import { ClientType } from '../../models/client-type';
import { flow } from './flow.jsx'

const TextFlow = flow((props, ref) => {
const { onPop, onUpdate, onPlay, onStop, isPlaying, data, clients } = props
Expand Down
18 changes: 12 additions & 6 deletions assets/shop/models/flows.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import FunctionFlow from './../components/flow-function/index.jsx'
import PromptFlow from './../components/flow-prompt/index.jsx'
import AssetsFlow from './../components/flow-assets/index.jsx'
import TextFlow from './../components/flow-text/index.jsx'
import CanvasFlow from './../components/flow-canvas/index.jsx'
import ObjectFlow from './../components/flow-object/index.jsx'
import FunctionFlow from './../components/flow/function-flow.jsx'
import PromptFlow from './../components/flow/prompt-flow.jsx'
import AssetsFlow from './../components/flow/assets-flow.jsx'
import TextFlow from './../components/flow/text-flow.jsx'
import CanvasFlow from './../components/flow/canvas-flow.jsx'
import ObjectFlow from './../components/flow/object-flow.jsx'
import HtmlFlow from './../components/flow/html-flow.jsx'
import { ClientType } from './client-type';


Expand All @@ -16,6 +17,7 @@ export const flows = {
'@uniflow-io/uniflow-flow-assets': AssetsFlow,
'@uniflow-io/uniflow-flow-canvas': CanvasFlow,
'@uniflow-io/uniflow-flow-object': ObjectFlow,
'@uniflow-io/uniflow-flow-html': HtmlFlow,
}

export const flowsNames = {
Expand All @@ -25,6 +27,7 @@ export const flowsNames = {
'@uniflow-io/uniflow-flow-assets': 'Assets Flow',
'@uniflow-io/uniflow-flow-canvas': 'Canvas Flow',
'@uniflow-io/uniflow-flow-object': 'Object Flow',
'@uniflow-io/uniflow-flow-html': 'HTML Flow',
}

export const flowsClients = {
Expand All @@ -49,5 +52,8 @@ export const flowsClients = {
'@uniflow-io/uniflow-flow-object': [
ClientType.UNIFLOW,
],
'@uniflow-io/uniflow-flow-html': [
ClientType.UNIFLOW,
],
}

2 changes: 1 addition & 1 deletion assets/shop/models/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export default class Runner {
// Create a shared context that persists across iterations
const sharedContext = {
console: consoleBridge,
axios: fetchBridge,
fetch: fetchBridge,
};

// Get the keys from shared context for dynamic exclusion
Expand Down
4 changes: 2 additions & 2 deletions assets/shop/program.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ class Program extends React.Component {
<div className="col">
<h3>Infos</h3>
</div>
<div className="d-block col-auto">
{/*<div className="d-block col-auto">
<div className="btn-toolbar" role="toolbar" aria-label="flow actions">
<div className="btn-group-sm" role="group">
<a className="btn text-secondary" href={`/program/duplicate/${this.uid}`}>
Expand All @@ -514,7 +514,7 @@ class Program extends React.Component {
</a>
</div>
</div>
</div>
</div>*/}
</div>
<form className="form-sm-horizontal">
<div className="row mb-3">
Expand Down
Loading
Loading