Skip to content

Commit 980c6c5

Browse files
authored
Merge pull request #122 from Jorricks/mr/master/jorrick/add-table-component
WIP - ✨ Add table component
2 parents 8d29520 + d9a2aee commit 980c6c5

File tree

3 files changed

+235
-5
lines changed

3 files changed

+235
-5
lines changed

blockkit/core.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def validate(self, field_name: str, value: Any) -> None:
139139
)
140140

141141

142+
# TODO: add support for checking generic types like Typed(list[RawText | RichText])
142143
class Typed(FieldValidator):
143144
def __init__(self, *types: type):
144145
if not types:
@@ -449,10 +450,19 @@ def build(self):
449450
if hasattr(field.value, "build"):
450451
obj[field.name] = field.value.build()
451452
if isinstance(field.value, list | tuple | set):
452-
obj[field.name] = [
453-
item.build() if hasattr(item, "build") else item
454-
for item in field.value
455-
]
453+
items = []
454+
for item in field.value:
455+
if isinstance(item, list | tuple | set):
456+
nested_items = [
457+
nested.build() if hasattr(nested, "build") else nested
458+
for nested in item
459+
]
460+
items.append(nested_items)
461+
else:
462+
items.append(item.build() if hasattr(item, "build") else item)
463+
464+
obj[field.name] = items
465+
456466
return obj
457467

458468

@@ -796,6 +806,21 @@ def type(self, type: Literal["plain_text", "mrkdwn"]) -> Self:
796806
)
797807

798808

809+
class RawText(Component):
810+
"""
811+
Raw text object
812+
"""
813+
814+
def __init__(self, text: str = None):
815+
super().__init__("raw_text")
816+
self.text(text)
817+
818+
def text(self, text: str | None) -> Self:
819+
return self._add_field(
820+
"text", text, validators=[Typed(str), Required(), Length(1, 10000)]
821+
)
822+
823+
799824
class Confirm(Component, StyleMixin):
800825
"""
801826
Confirmation dialog
@@ -2986,6 +3011,80 @@ def expand(self, expand: bool | None = True) -> Self:
29863011
return self._add_field("expand", expand, validators=[Typed(bool)])
29873012

29883013

3014+
class ColumnSettings(Component):
3015+
"""
3016+
Column settings
3017+
3018+
Lets you change text alignment and text wrapping behavior for table columns
3019+
3020+
Slack docs:
3021+
https://docs.slack.dev/reference/block-kit/blocks/table-block/
3022+
"""
3023+
3024+
LEFT: Final[Literal["left"]] = "left"
3025+
CENTER: Final[Literal["center"]] = "center"
3026+
RIGHT: Final[Literal["right"]] = "right"
3027+
3028+
def __init__(
3029+
self,
3030+
align: Literal["left", "center", "right"] | None = None,
3031+
is_wrapped: bool | None = None,
3032+
):
3033+
super().__init__()
3034+
self.align(align)
3035+
self.is_wrapped(is_wrapped)
3036+
3037+
def align(self, align: Literal["left", "center", "right"] | None) -> Self:
3038+
return self._add_field(
3039+
"align",
3040+
align,
3041+
validators=[Typed(str), Strings(self.LEFT, self.CENTER, self.RIGHT)],
3042+
)
3043+
3044+
def is_wrapped(self, is_wrapped: bool = True) -> Self:
3045+
return self._add_field("is_wrapped", is_wrapped, validators=[Typed(bool)])
3046+
3047+
3048+
class Table(Component, BlockIdMixin):
3049+
"""
3050+
Table block
3051+
3052+
Displays structured information in a table.
3053+
3054+
Slack docs:
3055+
https://docs.slack.dev/reference/block-kit/blocks/table-block
3056+
"""
3057+
3058+
def __init__(
3059+
self,
3060+
rows: list[list[RawText | RichText]] | None = None,
3061+
column_settings: list[ColumnSettings] | None = None,
3062+
block_id: str | None = None,
3063+
):
3064+
super().__init__("table")
3065+
self.rows(*rows or ())
3066+
self.column_settings(*column_settings or ())
3067+
self.block_id(block_id)
3068+
3069+
def rows(self, *rows: list[RawText | RichText]) -> Self:
3070+
return self._add_field(
3071+
"rows",
3072+
list(rows),
3073+
validators=[Typed(list), Required(), Length(1, 100)],
3074+
)
3075+
3076+
def add_row(self, *row: list[RawText | RichText]) -> Self:
3077+
return self._add_field_value("rows", list(row))
3078+
3079+
def column_settings(self, *column_settings: ColumnSettings) -> Self:
3080+
return self._add_field(
3081+
"column_settings", list(column_settings), validators=[Typed(ColumnSettings)]
3082+
)
3083+
3084+
def add_column_setting(self, column_setting: ColumnSettings) -> Self:
3085+
return self._add_field_value("column_settings", column_setting)
3086+
3087+
29893088
class Video(Component, BlockIdMixin):
29903089
"""
29913090
Video block
@@ -3088,6 +3187,7 @@ def author_name(self, author_name: str | None) -> Self:
30883187
| Markdown
30893188
| RichText
30903189
| Section
3190+
| Table
30913191
| Video
30923192
)
30933193

