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..b8bf7ff9d 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 @@ -10,6 +10,7 @@ import com.slack.api.model.admin.AppWorkflow; import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; +import com.slack.api.model.block.DataTableCell; import com.slack.api.model.block.LayoutBlock; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; @@ -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(DataTableCell.class, new GsonDataTableCellFactory(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..747fe8d7a 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 @@ -119,6 +119,12 @@ public static SectionBlock section(ModelConfigurator configurator) { + return configurator.configure(DataTableBlock.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/model/block/DataTableBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataTableBlock.java new file mode 100644 index 000000000..b6f5d3880 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataTableBlock.java @@ -0,0 +1,55 @@ +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 a sortable, paginated grid of tabular data. + * + *

Each row is a list of {@link DataTableCell cells}; a cell may be a + * {@link RawTextDataTableCell} ({@code raw_text}), {@link RawNumberDataTableCell} + * ({@code raw_number}), or a {@link RichTextBlock} ({@code rich_text}). The first + * row is the header row; header cells cannot use {@code rich_text}. A data table + * supports 1-20 columns and up to 100 data rows (101 rows including the header), + * with all rows sharing the same column count.

+ * + * @see Data table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataTableBlock implements LayoutBlock { + public static final String TYPE = "data_table"; + private final String type = TYPE; + + /** + * The rows of the table. Each row is a list of cells. The first row is the header row. + * Minimum 2 rows (header plus one data row); maximum 101 rows. Every row must contain + * the same number of cells (1-20). + */ + @Builder.Default + private List> rows = new ArrayList<>(); + + /** + * Required. The caption describing the table, used as the caption of the rendered HTML element. + */ + private String caption; + + /** + * The number of rows shown per page. Valid range 1-100; defaults to 5 when omitted. + */ + private Integer pageSize; + + /** + * Zero-based index of the column that identifies each row. Defaults to 0 when omitted. + */ + private Integer rowHeaderColumnIndex; + + private String blockId; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/DataTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataTableCell.java new file mode 100644 index 000000000..36a7f4ae5 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataTableCell.java @@ -0,0 +1,20 @@ +package com.slack.api.model.block; + +/** + * A single cell within a {@link DataTableBlock} row. A cell can be one of: + * + *
    + *
  • {@link RawTextDataTableCell} ({@code raw_text})
  • + *
  • {@link RawNumberDataTableCell} ({@code raw_number})
  • + *
  • {@link com.slack.api.model.block.RichTextBlock RichTextBlock} ({@code rich_text})
  • + *
+ * + *

Header cells (those in the first row) cannot use the {@code rich_text} type.

+ * + * @see Data table block + */ +public interface DataTableCell { + + String getType(); + +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberDataTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberDataTableCell.java new file mode 100644 index 000000000..57ffab1e8 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RawNumberDataTableCell.java @@ -0,0 +1,25 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A {@code raw_number} cell within a {@link DataTableBlock}, holding a numeric value. + * The {@code text} field carries the display representation of the value (for example a + * formatted string) and must be at least one character long. Columns made up entirely of + * {@code raw_number} cells are sorted numerically. + * + * @see Data table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawNumberDataTableCell implements DataTableCell { + 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/RawTextDataTableCell.java b/slack-api-model/src/main/java/com/slack/api/model/block/RawTextDataTableCell.java new file mode 100644 index 000000000..1b2230565 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/RawTextDataTableCell.java @@ -0,0 +1,22 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A {@code raw_text} cell within a {@link DataTableBlock}, holding unformatted plain text. + * The {@code text} must be at least one character long. + * + * @see Data table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawTextDataTableCell implements DataTableCell { + 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..2b905db59 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, DataTableCell { 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/util/json/GsonDataTableCellFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonDataTableCellFactory.java new file mode 100644 index 000000000..d45fa9f5e --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonDataTableCellFactory.java @@ -0,0 +1,56 @@ +package com.slack.api.util.json; + +import com.google.gson.*; +import com.slack.api.model.block.DataTableCell; +import com.slack.api.model.block.RawNumberDataTableCell; +import com.slack.api.model.block.RawTextDataTableCell; +import com.slack.api.model.block.RichTextBlock; + +import java.lang.reflect.Type; + +/** + * Factory for deserializing the cells of a + * {@link com.slack.api.model.block.DataTableBlock data table block}. + * + * @see Data table block + */ +public class GsonDataTableCellFactory implements JsonDeserializer, JsonSerializer { + + private final boolean failOnUnknownProperties; + + public GsonDataTableCellFactory() { + this(false); + } + + public GsonDataTableCellFactory(boolean failOnUnknownProperties) { + this.failOnUnknownProperties = failOnUnknownProperties; + } + + @Override + public DataTableCell 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 = getDataTableCellClassInstance(typeName); + return context.deserialize(jsonObject, clazz); + } + + @Override + public JsonElement serialize(DataTableCell src, Type typeOfSrc, JsonSerializationContext context) { + return context.serialize(src); + } + + private Class getDataTableCellClassInstance(String typeName) { + switch (typeName) { + case RawTextDataTableCell.TYPE: + return RawTextDataTableCell.class; + case RawNumberDataTableCell.TYPE: + return RawNumberDataTableCell.class; + case RichTextBlock.TYPE: + return RichTextBlock.class; + default: + throw new JsonParseException("Unknown data table cell type: " + typeName); + } + } +} 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..c22319913 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 @@ -59,6 +59,8 @@ private Class getLayoutClassInstance(String typeName) { return VideoBlock.class; case RichTextBlock.TYPE: return RichTextBlock.class; + case DataTableBlock.TYPE: + return DataTableBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; default: 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..f9ce217ce 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 @@ -1742,6 +1742,94 @@ public void parseVideoBlocks() { assertEquals("https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1", block.getVideoUrl()); } + @Test + public void parseDataTableBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/data-table-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"data_table\",\n" + + " \"block_id\": \"bid\",\n" + + " \"caption\": \"Quarterly results\",\n" + + " \"page_size\": 10,\n" + + " \"row_header_column_index\": 0,\n" + + " \"rows\": [\n" + + " [\n" + + " { \"type\": \"raw_text\", \"text\": \"Team\" },\n" + + " { \"type\": \"raw_text\", \"text\": \"Revenue\" }\n" + + " ],\n" + + " [\n" + + " {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [ { \"type\": \"text\", \"text\": \"Platform\" } ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " { \"type\": \"raw_number\", \"value\": 1234.5, \"text\": \"$1,234.50\" }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Message message = GsonFactory.createSnakeCase().fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + + DataTableBlock block = (DataTableBlock) message.getBlocks().get(0); + assertThat(block.getType(), is("data_table")); + assertThat(block.getBlockId(), is("bid")); + assertThat(block.getCaption(), is("Quarterly results")); + assertThat(block.getPageSize(), is(10)); + assertThat(block.getRowHeaderColumnIndex(), is(0)); + assertThat(block.getRows().size(), is(2)); + + RawTextDataTableCell header = (RawTextDataTableCell) block.getRows().get(0).get(0); + assertThat(header.getType(), is("raw_text")); + assertThat(header.getText(), is("Team")); + + DataTableCell richCell = block.getRows().get(1).get(0); + assertTrue(richCell instanceof RichTextBlock); + assertThat(richCell.getType(), is("rich_text")); + + RawNumberDataTableCell numberCell = (RawNumberDataTableCell) block.getRows().get(1).get(1); + assertThat(numberCell.getType(), is("raw_number")); + assertThat(numberCell.getValue(), is(1234.5)); + assertThat(numberCell.getText(), is("$1,234.50")); + } + + @Test + public void buildDataTableBlock() { + DataTableBlock block = dataTable(t -> t + .blockId("bid") + .caption("Quarterly results") + .pageSize(10) + .rowHeaderColumnIndex(0) + .rows(Arrays.asList( + Arrays.asList( + RawTextDataTableCell.builder().text("Team").build(), + RawTextDataTableCell.builder().text("Revenue").build() + ), + Arrays.asList( + RawTextDataTableCell.builder().text("Platform").build(), + RawNumberDataTableCell.builder().value(1234.5).text("$1,234.50").build() + ) + ))); + assertThat(block, is(notNullValue())); + + Gson gson = GsonFactory.createSnakeCase(); + String json = gson.toJson(block); + DataTableBlock restored = (DataTableBlock) gson.fromJson(json, LayoutBlock.class); + assertThat(restored.getType(), is("data_table")); + assertThat(restored.getCaption(), is("Quarterly results")); + assertThat(restored.getPageSize(), is(10)); + assertThat(restored.getRows().size(), is(2)); + RawNumberDataTableCell numberCell = (RawNumberDataTableCell) restored.getRows().get(1).get(1); + assertThat(numberCell.getValue(), is(1234.5)); + } + @Test public void parseLinkTriggerMessages() { // https://tools.slack.dev/deno-slack-sdk/ 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..f114eb8eb 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 @@ -7,6 +7,7 @@ import com.slack.api.model.File; import com.slack.api.model.block.ContextBlockElement; import com.slack.api.model.block.ContextActionsBlockElement; +import com.slack.api.model.block.DataTableCell; import com.slack.api.model.block.LayoutBlock; import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; @@ -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(DataTableCell.class, new GsonDataTableCellFactory(failOnUnknownProperties)) .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory(failOnUnknownProperties)) .registerTypeAdapter(Attachment.VideoHtml.class,