-
Notifications
You must be signed in to change notification settings - Fork 172
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1785 from elementary-data/ele-3960-adaptive-cards
Ele 3960 adaptive cards
- Loading branch information
Showing
27 changed files
with
1,454 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
import uuid | ||
from typing import Any, Dict, List, Optional | ||
|
||
from elementary.messages.blocks import ( | ||
CodeBlock, | ||
DividerBlock, | ||
ExpandableBlock, | ||
FactListBlock, | ||
HeaderBlock, | ||
Icon, | ||
IconBlock, | ||
InlineBlock, | ||
LineBlock, | ||
LinesBlock, | ||
LinkBlock, | ||
TextBlock, | ||
TextStyle, | ||
) | ||
from elementary.messages.message_body import Color, MessageBlock, MessageBody | ||
|
||
ICON_TO_HTML = { | ||
Icon.RED_TRIANGLE: "🔺", | ||
Icon.X: "❌", | ||
Icon.WARNING: "⚠️", | ||
Icon.EXCLAMATION: "❗", | ||
Icon.CHECK: "✅", | ||
Icon.MAGNIFYING_GLASS: "🔎", | ||
Icon.HAMMER_AND_WRENCH: "🛠️", | ||
Icon.POLICE_LIGHT: "🚨", | ||
Icon.INFO: "ℹ️", | ||
Icon.EYE: "👁️", | ||
Icon.GEAR: "⚙️", | ||
Icon.BELL: "🔔", | ||
} | ||
|
||
COLOR_TO_STYLE = { | ||
Color.RED: "Attention", | ||
Color.YELLOW: "Warning", | ||
Color.GREEN: "Good", | ||
} | ||
|
||
|
||
def format_icon(icon: Icon) -> str: | ||
return ICON_TO_HTML[icon] | ||
|
||
|
||
def format_text_block(block: TextBlock) -> str: | ||
if block.style == TextStyle.BOLD: | ||
return f"**{block.text}**" | ||
elif block.style == TextStyle.ITALIC: | ||
return f"_{block.text}_" | ||
else: | ||
return block.text | ||
|
||
|
||
def format_inline_block(block: InlineBlock) -> str: | ||
if isinstance(block, IconBlock): | ||
return format_icon(block.icon) | ||
elif isinstance(block, TextBlock): | ||
return format_text_block(block) | ||
elif isinstance(block, LinkBlock): | ||
return f"[{block.text}]({block.url})" | ||
else: | ||
raise ValueError(f"Unsupported inline block type: {type(block)}") | ||
|
||
|
||
def format_line_block_text(block: LineBlock) -> str: | ||
return block.sep.join([format_inline_block(inline) for inline in block.inlines]) | ||
|
||
|
||
def format_line_block(block: LineBlock) -> Dict[str, Any]: | ||
text = format_line_block_text(block) | ||
|
||
return { | ||
"type": "TextBlock", | ||
"text": text, | ||
"wrap": True, | ||
} | ||
|
||
|
||
def format_lines_block(block: LinesBlock) -> List[Dict[str, Any]]: | ||
return [format_line_block(line_block) for line_block in block.lines] | ||
|
||
|
||
def format_header_block( | ||
block: HeaderBlock, color: Optional[Color] = None | ||
) -> Dict[str, Any]: | ||
return { | ||
"type": "Container", | ||
"items": [ | ||
{ | ||
"type": "TextBlock", | ||
"text": block.text, | ||
"weight": "bolder", | ||
"size": "large", | ||
"wrap": True, | ||
} | ||
], | ||
"style": COLOR_TO_STYLE[color] if color else "Default", | ||
} | ||
|
||
|
||
def format_code_block(block: CodeBlock) -> Dict[str, Any]: | ||
return { | ||
"type": "Container", | ||
"style": "emphasis", | ||
"items": [ | ||
{ | ||
"type": "RichTextBlock", | ||
"inlines": [ | ||
{ | ||
"type": "TextRun", | ||
"text": block.text, | ||
"fontType": "Monospace", | ||
} | ||
], | ||
} | ||
], | ||
} | ||
|
||
|
||
def format_fact_list_block(block: FactListBlock) -> Dict[str, Any]: | ||
return { | ||
"type": "FactSet", | ||
"facts": [ | ||
{ | ||
"title": format_line_block_text(fact.title), | ||
"value": format_line_block_text(fact.value), | ||
} | ||
for fact in block.facts | ||
], | ||
} | ||
|
||
|
||
def format_message_block( | ||
block: MessageBlock, color: Optional[Color] = None | ||
) -> List[Dict[str, Any]]: | ||
if isinstance(block, HeaderBlock): | ||
return [format_header_block(block, color)] | ||
elif isinstance(block, CodeBlock): | ||
return [format_code_block(block)] | ||
elif isinstance(block, LinesBlock): | ||
return format_lines_block(block) | ||
elif isinstance(block, FactListBlock): | ||
return [format_fact_list_block(block)] | ||
elif isinstance(block, ExpandableBlock): | ||
return format_expandable_block(block) | ||
else: | ||
raise ValueError(f"Unsupported message block type: {type(block)}") | ||
|
||
|
||
def split_message_blocks_by_divider( | ||
blocks: List[MessageBlock], | ||
) -> List[List[MessageBlock]]: | ||
first_divider_index = next( | ||
(i for i, block in enumerate(blocks) if isinstance(block, DividerBlock)), | ||
None, | ||
) | ||
if first_divider_index is None: | ||
return [blocks] if blocks else [] | ||
return [ | ||
blocks[:first_divider_index], | ||
*split_message_blocks_by_divider(blocks[first_divider_index + 1 :]), | ||
] | ||
|
||
|
||
def format_divided_message_blocks( | ||
blocks: List[MessageBlock], | ||
divider: bool = False, | ||
color: Optional[Color] = None, | ||
) -> Dict[str, Any]: | ||
return { | ||
"type": "Container", | ||
"separator": divider, | ||
"items": [ | ||
item for block in blocks for item in format_message_block(block, color) | ||
], | ||
} | ||
|
||
|
||
def format_expandable_block(block: ExpandableBlock) -> List[Dict[str, Any]]: | ||
block_title = block.title | ||
expandable_target_id = f"expandable-{uuid.uuid4()}" | ||
return [ | ||
{ | ||
"type": "ActionSet", | ||
"actions": [ | ||
{ | ||
"type": "Action.ToggleVisibility", | ||
"title": block_title, | ||
"targetElements": [expandable_target_id], | ||
} | ||
], | ||
}, | ||
{ | ||
"type": "Container", | ||
"id": expandable_target_id, | ||
"items": format_message_blocks(block.body), | ||
"isVisible": block.expanded, | ||
}, | ||
] | ||
|
||
|
||
def format_message_blocks( | ||
blocks: List[MessageBlock], color: Optional[Color] = None | ||
) -> List[Dict[str, Any]]: | ||
if not blocks: | ||
return [] | ||
|
||
message_blocks = split_message_blocks_by_divider(blocks) | ||
# The divider is not a block in adaptive cards, it's a property of the container. | ||
return [ | ||
format_divided_message_blocks(blocks, divider=True, color=color) | ||
for blocks in message_blocks | ||
] | ||
|
||
|
||
def format_adaptive_card_body(message: MessageBody) -> List[Dict[str, Any]]: | ||
return format_message_blocks(message.blocks, message.color) | ||
|
||
|
||
def format_adaptive_card(message: MessageBody, version: str = "1.6") -> Dict[str, Any]: | ||
if version < "1.2" or version > "1.6": | ||
raise ValueError(f"Version {version} is not supported") | ||
return { | ||
"type": "AdaptiveCard", | ||
"body": format_adaptive_card_body(message), | ||
"version": version, | ||
} |
Empty file.
Empty file.
Empty file.
93 changes: 93 additions & 0 deletions
93
tests/unit/messages/formats/adaptive_cards/fixtures/all_blocks_green.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
{ | ||
"type": "AdaptiveCard", | ||
"body": [ | ||
{ | ||
"type": "Container", | ||
"separator": true, | ||
"items": [ | ||
{ | ||
"type": "Container", | ||
"items": [ | ||
{ | ||
"type": "TextBlock", | ||
"text": "Main Header", | ||
"weight": "bolder", | ||
"size": "large", | ||
"wrap": true | ||
} | ||
], | ||
"style": "Good" | ||
}, | ||
{ | ||
"type": "TextBlock", | ||
"text": "Normal text **Bold text** _Italic text_", | ||
"wrap": true | ||
}, | ||
{ | ||
"type": "TextBlock", | ||
"text": "- First bullet point", | ||
"wrap": true | ||
}, | ||
{ | ||
"type": "TextBlock", | ||
"text": "- Second bullet point", | ||
"wrap": true | ||
}, | ||
{ | ||
"type": "TextBlock", | ||
"text": "\u2705 Check item", | ||
"wrap": true | ||
}, | ||
{ | ||
"type": "FactSet", | ||
"facts": [ | ||
{ | ||
"title": "Status", | ||
"value": "Passed" | ||
}, | ||
{ | ||
"title": "Tags", | ||
"value": "test, example" | ||
} | ||
] | ||
}, | ||
{ | ||
"type": "ActionSet", | ||
"actions": [ | ||
{ | ||
"type": "Action.ToggleVisibility", | ||
"title": "Show Details", | ||
"targetElements": [ | ||
"expandable-00000000-0000-0000-0000-000000000001" | ||
] | ||
} | ||
] | ||
}, | ||
{ | ||
"type": "Container", | ||
"id": "expandable-00000000-0000-0000-0000-000000000001", | ||
"items": [ | ||
{ | ||
"type": "Container", | ||
"separator": true, | ||
"items": [ | ||
{ | ||
"type": "TextBlock", | ||
"text": "\ud83d\udd0e **Details Section**", | ||
"wrap": true | ||
}, | ||
{ | ||
"type": "TextBlock", | ||
"text": "Here's some content with a [link](https://example.com)", | ||
"wrap": true | ||
} | ||
] | ||
} | ||
], | ||
"isVisible": false | ||
} | ||
] | ||
} | ||
], | ||
"version": "1.6" | ||
} |
Oops, something went wrong.