Explicit header, text and bullet blocks rather than one HTML block in ui spec

This commit is contained in:
Julian Stirling 2026-03-10 15:25:04 +00:00
parent 85ebfef44a
commit c4d0c0e9da
5 changed files with 82 additions and 41 deletions

View file

@ -42,7 +42,8 @@ from openflexure_microscope_server.things.stage import BaseStage
from openflexure_microscope_server.ui import (
UI_ELEMENT_RESPONSE,
Accordion,
HTMLBlock,
HeaderBlock,
TextBlock,
UIElementList,
action_button_for,
property_control_for,
@ -642,7 +643,8 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
return UIElementList(
[
HTMLBlock(html=f"<h4>{self.display_name}</h4><p>{self.ui_blurb}<p>"),
HeaderBlock(text=self.display_name, level=4),
TextBlock(text=self.ui_blurb),
Accordion(title="Background Detect", children=background_ui),
Accordion(
title="Scan Settings",
@ -750,7 +752,8 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
)
return UIElementList(
[
HTMLBlock(html=f"<h4>{self.display_name}</h4><p>{self.ui_blurb}<p>"),
HeaderBlock(text=self.display_name, level=4),
TextBlock(text=self.ui_blurb),
Accordion(
title="Scan Settings",
children=scan_settings,

View file

@ -5,24 +5,12 @@ from html.parser import HTMLParser
from typing import Annotated, Any, Literal, Optional
from urllib.parse import urlparse
from pydantic import BaseModel, BeforeValidator, RootModel
from pydantic import BaseModel, BeforeValidator, Field, RootModel
import labthings_fastapi as lt
ALLOWED_TAGS = {
"p",
"i",
"b",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"ul",
"li",
"a",
}
# Only allowed tags are italic, bold, and link. Also allows self closing br tags.
ALLOWED_TAGS = {"i", "b", "a"}
ALLOWED_SCHEMES = {"http", "https"}
@ -116,8 +104,7 @@ class SafeHTMLParser(HTMLParser):
def sanitise_html(html: str) -> str:
"""Santitise HTML to only have a small list of allowed tags.
Tags allowed without attrs: ``<p>, <i>, <b>, <h1>, <h2>, <h3>, <h4>, <h5>, <h6>,``
``<ul>, <li>``
Tags allowed without attrs: ``<i>, <b>,``
Tags allowed with attrs: ``<a>`` is allowed with ``href`` only. This automatically
appends ``target="_blank" rel="noopener noreferrer"`` so the link opens externally.
@ -137,14 +124,31 @@ def sanitise_html(html: str) -> str:
HtmlFragment = Annotated[str, BeforeValidator(sanitise_html)]
class HTMLBlock(BaseModel):
"""The data required for creating a block of HTML for the UI.
class HeaderBlock(BaseModel):
"""The data required for header."""
The HTML is limited to a small number of tags.
"""
element_type: Literal["header_block"] = "header_block"
text: HtmlFragment
"""The header text (can include basic HTML tags)."""
element_type: Literal["html_block"] = "html_block"
html: HtmlFragment
level: int = Field(default=2, ge=1, le=6)
"""The header level. A number from 1-6."""
class TextBlock(BaseModel):
"""The data required for creating a block text."""
element_type: Literal["text_block"] = "text_block"
text: HtmlFragment
"""The text (can include basic HTML tags)."""
class BulletBlock(BaseModel):
"""The data required for creating a block text."""
element_type: Literal["bullet_block"] = "bullet_block"
bullets: list[HtmlFragment]
"""A list of text elements (can include basic HTML tags)."""
class ActionButton(BaseModel):
@ -304,7 +308,15 @@ class Container(BaseModel):
children: "UIElementList"
UIElementModels = HTMLBlock | PropertyControl | ActionButton | Accordion | Container
UIElementModels = (
HeaderBlock
| TextBlock
| BulletBlock
| PropertyControl
| ActionButton
| Accordion
| Container
)
class UIElementList(RootModel[list[UIElementModels]]):

View file

@ -20,7 +20,7 @@ The script is removed but the contents remain.
### Result
```html
alert(&quot;x&quot;)
<p>Hello</p>
Hello
```
## Test script in bold tag
@ -82,3 +82,14 @@ Remove the javascript from the links.
```html
<a href="https://openflexure.org" target="_blank" rel="noopener noreferrer">OpenFlexure</a>
```
## Test basic formatting preserved
Check basic elements are not changed
### Input
```html
This is <b>bold</b> <i>italic</i> and on a new <br/> line.
```
### Result
```html
This is <b>bold</b> <i>italic</i> and on a new <br/> line.
```

View file

@ -21,8 +21,9 @@ from openflexure_microscope_server.things.scan_workflows import (
from openflexure_microscope_server.ui import (
Accordion,
ActionButton,
HTMLBlock,
HeaderBlock,
PropertyControl,
TextBlock,
UIElementList,
)
@ -385,20 +386,24 @@ def test_histo_workflow_settings_ui(histo_workflow, mocker):
ui = histo_workflow.settings_ui().root
# The UI is in 3 blocks
assert len(ui) == 3
# The top HTML block
assert isinstance(ui[0], HTMLBlock)
assert "<h4>Histo Scan</h4>" in ui[0].html
# The UI is in 4 blocks
assert len(ui) == 4
# The top is a header
assert isinstance(ui[0], HeaderBlock)
assert ui[0].text == "Histo Scan"
# The then some text (note the & is escaped)
assert isinstance(ui[1], TextBlock)
fragment = "This scan workflow is optimised for scanning H&amp;E stained biopsies."
assert fragment in ui[1].text
# Followed by a background detect accordion
assert isinstance(ui[1], Accordion)
assert ui[1].title == "Background Detect"
# And a Scan Settings accordion
assert isinstance(ui[2], Accordion)
assert ui[2].title == "Scan Settings"
assert ui[2].title == "Background Detect"
# And a Scan Settings accordion
assert isinstance(ui[3], Accordion)
assert ui[3].title == "Scan Settings"
# Background UI is ...
background_ui = ui[1].children.root
background_ui = ui[2].children.root
# what the detector specifies as it settings ...
assert background_ui[0] is mock_prop_control
# plus 2 action buttons
@ -409,7 +414,7 @@ def test_histo_workflow_settings_ui(histo_workflow, mocker):
assert background_ui[2].action == "check_background"
# Finally check the contents of the scan settings accordion
scan_settings = ui[2].children.root
scan_settings = ui[3].children.root
assert len(scan_settings) == 8
for element in scan_settings:
assert isinstance(element, PropertyControl)

View file

@ -1,7 +1,17 @@
<template>
<div v-for="(element, index) in elements" :key="index" class="uk-margin">
<!-- eslint-disable vue/no-v-html -->
<div v-if="element.element_type === 'html_block'" v-html="element.html"></div>
<component :is="'h' + element.level" v-if="element.element_type === 'header_block'">
<span v-html="element.text"></span>
</component>
<p v-if="element.element_type === 'text_block'" v-html="element.text"></p>
<ul v-if="element.element_type === 'bullet_block'">
<li
v-for="(bulletText, bulletIndex) in element.bullets"
:key="'bullet-' + bulletIndex"
v-html="bulletText"
></li>
</ul>
<!-- eslint-enable -->
<server-specified-property-control
v-else-if="element.element_type === 'property_control'"