tests/test_core.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from blockkit.core import ColumnSettings
2+
from blockkit.core import RawText
13
from datetime import date, datetime, time
24
from zoneinfo import ZoneInfo
35

@@ -27,6 +29,7 @@
2729
Image,
2830
ImageEl,
2931
Input,
32+
Table,
3033
InputParameter,
3134
Markdown,
3235
Message,
@@ -3290,6 +3293,133 @@ def test_builds_fields(self):
32903293
assert got == want
32913294

32923295

3296+
class TestTable:
3297+
def test_builds(self):
3298+
want = {
3299+
"type": "table",
3300+
"column_settings": [
3301+
{"is_wrapped": True},
3302+
{"align": "right"},
3303+
],
3304+
"rows": [
3305+
[
3306+
{"type": "raw_text", "text": "Header A"},
3307+
{"type": "raw_text", "text": "Header B"},
3308+
],
3309+
[
3310+
{"type": "raw_text", "text": "Data 1A"},
3311+
{
3312+
"type": "rich_text",
3313+
"elements": [
3314+
{
3315+
"type": "rich_text_section",
3316+
"elements": [
3317+
{
3318+
"text": "Data 1B",
3319+
"type": "link",
3320+
"url": "https://slack.com",
3321+
}
3322+
],
3323+
}
3324+
],
3325+
},
3326+
],
3327+
[
3328+
{"type": "raw_text", "text": "Data 2A"},
3329+
{
3330+
"type": "rich_text",
3331+
"elements": [
3332+
{
3333+
"type": "rich_text_section",
3334+
"elements": [
3335+
{
3336+
"text": "Data 2B",
3337+
"type": "link",
3338+
"url": "https://slack.com",
3339+
}
3340+
],
3341+
}
3342+
],
3343+
},
3344+
],
3345+
],
3346+
}
3347+
3348+
got = Table(
3349+
column_settings=[
3350+
ColumnSettings(is_wrapped=True),
3351+
ColumnSettings(align=ColumnSettings.RIGHT),
3352+
],
3353+
rows=[
3354+
[
3355+
RawText(text="Header A"),
3356+
RawText(text="Header B"),
3357+
],
3358+
[
3359+
RawText(text="Data 1A"),
3360+
RichText(
3361+
elements=[
3362+
RichTextSection(
3363+
elements=[
3364+
RichLinkEl(
3365+
text="Data 1B",
3366+
url="https://slack.com",
3367+
)
3368+
]
3369+
)
3370+
]
3371+
),
3372+
],
3373+
[
3374+
{"type": "raw_text", "text": "Data 2A"},
3375+
{
3376+
"type": "rich_text",
3377+
"elements": [
3378+
{
3379+
"type": "rich_text_section",
3380+
"elements": [
3381+
{
3382+
"text": "Data 2B",
3383+
"type": "link",
3384+
"url": "https://slack.com",
3385+
}
3386+
],
3387+
}
3388+
],
3389+
},
3390+
],
3391+
],
3392+
).build()
3393+
assert got == want
3394+
3395+
got = (
3396+
Table()
3397+
.add_column_setting(ColumnSettings(is_wrapped=True))
3398+
.add_column_setting(ColumnSettings(align=ColumnSettings.RIGHT))
3399+
.add_row(
3400+
RawText().text("Header A"),
3401+
RawText().text("Header B"),
3402+
)
3403+
.add_row(
3404+
RawText().text("Data 1A"),
3405+
RichText().add_element(
3406+
RichTextSection().add_element(
3407+
RichLinkEl().text("Data 1B").url("https://slack.com")
3408+
)
3409+
),
3410+
)
3411+
.add_row(
3412+
RawText().text("Data 2A"),
3413+
RichText().add_element(
3414+
RichTextSection().add_element(
3415+
RichLinkEl().text("Data 2B").url("https://slack.com")
3416+
)
3417+
),
3418+
)
3419+
).build()
3420+
assert got == want
3421+
3422+
32933423
class TestVideo:
32943424
def test_builds(self):
32953425
want = {

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)