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 a823465cc..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,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.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; @@ -80,6 +81,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 fe1eb8e13..3c94429b8 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 @@ -153,6 +153,12 @@ public static ContainerBlock container(ModelConfigurator configurator) { + return configurator.configure(DataTableBlock.builder()).build(); + } + // TaskCardBlock public static TaskCardBlock taskCard(ModelConfigurator configurator) { 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..83f259409 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataTableBlock.java @@ -0,0 +1,48 @@ +package com.slack.api.model.block; + +import com.slack.api.model.block.composition.TableCell; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays rich tables that support pagination, sorting, filtering, and interactivity. + * + * @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; + + /** + * An array consisting of table rows. + */ + @Builder.Default + private List> rows = new ArrayList<>(); + + /** + * A caption for the table; used as the value for the HTML caption element. + */ + private String caption; + + /** + * Number of rows per page. Min {@code 1}, Max {@code 100}. Defaults to {@code 5} if omitted. + */ + private Integer pageSize; + + /** + * The 0-based index of the column that uniquely identifies each row (the row header). This column + * is treated as the row's primary identifier for screen readers. Defaults to {@code 0} if omitted. + */ + private Integer rowHeaderColumnIndex; + + private String blockId; +} 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..0af63bb7b 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 @@ -1,5 +1,6 @@ package com.slack.api.model.block; +import com.slack.api.model.block.composition.TableCell; import com.slack.api.model.block.element.BlockElement; import lombok.AllArgsConstructor; import lombok.Builder; @@ -16,7 +17,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/composition/BlockCompositions.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/BlockCompositions.java index f6413c5f0..e09e12ce5 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/composition/BlockCompositions.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/BlockCompositions.java @@ -54,6 +54,26 @@ public static MarkdownTextObject markdownText(String text) { return MarkdownTextObject.builder().text(text).build(); } + // RawNumberObject + + public static RawNumberObject rawNumber(ModelConfigurator configurator) { + return configurator.configure(RawNumberObject.builder()).build(); + } + + public static RawNumberObject rawNumber(Double value, String text) { + return RawNumberObject.builder().value(value).text(text).build(); + } + + // RawTextObject + + public static RawTextObject rawText(ModelConfigurator configurator) { + return configurator.configure(RawTextObject.builder()).build(); + } + + public static RawTextObject rawText(String text) { + return RawTextObject.builder().text(text).build(); + } + // OptionGroupObject public static OptionGroupObject optionGroup(ModelConfigurator configurator) { 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 new file mode 100644 index 000000000..8a71bf4a1 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawNumberObject.java @@ -0,0 +1,31 @@ +package com.slack.api.model.block.composition; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Defines an object containing a numeric value. + * + * @see Table block + * @see Data table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawNumberObject implements TableCell { + public static final String TYPE = "raw_number"; + private final String type = TYPE; + + /** + * The numeric value. + */ + private Double value; + + /** + * The text used to display the value. The minimum length is 1 character. + */ + private String text; +} 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 new file mode 100644 index 000000000..aa3a1a6f5 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/RawTextObject.java @@ -0,0 +1,26 @@ +package com.slack.api.model.block.composition; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Defines an object containing some text. + * + * @see Table block + * @see Data table block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RawTextObject implements TableCell { + public static final String TYPE = "raw_text"; + private final String type = TYPE; + + /** + * The text for the block. The minimum length is 1 character. + */ + private String text; +} 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 new file mode 100644 index 000000000..d83765f5f --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/TableCell.java @@ -0,0 +1,19 @@ +package com.slack.api.model.block.composition; + +/** + * A single cell within a table or data table row. A cell can be one of: + * + *
    + *
  • {@link RawTextObject} ({@code raw_text})
  • + *
  • {@link RawNumberObject} ({@code raw_number})
  • + *
  • {@link com.slack.api.model.block.RichTextBlock RichTextBlock} ({@code rich_text})
  • + *
+ * + * @see Table block + * @see Data table block + */ +public interface TableCell { + + String getType(); + +} 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 4c424cade..9fc4f7605 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 @@ -49,6 +49,8 @@ private Class getLayoutClassInstance(String typeName) { return ContextActionsBlock.class; case ContextBlock.TYPE: return ContextBlock.class; + case DataTableBlock.TYPE: + return DataTableBlock.class; case DividerBlock.TYPE: return DividerBlock.class; case FileBlock.TYPE: 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..d2b01ff7f --- /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.RichTextBlock; +import com.slack.api.model.block.composition.RawNumberObject; +import com.slack.api.model.block.composition.RawTextObject; +import com.slack.api.model.block.composition.TableCell; + +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 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 RawTextObject.TYPE: + return RawTextObject.class; + case RawNumberObject.TYPE: + return RawNumberObject.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 302b73bc5..4eeada10f 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,9 @@ 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.RawNumberObject; +import com.slack.api.model.block.composition.RawTextObject; +import com.slack.api.model.block.composition.TableCell; import com.slack.api.model.block.element.*; import com.slack.api.model.view.View; import org.junit.Test; @@ -1742,6 +1745,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)); + + RawTextObject header = (RawTextObject) block.getRows().get(0).get(0); + assertThat(header.getType(), is("raw_text")); + assertThat(header.getText(), is("Team")); + + TableCell richCell = block.getRows().get(1).get(0); + assertTrue(richCell instanceof RichTextBlock); + assertThat(richCell.getType(), is("rich_text")); + + RawNumberObject numberCell = (RawNumberObject) 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( + RawTextObject.builder().text("Team").build(), + RawTextObject.builder().text("Revenue").build() + ), + Arrays.asList( + RawTextObject.builder().text("Platform").build(), + RawNumberObject.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)); + RawNumberObject numberCell = (RawNumberObject) restored.getRows().get(1).get(1); + assertThat(numberCell.getValue(), is(1234.5)); + } + @Test public void parseAlertBlocks() { // https://docs.slack.dev/reference/block-kit/blocks/alert-block 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..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,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.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; @@ -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,