Skip to content

Commit 9c055a1

Browse files
authored
Merge pull request #1785 from elementary-data/ele-3960-adaptive-cards
Ele 3960 adaptive cards
2 parents 066e712 + cc614cf commit 9c055a1

27 files changed

+1454
-0
lines changed

elementary/messages/formats/__init__.py

Whitespace-only changes.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import uuid
2+
from typing import Any, Dict, List, Optional
3+
4+
from elementary.messages.blocks import (
5+
CodeBlock,
6+
DividerBlock,
7+
ExpandableBlock,
8+
FactListBlock,
9+
HeaderBlock,
10+
Icon,
11+
IconBlock,
12+
InlineBlock,
13+
LineBlock,
14+
LinesBlock,
15+
LinkBlock,
16+
TextBlock,
17+
TextStyle,
18+
)
19+
from elementary.messages.message_body import Color, MessageBlock, MessageBody
20+
21+
ICON_TO_HTML = {
22+
Icon.RED_TRIANGLE: "🔺",
23+
Icon.X: "❌",
24+
Icon.WARNING: "⚠️",
25+
Icon.EXCLAMATION: "❗",
26+
Icon.CHECK: "✅",
27+
Icon.MAGNIFYING_GLASS: "🔎",
28+
Icon.HAMMER_AND_WRENCH: "🛠️",
29+
Icon.POLICE_LIGHT: "🚨",
30+
Icon.INFO: "ℹ️",
31+
Icon.EYE: "👁️",
32+
Icon.GEAR: "⚙️",
33+
Icon.BELL: "🔔",
34+
}
35+
36+
COLOR_TO_STYLE = {
37+
Color.RED: "Attention",
38+
Color.YELLOW: "Warning",
39+
Color.GREEN: "Good",
40+
}
41+
42+
43+
def format_icon(icon: Icon) -> str:
44+
return ICON_TO_HTML[icon]
45+
46+
47+
def format_text_block(block: TextBlock) -> str:
48+
if block.style == TextStyle.BOLD:
49+
return f"**{block.text}**"
50+
elif block.style == TextStyle.ITALIC:
51+
return f"_{block.text}_"
52+
else:
53+
return block.text
54+
55+
56+
def format_inline_block(block: InlineBlock) -> str:
57+
if isinstance(block, IconBlock):
58+
return format_icon(block.icon)
59+
elif isinstance(block, TextBlock):
60+
return format_text_block(block)
61+
elif isinstance(block, LinkBlock):
62+
return f"[{block.text}]({block.url})"
63+
else:
64+
raise ValueError(f"Unsupported inline block type: {type(block)}")
65+
66+
67+
def format_line_block_text(block: LineBlock) -> str:
68+
return block.sep.join([format_inline_block(inline) for inline in block.inlines])
69+
70+
71+
def format_line_block(block: LineBlock) -> Dict[str, Any]:
72+
text = format_line_block_text(block)
73+
74+
return {
75+
"type": "TextBlock",
76+
"text": text,
77+
"wrap": True,
78+
}
79+
80+
81+
def format_lines_block(block: LinesBlock) -> List[Dict[str, Any]]:
82+
return [format_line_block(line_block) for line_block in block.lines]
83+
84+
85+
def format_header_block(
86+
block: HeaderBlock, color: Optional[Color] = None
87+
) -> Dict[str, Any]:
88+
return {
89+
"type": "Container",
90+
"items": [
91+
{
92+
"type": "TextBlock",
93+
"text": block.text,
94+
"weight": "bolder",
95+
"size": "large",
96+
"wrap": True,
97+
}
98+
],
99+
"style": COLOR_TO_STYLE[color] if color else "Default",
100+
}
101+
102+
103+
def format_code_block(block: CodeBlock) -> Dict[str, Any]:
104+
return {
105+
"type": "Container",
106+
"style": "emphasis",
107+
"items": [
108+
{
109+
"type": "RichTextBlock",
110+
"inlines": [
111+
{
112+
"type": "TextRun",
113+
"text": block.text,
114+
"fontType": "Monospace",
115+
}
116+
],
117+
}
118+
],
119+
}
120+
121+
122+
def format_fact_list_block(block: FactListBlock) -> Dict[str, Any]:
123+
return {
124+
"type": "FactSet",
125+
"facts": [
126+
{
127+
"title": format_line_block_text(fact.title),
128+
"value": format_line_block_text(fact.value),
129+
}
130+
for fact in block.facts
131+
],
132+
}
133+
134+
135+
def format_message_block(
136+
block: MessageBlock, color: Optional[Color] = None
137+
) -> List[Dict[str, Any]]:
138+
if isinstance(block, HeaderBlock):
139+
return [format_header_block(block, color)]
140+
elif isinstance(block, CodeBlock):
141+
return [format_code_block(block)]
142+
elif isinstance(block, LinesBlock):
143+
return format_lines_block(block)
144+
elif isinstance(block, FactListBlock):
145+
return [format_fact_list_block(block)]
146+
elif isinstance(block, ExpandableBlock):
147+
return format_expandable_block(block)
148+
else:
149+
raise ValueError(f"Unsupported message block type: {type(block)}")
150+
151+
152+
def split_message_blocks_by_divider(
153+
blocks: List[MessageBlock],
154+
) -> List[List[MessageBlock]]:
155+
first_divider_index = next(
156+
(i for i, block in enumerate(blocks) if isinstance(block, DividerBlock)),
157+
None,
158+
)
159+
if first_divider_index is None:
160+
return [blocks] if blocks else []
161+
return [
162+
blocks[:first_divider_index],
163+
*split_message_blocks_by_divider(blocks[first_divider_index + 1 :]),
164+
]
165+
166+
167+
def format_divided_message_blocks(
168+
blocks: List[MessageBlock],
169+
divider: bool = False,
170+
color: Optional[Color] = None,
171+
) -> Dict[str, Any]:
172+
return {
173+
"type": "Container",
174+
"separator": divider,
175+
"items": [
176+
item for block in blocks for item in format_message_block(block, color)
177+
],
178+
}
179+
180+
181+
def format_expandable_block(block: ExpandableBlock) -> List[Dict[str, Any]]:
182+
block_title = block.title
183+
expandable_target_id = f"expandable-{uuid.uuid4()}"
184+
return [
185+
{
186+
"type": "ActionSet",
187+
"actions": [
188+
{
189+
"type": "Action.ToggleVisibility",
190+
"title": block_title,
191+
"targetElements": [expandable_target_id],
192+
}
193+
],
194+
},
195+
{
196+
"type": "Container",
197+
"id": expandable_target_id,
198+
"items": format_message_blocks(block.body),
199+
"isVisible": block.expanded,
200+
},
201+
]
202+
203+
204+
def format_message_blocks(
205+
blocks: List[MessageBlock], color: Optional[Color] = None
206+
) -> List[Dict[str, Any]]:
207+
if not blocks:
208+
return []
209+
210+
message_blocks = split_message_blocks_by_divider(blocks)
211+
# The divider is not a block in adaptive cards, it's a property of the container.
212+
return [
213+
format_divided_message_blocks(blocks, divider=True, color=color)
214+
for blocks in message_blocks
215+
]
216+
217+
218+
def format_adaptive_card_body(message: MessageBody) -> List[Dict[str, Any]]:
219+
return format_message_blocks(message.blocks, message.color)
220+
221+
222+
def format_adaptive_card(message: MessageBody, version: str = "1.6") -> Dict[str, Any]:
223+
if version < "1.2" or version > "1.6":
224+
raise ValueError(f"Version {version} is not supported")
225+
return {
226+
"type": "AdaptiveCard",
227+
"body": format_adaptive_card_body(message),
228+
"version": version,
229+
}

