From 038976e253c5d727c985928c760ac468d3563ca6 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 26 Jun 2026 16:46:49 -0700 Subject: [PATCH 1/9] feat(model): add table Block Kit block Add support for the `table` layout block, which renders tabular data as a grid of rows and cells. Cells may be `raw_text`, `raw_number`, or a `rich_text` block, with optional per-column alignment/wrapping settings. - TableBlock (type "table") with rows, columnSettings, blockId - TableCell interface + RawTextTableCell, RawNumberTableCell impls; RichTextBlock now also implements TableCell for rich_text cells - TableColumnSetting (align, isWrapped) - GsonTableCellFactory for polymorphic cell (de)serialization, registered in GsonFactory; TableBlock registered in GsonLayoutBlockFactory - Blocks.table(...) builder helper - Tests covering parse, round-trip, and builder usage Docs: https://docs.slack.dev/reference/block-kit/blocks/table-block Co-Authored-By: Claude --- .../com/slack/api/util/json/GsonFactory.java | 2 + .../com/slack/api/model/block/Blocks.java | 6 ++ .../api/model/block/RawNumberTableCell.java | 23 +++++ .../api/model/block/RawTextTableCell.java | 21 +++++ .../slack/api/model/block/RichTextBlock.java | 2 +- .../com/slack/api/model/block/TableBlock.java | 43 +++++++++ .../com/slack/api/model/block/TableCell.java | 18 ++++ .../api/model/block/TableColumnSetting.java | 28 ++++++ .../api/util/json/GsonLayoutBlockFactory.java | 2 + .../api/util/json/GsonTableCellFactory.java | 56 ++++++++++++ .../api/model/block/BlockKitTest.java | 90 +++++++++++++++++++ .../java/test_locally/unit/GsonFactory.java | 2 + 12 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java create mode 100644 slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java diff --git a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java index 4468f2df4..3a25b3af3 100644 --- a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java +++ b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java @@ -11,6 +11,7 @@ import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.TableCell; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; @@ -79,6 +80,7 @@ public static void registerTypeAdapters(GsonBuilder builder, boolean failOnUnkno .registerTypeAdapter(ContextActionsBlockElement.class, new GsonContextActionsBlockElementFactory(failOnUnknownProps)) .registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory(failOnUnknownProps)) .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProps)) + .registerTypeAdapter(TableCell.class, new GsonTableCellFactory(failOnUnknownProps)) .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory()) .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory(failOnUnknownProps)) .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProps)) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index fd451d8d2..b2e730fe9 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -129,4 +129,10 @@ public static ShareShortcutBlock shareShortcut() { return ShareShortcutBlock.builder().build(); } + // TableBlock + + public static TableBlock table(ModelConfigurator configurator) { + return configurator.configure(TableBlock.builder()).build(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java new file mode 100644 index 000000000..0264b068f --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java @@ -0,0 +1,23 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A {@code raw_number} table cell, holding a numeric value. The optional {@code text} + * field carries the display representation of the value (for example a formatted string). + * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawNumberTableCell implements TableCell { + public static final String TYPE = "raw_number"; + private final String type = TYPE; + private Double value; + private String text; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java new file mode 100644 index 000000000..4bd36e77c --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java @@ -0,0 +1,21 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A {@code raw_text} table cell, holding unformatted plain text. + * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawTextTableCell implements TableCell { + public static final String TYPE = "raw_text"; + private final String type = TYPE; + private String text; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java index ad204e225..ac0476e29 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java @@ -16,7 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class RichTextBlock implements LayoutBlock { +public class RichTextBlock implements LayoutBlock, TableCell { public static final String TYPE = "rich_text"; private final String type = TYPE; @Builder.Default diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java new file mode 100644 index 000000000..61debad48 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java @@ -0,0 +1,43 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays tabular data as a grid of rows and cells. + * + *

Each row is a list of {@link TableCell cells}; a cell may be a + * {@link RawTextTableCell} ({@code raw_text}), {@link RawNumberTableCell} + * ({@code raw_number}), or a {@link RichTextBlock} ({@code rich_text}). A table + * supports up to 100 rows and up to 20 cells per row, with optional per-column + * behavior configured via {@link TableColumnSetting column settings}.

+ * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TableBlock implements LayoutBlock { + public static final String TYPE = "table"; + private final String type = TYPE; + + /** + * The rows of the table. Each row is a list of cells. Maximum 100 rows, with up to + * 20 cells per row. + */ + @Builder.Default + private List> rows = new ArrayList<>(); + + /** + * Per-column behavior configuration (alignment, wrapping). Maximum 20 items. + */ + private List columnSettings; + + private String blockId; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java new file mode 100644 index 000000000..27cda7555 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java @@ -0,0 +1,18 @@ +package com.slack.api.model.block; + +/** + * A single cell within a {@link TableBlock} row. A cell can be one of: + * + *
    + *
  • {@link RawTextTableCell} ({@code raw_text})
  • + *
  • {@link RawNumberTableCell} ({@code raw_number})
  • + *
  • {@link com.slack.api.model.block.RichTextBlock RichTextBlock} ({@code rich_text})
  • + *
+ * + * @see Table block + */ +public interface TableCell { + + String getType(); + +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java new file mode 100644 index 000000000..5e0411116 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java @@ -0,0 +1,28 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Describes the behavior of a single column in a {@link TableBlock}. Up to 20 column + * settings can be supplied, one per column. + * + * @see Table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TableColumnSetting { + /** + * Horizontal text alignment for the column. One of {@code left}, {@code center}, + * or {@code right}. Defaults to {@code left}. + */ + private String align; + /** + * Whether text in the column wraps onto multiple lines. Defaults to {@code false}. + */ + private Boolean isWrapped; +} diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java index 9286e6b51..810d5a1b7 100644 --- a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java @@ -61,6 +61,8 @@ private Class getLayoutClassInstance(String typeName) { return RichTextBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; + case TableBlock.TYPE: + return TableBlock.class; default: if (failOnUnknownProperties) { throw new JsonParseException("Unsupported layout block type: " + typeName); diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java new file mode 100644 index 000000000..176f80c01 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java @@ -0,0 +1,56 @@ +package com.slack.api.util.json; + +import com.google.gson.*; +import com.slack.api.model.block.RawNumberTableCell; +import com.slack.api.model.block.RawTextTableCell; +import com.slack.api.model.block.RichTextBlock; +import com.slack.api.model.block.TableCell; + +import java.lang.reflect.Type; + +/** + * Factory for deserializing the cells of a + * {@link com.slack.api.model.block.TableBlock table block}. + * + * @see Table block + */ +public class GsonTableCellFactory implements JsonDeserializer, JsonSerializer { + + private final boolean failOnUnknownProperties; + + public GsonTableCellFactory() { + this(false); + } + + public GsonTableCellFactory(boolean failOnUnknownProperties) { + this.failOnUnknownProperties = failOnUnknownProperties; + } + + @Override + public TableCell deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + final JsonObject jsonObject = json.getAsJsonObject(); + final JsonPrimitive prim = (JsonPrimitive) jsonObject.get("type"); + final String typeName = prim.getAsString(); + final Class clazz = getTableCellClassInstance(typeName); + return context.deserialize(jsonObject, clazz); + } + + @Override + public JsonElement serialize(TableCell src, Type typeOfSrc, JsonSerializationContext context) { + return context.serialize(src); + } + + private Class getTableCellClassInstance(String typeName) { + switch (typeName) { + case RawTextTableCell.TYPE: + return RawTextTableCell.class; + case RawNumberTableCell.TYPE: + return RawNumberTableCell.class; + case RichTextBlock.TYPE: + return RichTextBlock.class; + default: + throw new JsonParseException("Unknown table cell type: " + typeName); + } + } +} diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index 1fc12d58d..aa698a375 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -1940,4 +1940,94 @@ public void parseRichTextElements() { RichTextSectionElement section = (RichTextSectionElement) ((RichTextBlock) message.getBlocks().get(0)).getElements().get(0); assertThat(section.getElements().size(), is(10)); } + + @Test + public void parseTableBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/table-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"table\",\n" + + " \"block_id\": \"table1\",\n" + + " \"column_settings\": [\n" + + " { \"align\": \"left\", \"is_wrapped\": false },\n" + + " { \"align\": \"right\", \"is_wrapped\": true }\n" + + " ],\n" + + " \"rows\": [\n" + + " [\n" + + " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + + " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [\n" + + " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Gson gson = GsonFactory.createSnakeCase(); + Message message = gson.fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + + TableBlock table = (TableBlock) message.getBlocks().get(0); + assertThat(table.getType(), is("table")); + assertThat(table.getBlockId(), is("table1")); + + assertThat(table.getColumnSettings().size(), is(2)); + assertThat(table.getColumnSettings().get(0).getAlign(), is("left")); + assertThat(table.getColumnSettings().get(0).getIsWrapped(), is(false)); + assertThat(table.getColumnSettings().get(1).getAlign(), is("right")); + assertThat(table.getColumnSettings().get(1).getIsWrapped(), is(true)); + + assertThat(table.getRows().size(), is(2)); + + RawTextTableCell header0 = (RawTextTableCell) table.getRows().get(0).get(0); + assertThat(header0.getType(), is(RawTextTableCell.TYPE)); + assertThat(header0.getText(), is("Item")); + + RichTextBlock richCell = (RichTextBlock) table.getRows().get(1).get(0); + assertThat(richCell.getType(), is(RichTextBlock.TYPE)); + RichTextSectionElement richSection = (RichTextSectionElement) richCell.getElements().get(0); + RichTextSectionElement.Text richText = (RichTextSectionElement.Text) richSection.getElements().get(0); + assertThat(richText.getText(), is("Rent")); + assertThat(richText.getStyle().isBold(), is(true)); + + RawNumberTableCell numberCell = (RawNumberTableCell) table.getRows().get(1).get(1); + assertThat(numberCell.getType(), is(RawNumberTableCell.TYPE)); + assertThat(numberCell.getValue(), is(400.0)); + assertThat(numberCell.getText(), is("$400")); + + // round-trips back to a table block + String output = gson.toJson(table); + TableBlock reparsed = gson.fromJson(output, TableBlock.class); + assertThat(reparsed.getRows().size(), is(2)); + assertThat(((RawTextTableCell) reparsed.getRows().get(0).get(1)).getText(), is("Cost")); + } + + @Test + public void buildTableBlock() { + TableBlock table = Blocks.table(t -> t + .blockId("b") + .columnSettings(Arrays.asList( + TableColumnSetting.builder().align("center").isWrapped(true).build())) + .rows(Arrays.asList( + Arrays.asList( + RawTextTableCell.builder().text("Header").build(), + RawNumberTableCell.builder().value(1.0).text("1").build())))); + assertThat(table, is(notNullValue())); + assertThat(table.getType(), is("table")); + assertThat(table.getRows().get(0).size(), is(2)); + } } diff --git a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java index 0d30bab1c..4d3b1c5fa 100644 --- a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java +++ b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java @@ -8,6 +8,7 @@ import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.TableCell; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; @@ -39,6 +40,7 @@ public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unkn .registerTypeAdapter(ContextActionsBlockElement.class, new GsonContextActionsBlockElementFactory(failOnUnknownProperties)) .registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProperties)) .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProperties)) + .registerTypeAdapter(TableCell.class, new GsonTableCellFactory(failOnUnknownProperties)) .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory(failOnUnknownProperties)) .registerTypeAdapter(Attachment.VideoHtml.class, From cdf0bf743f45bf7ef8e82ac645e942c1579a2639 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 28 Aug 2026 13:28:49 -0400 Subject: [PATCH 2/9] fix: resolve merge conflict errors in Blocks.java and BlockKitTest.java --- .../com/slack/api/model/block/Blocks.java | 1 + .../api/model/block/BlockKitTest.java | 126 ++++++++++-------- 2 files changed, 68 insertions(+), 59 deletions(-) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index b3f17b4d4..ad6160fc1 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -151,5 +151,6 @@ public static ShareShortcutBlock shareShortcut() { public static TableBlock table(ModelConfigurator configurator) { return configurator.configure(TableBlock.builder()).build(); + } } diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index 55a0996ab..fef740e75 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -1999,40 +1999,6 @@ public void parseRichTextElements() { } @Test - public void parseTableBlock() { - // https://docs.slack.dev/reference/block-kit/blocks/table-block - String json = "{\n" + - " \"blocks\": [\n" + - " {\n" + - " \"type\": \"table\",\n" + - " \"block_id\": \"table1\",\n" + - " \"column_settings\": [\n" + - " { \"align\": \"left\", \"is_wrapped\": false },\n" + - " { \"align\": \"right\", \"is_wrapped\": true }\n" + - " ],\n" + - " \"rows\": [\n" + - " [\n" + - " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + - " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + - " ],\n" + - " [\n" + - " {\n" + - " \"type\": \"rich_text\",\n" + - " \"elements\": [\n" + - " {\n" + - " \"type\": \"rich_text_section\",\n" + - " \"elements\": [\n" + - " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + - " ]\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; public void parseCardBlock() { // https://docs.slack.dev/reference/block-kit/blocks/card-block String json = "{\"blocks\":[\n" + @@ -2078,6 +2044,70 @@ public void parseCardBlock() { assertThat(message, is(notNullValue())); assertThat(message.getBlocks().size(), is(1)); + CardBlock card = (CardBlock) message.getBlocks().get(0); + assertThat(card.getType(), is("card")); + assertThat(card.getBlockId(), is("card1")); + assertThat(card.getIcon().getImageUrl(), is("https://picsum.photos/36/36")); + assertThat(card.getIcon().getAltText(), is("Icon")); + assertThat(card.getHeroImage().getImageUrl(), is("https://picsum.photos/400/300")); + assertThat(card.getHeroImage().getAltText(), is("Sample hero image")); + assertThat(card.getTitle().getType(), is("mrkdwn")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getTitle()).getText(), is("Lumon Industries")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getSubtitle()).getText(), is("Committed to work-life balance")); + assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getBody()).getText(), is("Please enjoy each card equally.")); + assertThat(card.getActions().size(), is(1)); + ButtonElement button = (ButtonElement) card.getActions().get(0); + assertThat(button.getActionId(), is("button_action")); + assertThat(button.getText().getText(), is("Action Button")); + + // Round-trip back to JSON and ensure the card survives serialization. + Message roundTrip = gson.fromJson(gson.toJson(message), Message.class); + CardBlock reparsed = (CardBlock) roundTrip.getBlocks().get(0); + assertThat(reparsed.getBlockId(), is("card1")); + assertThat(reparsed.getActions().size(), is(1)); + assertThat(((ButtonElement) reparsed.getActions().get(0)).getActionId(), is("button_action")); + } + + @Test + public void parseTableBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/table-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"table\",\n" + + " \"block_id\": \"table1\",\n" + + " \"column_settings\": [\n" + + " { \"align\": \"left\", \"is_wrapped\": false },\n" + + " { \"align\": \"right\", \"is_wrapped\": true }\n" + + " ],\n" + + " \"rows\": [\n" + + " [\n" + + " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + + " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [\n" + + " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Gson gson = GsonFactory.createSnakeCase(); + Message message = gson.fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + TableBlock table = (TableBlock) message.getBlocks().get(0); assertThat(table.getType(), is("table")); assertThat(table.getBlockId(), is("table1")); @@ -2108,9 +2138,9 @@ public void parseCardBlock() { // round-trips back to a table block String output = gson.toJson(table); - TableBlock reparsed = gson.fromJson(output, TableBlock.class); - assertThat(reparsed.getRows().size(), is(2)); - assertThat(((RawTextTableCell) reparsed.getRows().get(0).get(1)).getText(), is("Cost")); + TableBlock reparsedTable = gson.fromJson(output, TableBlock.class); + assertThat(reparsedTable.getRows().size(), is(2)); + assertThat(((RawTextTableCell) reparsedTable.getRows().get(0).get(1)).getText(), is("Cost")); } @Test @@ -2126,28 +2156,6 @@ public void buildTableBlock() { assertThat(table, is(notNullValue())); assertThat(table.getType(), is("table")); assertThat(table.getRows().get(0).size(), is(2)); - CardBlock card = (CardBlock) message.getBlocks().get(0); - assertThat(card.getType(), is("card")); - assertThat(card.getBlockId(), is("card1")); - assertThat(card.getIcon().getImageUrl(), is("https://picsum.photos/36/36")); - assertThat(card.getIcon().getAltText(), is("Icon")); - assertThat(card.getHeroImage().getImageUrl(), is("https://picsum.photos/400/300")); - assertThat(card.getHeroImage().getAltText(), is("Sample hero image")); - assertThat(card.getTitle().getType(), is("mrkdwn")); - assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getTitle()).getText(), is("Lumon Industries")); - assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getSubtitle()).getText(), is("Committed to work-life balance")); - assertThat(((com.slack.api.model.block.composition.MarkdownTextObject) card.getBody()).getText(), is("Please enjoy each card equally.")); - assertThat(card.getActions().size(), is(1)); - ButtonElement button = (ButtonElement) card.getActions().get(0); - assertThat(button.getActionId(), is("button_action")); - assertThat(button.getText().getText(), is("Action Button")); - - // Round-trip back to JSON and ensure the card survives serialization. - Message roundTrip = gson.fromJson(gson.toJson(message), Message.class); - CardBlock reparsed = (CardBlock) roundTrip.getBlocks().get(0); - assertThat(reparsed.getBlockId(), is("card1")); - assertThat(reparsed.getActions().size(), is(1)); - assertThat(((ButtonElement) reparsed.getActions().get(0)).getActionId(), is("button_action")); } @Test From 2f5a86e28886bbe4e7d12d655129ee6b2ee574c0 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 28 Aug 2026 13:35:47 -0400 Subject: [PATCH 3/9] style: alphabetize block methods, switch cases, and tests for readability --- .../com/slack/api/model/block/Blocks.java | 12 +- .../api/util/json/GsonLayoutBlockFactory.java | 40 ++-- .../api/model/block/BlockKitTest.java | 180 +++++++++--------- 3 files changed, 117 insertions(+), 115 deletions(-) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index ad6160fc1..0a593ca96 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -137,12 +137,8 @@ public static SectionBlock section(ModelConfigurator configurator) { - return configurator.configure(VideoBlock.builder()).build(); - } - // ShareShortcutBlock + public static ShareShortcutBlock shareShortcut() { return ShareShortcutBlock.builder().build(); } @@ -153,4 +149,10 @@ public static TableBlock table(ModelConfigurator c return configurator.configure(TableBlock.builder()).build(); } + // VideoBlock + + public static VideoBlock video(ModelConfigurator configurator) { + return configurator.configure(VideoBlock.builder()).build(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java index 3618b5666..62c304c98 100644 --- a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java @@ -33,42 +33,42 @@ public LayoutBlock deserialize(JsonElement json, Type typeOfT, JsonDeserializati private Class getLayoutClassInstance(String typeName) { switch (typeName) { - case SectionBlock.TYPE: - return SectionBlock.class; - case DividerBlock.TYPE: - return DividerBlock.class; - case ImageBlock.TYPE: - return ImageBlock.class; + case ActionsBlock.TYPE: + return ActionsBlock.class; + case AlertBlock.TYPE: + return AlertBlock.class; + case CallBlock.TYPE: + return CallBlock.class; + case CardBlock.TYPE: + return CardBlock.class; + case CarouselBlock.TYPE: + return CarouselBlock.class; case ContextBlock.TYPE: return ContextBlock.class; case ContextActionsBlock.TYPE: return ContextActionsBlock.class; - case CallBlock.TYPE: - return CallBlock.class; - case ActionsBlock.TYPE: - return ActionsBlock.class; + case DividerBlock.TYPE: + return DividerBlock.class; case FileBlock.TYPE: return FileBlock.class; - case InputBlock.TYPE: - return InputBlock.class; case HeaderBlock.TYPE: return HeaderBlock.class; + case ImageBlock.TYPE: + return ImageBlock.class; + case InputBlock.TYPE: + return InputBlock.class; case MarkdownBlock.TYPE: return MarkdownBlock.class; - case VideoBlock.TYPE: - return VideoBlock.class; case RichTextBlock.TYPE: return RichTextBlock.class; + case SectionBlock.TYPE: + return SectionBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; case TableBlock.TYPE: return TableBlock.class; - case AlertBlock.TYPE: - return AlertBlock.class; - case CardBlock.TYPE: - return CardBlock.class; - case CarouselBlock.TYPE: - return CarouselBlock.class; + case VideoBlock.TYPE: + return VideoBlock.class; default: if (failOnUnknownProperties) { throw new JsonParseException("Unsupported layout block type: " + typeName); diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index fef740e75..ef93a1c01 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -2068,96 +2068,6 @@ public void parseCardBlock() { assertThat(((ButtonElement) reparsed.getActions().get(0)).getActionId(), is("button_action")); } - @Test - public void parseTableBlock() { - // https://docs.slack.dev/reference/block-kit/blocks/table-block - String json = "{\n" + - " \"blocks\": [\n" + - " {\n" + - " \"type\": \"table\",\n" + - " \"block_id\": \"table1\",\n" + - " \"column_settings\": [\n" + - " { \"align\": \"left\", \"is_wrapped\": false },\n" + - " { \"align\": \"right\", \"is_wrapped\": true }\n" + - " ],\n" + - " \"rows\": [\n" + - " [\n" + - " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + - " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + - " ],\n" + - " [\n" + - " {\n" + - " \"type\": \"rich_text\",\n" + - " \"elements\": [\n" + - " {\n" + - " \"type\": \"rich_text_section\",\n" + - " \"elements\": [\n" + - " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + - " ]\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; - Gson gson = GsonFactory.createSnakeCase(); - Message message = gson.fromJson(json, Message.class); - assertThat(message, is(notNullValue())); - assertThat(message.getBlocks().size(), is(1)); - - TableBlock table = (TableBlock) message.getBlocks().get(0); - assertThat(table.getType(), is("table")); - assertThat(table.getBlockId(), is("table1")); - - assertThat(table.getColumnSettings().size(), is(2)); - assertThat(table.getColumnSettings().get(0).getAlign(), is("left")); - assertThat(table.getColumnSettings().get(0).getIsWrapped(), is(false)); - assertThat(table.getColumnSettings().get(1).getAlign(), is("right")); - assertThat(table.getColumnSettings().get(1).getIsWrapped(), is(true)); - - assertThat(table.getRows().size(), is(2)); - - RawTextTableCell header0 = (RawTextTableCell) table.getRows().get(0).get(0); - assertThat(header0.getType(), is(RawTextTableCell.TYPE)); - assertThat(header0.getText(), is("Item")); - - RichTextBlock richCell = (RichTextBlock) table.getRows().get(1).get(0); - assertThat(richCell.getType(), is(RichTextBlock.TYPE)); - RichTextSectionElement richSection = (RichTextSectionElement) richCell.getElements().get(0); - RichTextSectionElement.Text richText = (RichTextSectionElement.Text) richSection.getElements().get(0); - assertThat(richText.getText(), is("Rent")); - assertThat(richText.getStyle().isBold(), is(true)); - - RawNumberTableCell numberCell = (RawNumberTableCell) table.getRows().get(1).get(1); - assertThat(numberCell.getType(), is(RawNumberTableCell.TYPE)); - assertThat(numberCell.getValue(), is(400.0)); - assertThat(numberCell.getText(), is("$400")); - - // round-trips back to a table block - String output = gson.toJson(table); - TableBlock reparsedTable = gson.fromJson(output, TableBlock.class); - assertThat(reparsedTable.getRows().size(), is(2)); - assertThat(((RawTextTableCell) reparsedTable.getRows().get(0).get(1)).getText(), is("Cost")); - } - - @Test - public void buildTableBlock() { - TableBlock table = Blocks.table(t -> t - .blockId("b") - .columnSettings(Arrays.asList( - TableColumnSetting.builder().align("center").isWrapped(true).build())) - .rows(Arrays.asList( - Arrays.asList( - RawTextTableCell.builder().text("Header").build(), - RawNumberTableCell.builder().value(1.0).text("1").build())))); - assertThat(table, is(notNullValue())); - assertThat(table.getType(), is("table")); - assertThat(table.getRows().get(0).size(), is(2)); - } - @Test public void buildCardBlock() { CardBlock card = card(c -> c @@ -2292,4 +2202,94 @@ public void buildCarouselBlock() { assertThat(parsedCarousel.getElements().get(0).getBlockId(), is("card1")); assertThat(parsedCarousel.getElements().get(0).getActions().size(), is(1)); } + + @Test + public void parseTableBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/table-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"table\",\n" + + " \"block_id\": \"table1\",\n" + + " \"column_settings\": [\n" + + " { \"align\": \"left\", \"is_wrapped\": false },\n" + + " { \"align\": \"right\", \"is_wrapped\": true }\n" + + " ],\n" + + " \"rows\": [\n" + + " [\n" + + " { \"type\": \"raw_text\", \"text\": \"Item\" },\n" + + " { \"type\": \"raw_text\", \"text\": \"Cost\" }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [\n" + + " { \"type\": \"text\", \"text\": \"Rent\", \"style\": { \"bold\": true } }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " { \"type\": \"raw_number\", \"value\": 400, \"text\": \"$400\" }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Gson gson = GsonFactory.createSnakeCase(); + Message message = gson.fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + + TableBlock table = (TableBlock) message.getBlocks().get(0); + assertThat(table.getType(), is("table")); + assertThat(table.getBlockId(), is("table1")); + + assertThat(table.getColumnSettings().size(), is(2)); + assertThat(table.getColumnSettings().get(0).getAlign(), is("left")); + assertThat(table.getColumnSettings().get(0).getIsWrapped(), is(false)); + assertThat(table.getColumnSettings().get(1).getAlign(), is("right")); + assertThat(table.getColumnSettings().get(1).getIsWrapped(), is(true)); + + assertThat(table.getRows().size(), is(2)); + + RawTextTableCell header0 = (RawTextTableCell) table.getRows().get(0).get(0); + assertThat(header0.getType(), is(RawTextTableCell.TYPE)); + assertThat(header0.getText(), is("Item")); + + RichTextBlock richCell = (RichTextBlock) table.getRows().get(1).get(0); + assertThat(richCell.getType(), is(RichTextBlock.TYPE)); + RichTextSectionElement richSection = (RichTextSectionElement) richCell.getElements().get(0); + RichTextSectionElement.Text richText = (RichTextSectionElement.Text) richSection.getElements().get(0); + assertThat(richText.getText(), is("Rent")); + assertThat(richText.getStyle().isBold(), is(true)); + + RawNumberTableCell numberCell = (RawNumberTableCell) table.getRows().get(1).get(1); + assertThat(numberCell.getType(), is(RawNumberTableCell.TYPE)); + assertThat(numberCell.getValue(), is(400.0)); + assertThat(numberCell.getText(), is("$400")); + + // round-trips back to a table block + String output = gson.toJson(table); + TableBlock reparsedTable = gson.fromJson(output, TableBlock.class); + assertThat(reparsedTable.getRows().size(), is(2)); + assertThat(((RawTextTableCell) reparsedTable.getRows().get(0).get(1)).getText(), is("Cost")); + } + + @Test + public void buildTableBlock() { + TableBlock table = Blocks.table(t -> t + .blockId("b") + .columnSettings(Arrays.asList( + TableColumnSetting.builder().align("center").isWrapped(true).build())) + .rows(Arrays.asList( + Arrays.asList( + RawTextTableCell.builder().text("Header").build(), + RawNumberTableCell.builder().value(1.0).text("1").build())))); + assertThat(table, is(notNullValue())); + assertThat(table.getType(), is("table")); + assertThat(table.getRows().get(0).size(), is(2)); + } } From 7ecb24d90e89e7a655693026911d8a1273de1487 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 8 Sep 2026 16:12:39 -0400 Subject: [PATCH 4/9] refactor: move table cell types to composition package for reuse with data_table --- .../main/java/com/slack/api/util/json/GsonFactory.java | 2 +- .../java/com/slack/api/model/block/RichTextBlock.java | 2 +- .../main/java/com/slack/api/model/block/TableBlock.java | 8 ++------ .../model/block/{ => composition}/RawNumberTableCell.java | 3 ++- .../model/block/{ => composition}/RawTextTableCell.java | 3 ++- .../api/model/block/{ => composition}/TableCell.java | 5 +++-- .../com/slack/api/util/json/GsonTableCellFactory.java | 6 +++--- .../java/test_locally/api/model/block/BlockKitTest.java | 2 ++ .../src/test/java/test_locally/unit/GsonFactory.java | 2 +- 9 files changed, 17 insertions(+), 16 deletions(-) rename slack-api-model/src/main/java/com/slack/api/model/block/{ => composition}/RawNumberTableCell.java (80%) rename slack-api-model/src/main/java/com/slack/api/model/block/{ => composition}/RawTextTableCell.java (76%) rename slack-api-model/src/main/java/com/slack/api/model/block/{ => composition}/TableCell.java (63%) diff --git a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java index 46405a301..28a68eb0a 100644 --- a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java +++ b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java @@ -11,7 +11,7 @@ import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; import com.slack.api.model.block.LayoutBlock; -import com.slack.api.model.block.TableCell; +import com.slack.api.model.block.composition.TableCell; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java index ac0476e29..19bd1bd46 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RichTextBlock.java @@ -16,7 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class RichTextBlock implements LayoutBlock, TableCell { +public class RichTextBlock implements LayoutBlock, com.slack.api.model.block.composition.TableCell { public static final String TYPE = "rich_text"; private final String type = TYPE; @Builder.Default diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java index 61debad48..16ebf16a3 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java @@ -5,18 +5,14 @@ import lombok.Data; import lombok.NoArgsConstructor; +import com.slack.api.model.block.composition.TableCell; + import java.util.ArrayList; import java.util.List; /** * Displays tabular data as a grid of rows and cells. * - *

Each row is a list of {@link TableCell cells}; a cell may be a - * {@link RawTextTableCell} ({@code raw_text}), {@link RawNumberTableCell} - * ({@code raw_number}), or a {@link RichTextBlock} ({@code rich_text}). A table - * supports up to 100 rows and up to 20 cells per row, with optional per-column - * behavior configured via {@link TableColumnSetting column settings}.

- * * @see Table block */ @Data diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberTableCell.java similarity index 80% rename from slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java rename to slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberTableCell.java index 0264b068f..0c3f1c9e0 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberTableCell.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberTableCell.java @@ -1,4 +1,4 @@ -package com.slack.api.model.block; +package com.slack.api.model.block.composition; import lombok.AllArgsConstructor; import lombok.Builder; @@ -10,6 +10,7 @@ * field carries the display representation of the value (for example a formatted string). * * @see Table block + * @see Data table block */ @Data @Builder diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextTableCell.java similarity index 76% rename from slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java rename to slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextTableCell.java index 4bd36e77c..69e4aa983 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/RawTextTableCell.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextTableCell.java @@ -1,4 +1,4 @@ -package com.slack.api.model.block; +package com.slack.api.model.block.composition; import lombok.AllArgsConstructor; import lombok.Builder; @@ -9,6 +9,7 @@ * A {@code raw_text} table cell, holding unformatted plain text. * * @see Table block + * @see Data table block */ @Data @Builder diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java similarity index 63% rename from slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java rename to slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java index 27cda7555..ecc884e2d 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/TableCell.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java @@ -1,7 +1,7 @@ -package com.slack.api.model.block; +package com.slack.api.model.block.composition; /** - * A single cell within a {@link TableBlock} row. A cell can be one of: + * A single cell within a table or data table row. A cell can be one of: * *
    *
  • {@link RawTextTableCell} ({@code raw_text})
  • @@ -10,6 +10,7 @@ *
* * @see Table block + * @see Data table block */ public interface TableCell { diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java index 176f80c01..646f9a6de 100644 --- a/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonTableCellFactory.java @@ -1,10 +1,10 @@ package com.slack.api.util.json; import com.google.gson.*; -import com.slack.api.model.block.RawNumberTableCell; -import com.slack.api.model.block.RawTextTableCell; import com.slack.api.model.block.RichTextBlock; -import com.slack.api.model.block.TableCell; +import com.slack.api.model.block.composition.RawNumberTableCell; +import com.slack.api.model.block.composition.RawTextTableCell; +import com.slack.api.model.block.composition.TableCell; import java.lang.reflect.Type; diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index ef93a1c01..065f1cb1e 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -5,6 +5,8 @@ import com.slack.api.model.Message; import com.slack.api.model.block.*; import com.slack.api.model.block.composition.ConfirmationDialogObject; +import com.slack.api.model.block.composition.RawNumberTableCell; +import com.slack.api.model.block.composition.RawTextTableCell; import com.slack.api.model.block.element.*; import com.slack.api.model.view.View; import org.junit.Test; diff --git a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java index 4d3b1c5fa..cf45e95e4 100644 --- a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java +++ b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java @@ -8,7 +8,7 @@ import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; import com.slack.api.model.block.LayoutBlock; -import com.slack.api.model.block.TableCell; +import com.slack.api.model.block.composition.TableCell; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; From 729f537b4ff0f1a647f33e005a1770a98437c808 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 5 Nov 2026 16:00:15 -0800 Subject: [PATCH 5/9] refactor: keep block helpers in their existing order --- .../com/slack/api/model/block/Blocks.java | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index 675c0e0da..46a22b4f8 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -29,42 +29,6 @@ public static ActionsBlock actions(String blockId, List elements) return ActionsBlock.builder().blockId(blockId).elements(elements).build(); } - // AlertBlock - - public static AlertBlock alert(ModelConfigurator configurator) { - return configurator.configure(AlertBlock.builder()).build(); - } - - // AuthProviderBlock - - public static AuthProviderBlock authProvider(ModelConfigurator configurator) { - return configurator.configure(AuthProviderBlock.builder()).build(); - } - - // CallBlock - - public static CallBlock call(ModelConfigurator configurator) { - return configurator.configure(CallBlock.builder()).build(); - } - - // CardBlock - - public static CardBlock card(ModelConfigurator configurator) { - return configurator.configure(CardBlock.builder()).build(); - } - - // CarouselBlock - - public static CarouselBlock carousel(ModelConfigurator configurator) { - return configurator.configure(CarouselBlock.builder()).build(); - } - - // ContainerBlock - - public static ContainerBlock container(ModelConfigurator configurator) { - return configurator.configure(ContainerBlock.builder()).build(); - } - // ContextBlock public static ContextBlock context(ModelConfigurator configurator) { @@ -93,12 +57,6 @@ public static ContextActionsBlock contextActions(String blockId, List configurator) { - return configurator.configure(DataTableBlock.builder()).build(); - } - // DividerBlock public static DividerBlock divider(ModelConfigurator configurator) { @@ -113,18 +71,24 @@ public static DividerBlock divider() { return DividerBlock.builder().build(); } - // FileBlock - - public static FileBlock file(ModelConfigurator configurator) { - return configurator.configure(FileBlock.builder()).build(); - } - // HeaderBlock public static HeaderBlock header(ModelConfigurator configurator) { return configurator.configure(HeaderBlock.builder()).build(); } + // CallBlock + + public static CallBlock call(ModelConfigurator configurator) { + return configurator.configure(CallBlock.builder()).build(); + } + + // FileBlock + + public static FileBlock file(ModelConfigurator configurator) { + return configurator.configure(FileBlock.builder()).build(); + } + // ImageBlock public static ImageBlock image(ModelConfigurator configurator) { @@ -155,12 +119,52 @@ public static SectionBlock section(ModelConfigurator configurator) { + return configurator.configure(VideoBlock.builder()).build(); + } + // ShareShortcutBlock public static ShareShortcutBlock shareShortcut() { return ShareShortcutBlock.builder().build(); } + // AlertBlock + + public static AlertBlock alert(ModelConfigurator configurator) { + return configurator.configure(AlertBlock.builder()).build(); + } + + // AuthProviderBlock + + public static AuthProviderBlock authProvider(ModelConfigurator configurator) { + return configurator.configure(AuthProviderBlock.builder()).build(); + } + + // CardBlock + + public static CardBlock card(ModelConfigurator configurator) { + return configurator.configure(CardBlock.builder()).build(); + } + + // CarouselBlock + + public static CarouselBlock carousel(ModelConfigurator configurator) { + return configurator.configure(CarouselBlock.builder()).build(); + } + + // ContainerBlock + + public static ContainerBlock container(ModelConfigurator configurator) { + return configurator.configure(ContainerBlock.builder()).build(); + } + + // DataTableBlock + + public static DataTableBlock dataTable(ModelConfigurator configurator) { + return configurator.configure(DataTableBlock.builder()).build(); + } + // TableBlock public static TableBlock table(ModelConfigurator configurator) { @@ -173,10 +177,4 @@ public static TaskCardBlock taskCard(ModelConfigurator configurator) { - return configurator.configure(VideoBlock.builder()).build(); - } - } From db4c6c7fb52c47e55c23d75256760d5864556be4 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 5 Nov 2026 20:27:02 -0800 Subject: [PATCH 6/9] docs: link the table block from the shared table cell types Co-Authored-By: Claude Co-Authored-By: Claude Opus 5.5 (1M context) --- .../com/slack/api/model/block/composition/RawNumberObject.java | 1 + .../com/slack/api/model/block/composition/RawTextObject.java | 1 + .../java/com/slack/api/model/block/composition/TableCell.java | 1 + 3 files changed, 3 insertions(+) diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberObject.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberObject.java index 5c845eb09..8a71bf4a1 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberObject.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberObject.java @@ -8,6 +8,7 @@ /** * Defines an object containing a numeric value. * + * @see Table block * @see Data table block */ @Data diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextObject.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextObject.java index 6672aefff..aa3a1a6f5 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextObject.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextObject.java @@ -8,6 +8,7 @@ /** * Defines an object containing some text. * + * @see Table block * @see Data table block */ @Data diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java index 283a6ecb4..bed45dc8e 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java @@ -10,6 +10,7 @@ *
  • {@link com.slack.api.model.block.RichTextBlock RichTextBlock} ({@code rich_text})
  • * * + * @see Table block * @see Data table block */ public interface TableCell { From 9325589cc1a3f38daeb248a79906647de1d4f6c3 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 5 Nov 2026 20:27:35 -0800 Subject: [PATCH 7/9] test: restore the json parser import for the table tests Co-Authored-By: Claude Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/test/java/test_locally/api/model/block/BlockKitTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index f448246e2..77cb27f3f 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; import com.slack.api.model.Message; import com.slack.api.model.block.*; import com.slack.api.model.block.composition.ActionCellObject; From 0336acb4d4449e6da286797ca1f41146750d6535 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 5 Nov 2026 21:05:28 -0800 Subject: [PATCH 8/9] test: fold the raw_number cell into the table parse test Co-Authored-By: Claude Co-Authored-By: Claude Opus 5.5 (1M context) --- .../api/model/block/BlockKitTest.java | 69 ++++--------------- 1 file changed, 13 insertions(+), 56 deletions(-) diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index 77cb27f3f..aa2997663 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -2779,6 +2779,17 @@ public void parseTableBlock() { " }\n" + " ]\n" + " }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"raw_text\",\n" + + " \"text\": \"Data 3A\"\n" + + " },\n" + + " {\n" + + " \"type\": \"raw_number\",\n" + + " \"value\": 400.5,\n" + + " \"text\": \"$400.50\"\n" + + " }\n" + " ]\n" + " ]\n" + " }\n" + @@ -2796,7 +2807,7 @@ public void parseTableBlock() { assertThat(block.getColumnSettings().get(0).getIsWrapped(), is(true)); assertThat(block.getColumnSettings().get(1).getAlign(), is("right")); assertThat(block.getColumnSettings().get(1).getIsWrapped(), is(nullValue())); - assertThat(block.getRows().size(), is(3)); + assertThat(block.getRows().size(), is(4)); RawTextObject header = (RawTextObject) block.getRows().get(0).get(0); assertThat(header.getType(), is("raw_text")); @@ -2809,61 +2820,7 @@ public void parseTableBlock() { assertThat(link.getText(), is("Data 1B")); assertThat(link.getUrl(), is("https://slack.com")); - assertThat( - gson.toJsonTree(message.getBlocks()), - is(JsonParser.parseString(json).getAsJsonObject().get("blocks"))); - } - - @Test - public void parseTableBlockWithRawNumber() { - // https://docs.slack.dev/reference/block-kit/blocks/table-block - String json = "{\n" + - " \"blocks\": [\n" + - " {\n" + - " \"type\": \"table\",\n" + - " \"block_id\": \"bid\",\n" + - " \"column_settings\": [\n" + - " null,\n" + - " {\n" + - " \"align\": \"right\",\n" + - " \"is_wrapped\": false\n" + - " }\n" + - " ],\n" + - " \"rows\": [\n" + - " [\n" + - " {\n" + - " \"type\": \"raw_text\",\n" + - " \"text\": \"Item\"\n" + - " },\n" + - " {\n" + - " \"type\": \"raw_text\",\n" + - " \"text\": \"Cost\"\n" + - " }\n" + - " ],\n" + - " [\n" + - " {\n" + - " \"type\": \"raw_text\",\n" + - " \"text\": \"Rent\"\n" + - " },\n" + - " {\n" + - " \"type\": \"raw_number\",\n" + - " \"value\": 400.5,\n" + - " \"text\": \"$400.50\"\n" + - " }\n" + - " ]\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; - Gson gson = GsonFactory.createSnakeCase(); - Message message = gson.fromJson(json, Message.class); - TableBlock block = (TableBlock) message.getBlocks().get(0); - assertThat(block.getBlockId(), is("bid")); - assertThat(block.getColumnSettings().get(0), is(nullValue())); - assertThat(block.getColumnSettings().get(1).getAlign(), is("right")); - assertThat(block.getColumnSettings().get(1).getIsWrapped(), is(false)); - - RawNumberObject numberCell = (RawNumberObject) block.getRows().get(1).get(1); + RawNumberObject numberCell = (RawNumberObject) block.getRows().get(3).get(1); assertThat(numberCell.getType(), is("raw_number")); assertThat(numberCell.getValue(), is(400.5)); assertThat(numberCell.getText(), is("$400.50")); From 872276b2a85b118f5e85dd613d7c4cae84c89ea7 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 5 Nov 2026 21:14:34 -0800 Subject: [PATCH 9/9] refactor: nest the column setting in the table block Co-Authored-By: Claude Co-Authored-By: Claude Opus 5.5 (1M context) --- .../com/slack/api/model/block/TableBlock.java | 25 +++++++++++++++- .../api/model/block/TableColumnSetting.java | 29 ------------------- .../api/model/block/BlockKitTest.java | 4 +-- 3 files changed, 26 insertions(+), 32 deletions(-) delete mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java index 3bcb48748..b9f41e28e 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TableBlock.java @@ -35,7 +35,30 @@ public class TableBlock implements LayoutBlock { * same number of columns in the table as there are in the array itself. Any additional columns will have the * default behavior. Maximum 20 items. */ - private List columnSettings; + private List columnSettings; private String blockId; + + /** + * Describes the behavior of a column in a {@link TableBlock}. + * + * @see Schema for column_settings + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ColumnSetting { + + /** + * The alignment for items in this column. Can be {@code left}, {@code center}, or {@code right}. Defaults to + * {@code left} if not defined. + */ + private String align; + + /** + * Whether the contents of this column should be wrapped or not. Defaults to {@code false} if not defined. + */ + private Boolean isWrapped; + } } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java b/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java deleted file mode 100644 index 403b170e6..000000000 --- a/slack-api-model/src/main/java/com/slack/api/model/block/TableColumnSetting.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.slack.api.model.block; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Describes the behavior of a column in a {@link TableBlock}. - * - * @see Schema for column_settings - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class TableColumnSetting { - - /** - * The alignment for items in this column. Can be {@code left}, {@code center}, or {@code right}. Defaults to - * {@code left} if not defined. - */ - private String align; - - /** - * Whether the contents of this column should be wrapped or not. Defaults to {@code false} if not defined. - */ - private Boolean isWrapped; -} diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index aa2997663..d8456cdd0 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -2835,8 +2835,8 @@ public void buildTableBlock() { TableBlock block = table(t -> t .blockId("bid") .columnSettings(Arrays.asList( - TableColumnSetting.builder().isWrapped(true).build(), - TableColumnSetting.builder().align("right").build() + TableBlock.ColumnSetting.builder().isWrapped(true).build(), + TableBlock.ColumnSetting.builder().align("right").build() )) .rows(Arrays.asList( Arrays.asList(