Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ public static DataTableBlock dataTable(ModelConfigurator<DataTableBlock.DataTabl
return configurator.configure(DataTableBlock.builder()).build();
}

// TableBlock

public static TableBlock table(ModelConfigurator<TableBlock.TableBlockBuilder> configurator) {
return configurator.configure(TableBlock.builder()).build();
}

// TaskCardBlock

public static TaskCardBlock taskCard(ModelConfigurator<TaskCardBlock.TaskCardBlockBuilder> configurator) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 structured information in a table.
*
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/table-block">Table block</a>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TableBlock implements LayoutBlock {
public static final String TYPE = "table";
private final String type = TYPE;

/**
* An array consisting of table rows. Maximum 100 rows. Each row object is an array with a max of 20 table
* cells. Table cells can have a type of {@code rich_text}, {@code raw_text}, or {@code raw_number}.
*/
Comment on lines +25 to +28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪓 suggestion (non-blocking): I think we might not need the field Javadoc here and on ColumnSetting. It restates the docs page (limits, defaults, even its doubled "the the" below), which the class @see already links, so it can drift. Same idea for the // TableBlock marker in Blocks.java and the docs URL comment in the test.

Suggested change
/**
* An array consisting of table rows. Maximum 100 rows. Each row object is an array with a max of 20 table
* cells. Table cells can have a type of {@code rich_text}, {@code raw_text}, or {@code raw_number}.
*/

@Builder.Default
private List<List<TableCell>> rows = new ArrayList<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 question (non-blocking): rows takes any TableCell, which includes ActionCellObject, so a table can build (and GsonTableCellFactory will parse) an action_cell that the table block spec doesn't allow. Right now only the TableCell Javadoc says it's for data tables. What do you think about a narrower cell type for table rows so the type carries that rule? Fair to skip if a second factory isn't worth it 🤔


/**
* An array describing column behavior. If there are fewer items in the {@code column_settings} array than
* there are columns in the table, then the items in the the {@code column_settings} array will describe the
* 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<ColumnSetting> columnSettings;

private String blockId;

/**
* Describes the behavior of a column in a {@link TableBlock}.
*
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/table-block#schema-for-column_settings">Schema for column_settings</a>
*/
@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/**
* Defines an object containing a numeric value.
*
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/table-block">Table block</a>
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/data-table-block">Data table block</a>
*/
@Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/**
* Defines an object containing some text.
*
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/table-block">Table block</a>
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/data-table-block">Data table block</a>
*/
@Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* <li>{@link com.slack.api.model.block.RichTextBlock RichTextBlock} ({@code rich_text})</li>
* </ul>
*
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/table-block">Table block</a>
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/data-table-block">Data table block</a>
*/
public interface TableCell {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ private Class<? extends LayoutBlock> getLayoutClassInstance(String typeName) {
return SectionBlock.class;
case ShareShortcutBlock.TYPE:
return ShareShortcutBlock.class;
case TableBlock.TYPE:
return TableBlock.class;
case TaskCardBlock.TYPE:
return TaskCardBlock.class;
case VideoBlock.TYPE:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

/**
* Factory for deserializing the cells of a
* {@link com.slack.api.model.block.TableBlock table block} or a
* {@link com.slack.api.model.block.DataTableBlock data table block}.
*
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/table-block">Table block</a>
* @see <a href="https://docs.slack.dev/reference/block-kit/blocks/data-table-block">Data table block</a>
*/
public class GsonTableCellFactory implements JsonDeserializer<TableCell>, JsonSerializer<TableCell> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2710,4 +2711,156 @@ public void buildAuthProviderBlock() {
"\"name\":{\"type\":\"plain_text\",\"text\":\"Acme\"}," +
"\"is_connected\":false,\"auth_url\":\"https://acme.example.com/oauth\"}"));
}

@Test
public void parseTableBlock() {
// https://docs.slack.dev/reference/block-kit/blocks/table-block
String json = "{\n" +
" \"blocks\": [\n" +
" {\n" +
" \"type\": \"table\",\n" +
" \"column_settings\": [\n" +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 suggestion (non-blocking): the spec says a null entry in column_settings skips that column. Could we add a null to this fixture? The existing toJsonTree round-trip would pin it for free.

" {\n" +
" \"is_wrapped\": true\n" +
" },\n" +
" {\n" +
" \"align\": \"right\"\n" +
" }\n" +
" ],\n" +
" \"rows\": [\n" +
" [\n" +
" {\n" +
" \"type\": \"raw_text\",\n" +
" \"text\": \"Header A\"\n" +
" },\n" +
" {\n" +
" \"type\": \"raw_text\",\n" +
" \"text\": \"Header B\"\n" +
" }\n" +
" ],\n" +
" [\n" +
" {\n" +
" \"type\": \"raw_text\",\n" +
" \"text\": \"Data 1A\"\n" +
" },\n" +
" {\n" +
" \"type\": \"rich_text\",\n" +
" \"elements\": [\n" +
" {\n" +
" \"type\": \"rich_text_section\",\n" +
" \"elements\": [\n" +
" {\n" +
" \"text\": \"Data 1B\",\n" +
" \"type\": \"link\",\n" +
" \"url\": \"https://slack.com\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" ]\n" +
" }\n" +
" ],\n" +
" [\n" +
" {\n" +
" \"type\": \"raw_text\",\n" +
" \"text\": \"Data 2A\"\n" +
" },\n" +
" {\n" +
" \"type\": \"rich_text\",\n" +
" \"elements\": [\n" +
" {\n" +
" \"type\": \"rich_text_section\",\n" +
" \"elements\": [\n" +
" {\n" +
" \"text\": \"Data 2B\",\n" +
" \"type\": \"link\",\n" +
" \"url\": \"https://slack.com\"\n" +
" }\n" +
" ]\n" +
" }\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" +
" ]\n" +
"}";
Gson gson = GsonFactory.createSnakeCase();
Message message = gson.fromJson(json, Message.class);
assertThat(message, is(notNullValue()));
assertThat(message.getBlocks().size(), is(1));

TableBlock block = (TableBlock) message.getBlocks().get(0);
assertThat(block.getType(), is("table"));
assertThat(block.getColumnSettings().size(), is(2));
assertThat(block.getColumnSettings().get(0).getAlign(), is(nullValue()));
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(4));

RawTextObject header = (RawTextObject) block.getRows().get(0).get(0);
assertThat(header.getType(), is("raw_text"));
assertThat(header.getText(), is("Header A"));

RichTextBlock richCell = (RichTextBlock) block.getRows().get(1).get(1);
assertThat(richCell.getType(), is("rich_text"));
RichTextSectionElement section = (RichTextSectionElement) richCell.getElements().get(0);
RichTextSectionElement.Link link = (RichTextSectionElement.Link) section.getElements().get(0);
assertThat(link.getText(), is("Data 1B"));
assertThat(link.getUrl(), is("https://slack.com"));

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"));

assertThat(
gson.toJsonTree(message.getBlocks()),
is(JsonParser.parseString(json).getAsJsonObject().get("blocks")));
}

@Test
public void buildTableBlock() {
TableBlock block = table(t -> t
.blockId("bid")
.columnSettings(Arrays.asList(
TableBlock.ColumnSetting.builder().isWrapped(true).build(),
TableBlock.ColumnSetting.builder().align("right").build()
))
Comment on lines +2837 to +2840

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 note: Instead of a separate class we might prefer to nest the column settings of a table within the TableBlock class! Here's how it appears in actual code.

.rows(Arrays.asList(
Arrays.asList(
RawTextObject.builder().text("Item").build(),
RawTextObject.builder().text("Cost").build()
),
Arrays.asList(
RawTextObject.builder().text("Rent").build(),
RawNumberObject.builder().value(400.5).text("$400.50").build()
)
)));
assertThat(block, is(notNullValue()));

Gson gson = GsonFactory.createSnakeCase();
String json = gson.toJson(block);
assertThat(json, is("{\"type\":\"table\"," +
"\"rows\":[" +
"[{\"type\":\"raw_text\",\"text\":\"Item\"},{\"type\":\"raw_text\",\"text\":\"Cost\"}]," +
"[{\"type\":\"raw_text\",\"text\":\"Rent\"},{\"type\":\"raw_number\",\"value\":400.5,\"text\":\"$400.50\"}]" +
"]," +
"\"column_settings\":[{\"is_wrapped\":true},{\"align\":\"right\"}]," +
"\"block_id\":\"bid\"}"));

TableBlock restored = (TableBlock) gson.fromJson(json, LayoutBlock.class);
assertThat(restored, is(block));
}
}
Loading