tests/unit/messages/__init__.py

Whitespace-only changes.

tests/unit/messages/formats/__init__.py

Whitespace-only changes.

tests/unit/messages/formats/adaptive_cards/__init__.py

Whitespace-only changes.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"type": "AdaptiveCard",
3+
"body": [
4+
{
5+
"type": "Container",
6+
"separator": true,
7+
"items": [
8+
{
9+
"type": "Container",
10+
"items": [
11+
{
12+
"type": "TextBlock",
13+
"text": "Main Header",
14+
"weight": "bolder",
15+
"size": "large",
16+
"wrap": true
17+
}
18+
],
19+
"style": "Good"
20+
},
21+
{
22+
"type": "TextBlock",
23+
"text": "Normal text **Bold text** _Italic text_",
24+
"wrap": true
25+
},
26+
{
27+
"type": "TextBlock",
28+
"text": "- First bullet point",
29+
"wrap": true
30+
},
31+
{
32+
"type": "TextBlock",
33+
"text": "- Second bullet point",
34+
"wrap": true
35+
},
36+
{
37+
"type": "TextBlock",
38+
"text": "\u2705 Check item",
39+
"wrap": true
40+
},
41+
{
42+
"type": "FactSet",
43+
"facts": [
44+
{
45+
"title": "Status",
46+
"value": "Passed"
47+
},
48+
{
49+
"title": "Tags",
50+
"value": "test, example"
51+
}
52+
]
53+
},
54+
{
55+
"type": "ActionSet",
56+
"actions": [
57+
{
58+
"type": "Action.ToggleVisibility",
59+
"title": "Show Details",
60+
"targetElements": [
61+
"expandable-00000000-0000-0000-0000-000000000001"
62+
]
63+
}
64+
]
65+
},
66+
{
67+
"type": "Container",
68+
"id": "expandable-00000000-0000-0000-0000-000000000001",
69+
"items": [
70+
{
71+
"type": "Container",
72+
"separator": true,
73+
"items": [
74+
{
75+
"type": "TextBlock",
76+
"text": "\ud83d\udd0e **Details Section**",
77+
"wrap": true
78+
},
79+
{
80+
"type": "TextBlock",
81+
"text": "Here's some content with a [link](https://example.com)",
82+
"wrap": true
83+
}
84+
]
85+
}
86+
],
87+
"isVisible": false
88+
}
89+
]
90+
}
91+
],
92+
"version": "1.6"
93+
}

0 commit comments

Comments
 (0)