Explicit header, text and bullet blocks rather than one HTML block in ui spec
This commit is contained in:
parent
85ebfef44a
commit
c4d0c0e9da
5 changed files with 82 additions and 41 deletions
|
|
@ -42,7 +42,8 @@ from openflexure_microscope_server.things.stage import BaseStage
|
||||||
from openflexure_microscope_server.ui import (
|
from openflexure_microscope_server.ui import (
|
||||||
UI_ELEMENT_RESPONSE,
|
UI_ELEMENT_RESPONSE,
|
||||||
Accordion,
|
Accordion,
|
||||||
HTMLBlock,
|
HeaderBlock,
|
||||||
|
TextBlock,
|
||||||
UIElementList,
|
UIElementList,
|
||||||
action_button_for,
|
action_button_for,
|
||||||
property_control_for,
|
property_control_for,
|
||||||
|
|
@ -642,7 +643,8 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
||||||
|
|
||||||
return UIElementList(
|
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="Background Detect", children=background_ui),
|
||||||
Accordion(
|
Accordion(
|
||||||
title="Scan Settings",
|
title="Scan Settings",
|
||||||
|
|
@ -750,7 +752,8 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
|
||||||
)
|
)
|
||||||
return UIElementList(
|
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(
|
Accordion(
|
||||||
title="Scan Settings",
|
title="Scan Settings",
|
||||||
children=scan_settings,
|
children=scan_settings,
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,12 @@ from html.parser import HTMLParser
|
||||||
from typing import Annotated, Any, Literal, Optional
|
from typing import Annotated, Any, Literal, Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pydantic import BaseModel, BeforeValidator, RootModel
|
from pydantic import BaseModel, BeforeValidator, Field, RootModel
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
import labthings_fastapi as lt
|
||||||
|
|
||||||
ALLOWED_TAGS = {
|
# Only allowed tags are italic, bold, and link. Also allows self closing br tags.
|
||||||
"p",
|
ALLOWED_TAGS = {"i", "b", "a"}
|
||||||
"i",
|
|
||||||
"b",
|
|
||||||
"h1",
|
|
||||||
"h2",
|
|
||||||
"h3",
|
|
||||||
"h4",
|
|
||||||
"h5",
|
|
||||||
"h6",
|
|
||||||
"ul",
|
|
||||||
"li",
|
|
||||||
"a",
|
|
||||||
}
|
|
||||||
|
|
||||||
ALLOWED_SCHEMES = {"http", "https"}
|
ALLOWED_SCHEMES = {"http", "https"}
|
||||||
|
|
||||||
|
|
@ -116,8 +104,7 @@ class SafeHTMLParser(HTMLParser):
|
||||||
def sanitise_html(html: str) -> str:
|
def sanitise_html(html: str) -> str:
|
||||||
"""Santitise HTML to only have a small list of allowed tags.
|
"""Santitise HTML to only have a small list of allowed tags.
|
||||||
|
|
||||||
Tags allowed without attrs: ``<p>, <i>, <b>, <h1>, <h2>, <h3>, <h4>, <h5>, <h6>,``
|
Tags allowed without attrs: ``<i>, <b>,``
|
||||||
``<ul>, <li>``
|
|
||||||
|
|
||||||
Tags allowed with attrs: ``<a>`` is allowed with ``href`` only. This automatically
|
Tags allowed with attrs: ``<a>`` is allowed with ``href`` only. This automatically
|
||||||
appends ``target="_blank" rel="noopener noreferrer"`` so the link opens externally.
|
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)]
|
HtmlFragment = Annotated[str, BeforeValidator(sanitise_html)]
|
||||||
|
|
||||||
|
|
||||||
class HTMLBlock(BaseModel):
|
class HeaderBlock(BaseModel):
|
||||||
"""The data required for creating a block of HTML for the UI.
|
"""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"
|
level: int = Field(default=2, ge=1, le=6)
|
||||||
html: HtmlFragment
|
"""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):
|
class ActionButton(BaseModel):
|
||||||
|
|
@ -304,7 +308,15 @@ class Container(BaseModel):
|
||||||
children: "UIElementList"
|
children: "UIElementList"
|
||||||
|
|
||||||
|
|
||||||
UIElementModels = HTMLBlock | PropertyControl | ActionButton | Accordion | Container
|
UIElementModels = (
|
||||||
|
HeaderBlock
|
||||||
|
| TextBlock
|
||||||
|
| BulletBlock
|
||||||
|
| PropertyControl
|
||||||
|
| ActionButton
|
||||||
|
| Accordion
|
||||||
|
| Container
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UIElementList(RootModel[list[UIElementModels]]):
|
class UIElementList(RootModel[list[UIElementModels]]):
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ The script is removed but the contents remain.
|
||||||
### Result
|
### Result
|
||||||
```html
|
```html
|
||||||
alert("x")
|
alert("x")
|
||||||
<p>Hello</p>
|
Hello
|
||||||
```
|
```
|
||||||
## Test script in bold tag
|
## Test script in bold tag
|
||||||
|
|
||||||
|
|
@ -81,4 +81,15 @@ Remove the javascript from the links.
|
||||||
### Result
|
### Result
|
||||||
```html
|
```html
|
||||||
<a href="https://openflexure.org" target="_blank" rel="noopener noreferrer">OpenFlexure</a>
|
<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.
|
||||||
```
|
```
|
||||||
|
|
@ -21,8 +21,9 @@ from openflexure_microscope_server.things.scan_workflows import (
|
||||||
from openflexure_microscope_server.ui import (
|
from openflexure_microscope_server.ui import (
|
||||||
Accordion,
|
Accordion,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
HTMLBlock,
|
HeaderBlock,
|
||||||
PropertyControl,
|
PropertyControl,
|
||||||
|
TextBlock,
|
||||||
UIElementList,
|
UIElementList,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -385,20 +386,24 @@ def test_histo_workflow_settings_ui(histo_workflow, mocker):
|
||||||
|
|
||||||
ui = histo_workflow.settings_ui().root
|
ui = histo_workflow.settings_ui().root
|
||||||
|
|
||||||
# The UI is in 3 blocks
|
# The UI is in 4 blocks
|
||||||
assert len(ui) == 3
|
assert len(ui) == 4
|
||||||
# The top HTML block
|
# The top is a header
|
||||||
assert isinstance(ui[0], HTMLBlock)
|
assert isinstance(ui[0], HeaderBlock)
|
||||||
assert "<h4>Histo Scan</h4>" in ui[0].html
|
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&E stained biopsies."
|
||||||
|
assert fragment in ui[1].text
|
||||||
# Followed by a background detect accordion
|
# 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 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 is ...
|
||||||
background_ui = ui[1].children.root
|
background_ui = ui[2].children.root
|
||||||
# what the detector specifies as it settings ...
|
# what the detector specifies as it settings ...
|
||||||
assert background_ui[0] is mock_prop_control
|
assert background_ui[0] is mock_prop_control
|
||||||
# plus 2 action buttons
|
# plus 2 action buttons
|
||||||
|
|
@ -409,7 +414,7 @@ def test_histo_workflow_settings_ui(histo_workflow, mocker):
|
||||||
assert background_ui[2].action == "check_background"
|
assert background_ui[2].action == "check_background"
|
||||||
|
|
||||||
# Finally check the contents of the scan settings accordion
|
# 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
|
assert len(scan_settings) == 8
|
||||||
for element in scan_settings:
|
for element in scan_settings:
|
||||||
assert isinstance(element, PropertyControl)
|
assert isinstance(element, PropertyControl)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(element, index) in elements" :key="index" class="uk-margin">
|
<div v-for="(element, index) in elements" :key="index" class="uk-margin">
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- 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 -->
|
<!-- eslint-enable -->
|
||||||
<server-specified-property-control
|
<server-specified-property-control
|
||||||
v-else-if="element.element_type === 'property_control'"
|
v-else-if="element.element_type === 'property_control'"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue