diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ActionsIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ActionsIF.java index 206ef082..649e0d18 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ActionsIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ActionsIF.java @@ -11,7 +11,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface ActionsIF extends Block { +public interface ActionsIF extends Block, ContainerChildBlock { String TYPE = "actions"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java index 783d42aa..7d3e0c68 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/Block.java @@ -33,6 +33,7 @@ @JsonSubTypes.Type(value = Card.class, name = Card.TYPE), @JsonSubTypes.Type(value = Table.class, name = Table.TYPE), @JsonSubTypes.Type(value = DataTable.class, name = DataTable.TYPE), + @JsonSubTypes.Type(value = ContainerBlock.class, name = ContainerBlock.TYPE), } ) @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java index c813da6a..786f13db 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/BlockElementLengthLimits.java @@ -20,7 +20,8 @@ public enum BlockElementLengthLimits { MAX_TABLE_COLUMNS(20), MAX_TABLE_BLOCK_ID_LENGTH(255), MIN_DATA_TABLE_ROWS(2), - MAX_DATA_TABLE_PAGE_SIZE(100); + MAX_DATA_TABLE_PAGE_SIZE(100), + MAX_CONTAINER_CHILD_BLOCKS(10); private final int limit; diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerBlockIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerBlockIF.java new file mode 100644 index 00000000..d7eb3329 --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerBlockIF.java @@ -0,0 +1,95 @@ +package com.hubspot.slack.client.models.blocks; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.hubspot.immutables.style.HubSpotStyle; +import com.hubspot.slack.client.models.blocks.elements.Image; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBlock; +import com.hubspot.slack.client.models.blocks.objects.Text; +import com.hubspot.slack.client.models.blocks.objects.TextType; +import java.util.Optional; +import org.immutables.value.Value; +import org.immutables.value.Value.Check; +import org.immutables.value.Value.Immutable; + +@Immutable +@HubSpotStyle +@JsonNaming(SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_ABSENT) +public interface ContainerBlockIF extends Block { + String TYPE = "container"; + + @Override + @Value.Derived + default String getType() { + return TYPE; + } + + Optional getTitle(); + + Optional getRichTextTitle(); + + ImmutableList getChildBlocks(); + + Optional getSubtitle(); + + Optional getIcon(); + + Optional getWidth(); + + Optional getHasHeaderDivider(); + + @JsonProperty("is_collapsible") + Optional isCollapsible(); + + @JsonProperty("default_collapsed") + Optional isDefaultCollapsed(); + + @Check + default void check() { + Preconditions.checkState( + getTitle().isPresent() || getRichTextTitle().isPresent(), + "container block must have either title or rich_text_title" + ); + getTitle() + .ifPresent(title -> { + Preconditions.checkState( + title.getType() == TextType.PLAIN_TEXT, + "title must use plain_text formatting" + ); + Preconditions.checkState( + title.getText().length() <= + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit(), + "title cannot exceed %s characters", + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit() + ); + }); + getSubtitle() + .ifPresent(subtitle -> + Preconditions.checkState( + subtitle.getText().length() <= + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit(), + "subtitle cannot exceed %s characters", + BlockElementLengthLimits.MAX_CARD_TITLE_LENGTH.getLimit() + ) + ); + Preconditions.checkState( + getChildBlocks().size() <= + BlockElementLengthLimits.MAX_CONTAINER_CHILD_BLOCKS.getLimit(), + "child_blocks cannot exceed %s blocks", + BlockElementLengthLimits.MAX_CONTAINER_CHILD_BLOCKS.getLimit() + ); + Preconditions.checkState( + !isDefaultCollapsed().orElse(false) || isCollapsible().orElse(false), + "default_collapsed requires is_collapsible to be true" + ); + Preconditions.checkState( + !isCollapsible().orElse(false) || !getHasHeaderDivider().orElse(false), + "has_header_divider does not apply to collapsible blocks" + ); + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerBlockWidth.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerBlockWidth.java new file mode 100644 index 00000000..90233a9c --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerBlockWidth.java @@ -0,0 +1,21 @@ +package com.hubspot.slack.client.models.blocks; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum ContainerBlockWidth { + NARROW("narrow"), + STANDARD("standard"), + WIDE("wide"), + FULL("full"); + + private final String value; + + ContainerBlockWidth(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerChildBlock.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerChildBlock.java new file mode 100644 index 00000000..977009ec --- /dev/null +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContainerChildBlock.java @@ -0,0 +1,26 @@ +package com.hubspot.slack.client.models.blocks; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.hubspot.slack.client.models.blocks.elements.richtextelements.RichTextBlock; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type", + defaultImpl = UnknownBlock.class +) +@JsonSubTypes( + { + @JsonSubTypes.Type(value = Actions.class, name = Actions.TYPE), + @JsonSubTypes.Type(value = Context.class, name = Context.TYPE), + @JsonSubTypes.Type(value = Divider.class, name = Divider.TYPE), + @JsonSubTypes.Type(value = File.class, name = File.TYPE), + @JsonSubTypes.Type(value = Header.class, name = Header.TYPE), + @JsonSubTypes.Type(value = Image.class, name = Image.TYPE), + @JsonSubTypes.Type(value = Input.class, name = Input.TYPE), + @JsonSubTypes.Type(value = RichTextBlock.class, name = RichTextBlock.TYPE), + @JsonSubTypes.Type(value = Section.class, name = Section.TYPE), + @JsonSubTypes.Type(value = Table.class, name = Table.TYPE), + } +) +public interface ContainerChildBlock {} diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContextIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContextIF.java index eb5cc59d..304355fe 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContextIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ContextIF.java @@ -13,7 +13,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface ContextIF extends Block { +public interface ContextIF extends Block, ContainerChildBlock { String TYPE = "context"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DividerIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DividerIF.java index c67520d7..2fe0782c 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DividerIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/DividerIF.java @@ -9,7 +9,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface DividerIF extends Block { +public interface DividerIF extends Block, ContainerChildBlock { String TYPE = "divider"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/FileIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/FileIF.java index 45f04c9e..b9af344b 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/FileIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/FileIF.java @@ -9,7 +9,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface FileIF extends Block { +public interface FileIF extends Block, ContainerChildBlock { String TYPE = "file"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/HeaderIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/HeaderIF.java index 4d16aefa..3918a8e8 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/HeaderIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/HeaderIF.java @@ -13,7 +13,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface HeaderIF extends Block { +public interface HeaderIF extends Block, ContainerChildBlock { String TYPE = "header"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ImageIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ImageIF.java index f3e58b6c..45a51bc5 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ImageIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/ImageIF.java @@ -12,7 +12,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface ImageIF extends Block, ImageBlockOrText { +public interface ImageIF extends Block, ImageBlockOrText, ContainerChildBlock { String TYPE = "image"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/InputIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/InputIF.java index 25828dfb..3a5e9b39 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/InputIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/InputIF.java @@ -14,7 +14,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface InputIF extends Block { +public interface InputIF extends Block, ContainerChildBlock { String TYPE = "input"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/SectionIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/SectionIF.java index 27fa29de..eed59394 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/SectionIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/SectionIF.java @@ -18,7 +18,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface SectionIF extends Block { +public interface SectionIF extends Block, ContainerChildBlock { String TYPE = "section"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java index 5f2dcaf5..da613c6e 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/TableIF.java @@ -16,7 +16,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface TableIF extends Block { +public interface TableIF extends Block, ContainerChildBlock { String TYPE = "table"; @Override diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/UnknownBlock.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/UnknownBlock.java index 6bc36975..c06bff3c 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/UnknownBlock.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/UnknownBlock.java @@ -2,7 +2,7 @@ import java.util.Optional; -public class UnknownBlock implements Block { +public class UnknownBlock implements Block, ContainerChildBlock { public static final String TYPE = "unknown"; diff --git a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextBlockIF.java b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextBlockIF.java index eb46b512..d3a2bba1 100644 --- a/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextBlockIF.java +++ b/slack-base/src/main/java/com/hubspot/slack/client/models/blocks/elements/richtextelements/RichTextBlockIF.java @@ -6,6 +6,7 @@ import com.google.common.collect.ImmutableList; import com.hubspot.immutables.style.HubSpotStyle; import com.hubspot.slack.client.models.blocks.Block; +import com.hubspot.slack.client.models.blocks.ContainerChildBlock; import org.immutables.value.Value; import org.immutables.value.Value.Immutable; @@ -16,7 +17,7 @@ @Immutable @HubSpotStyle @JsonNaming(SnakeCaseStrategy.class) -public interface RichTextBlockIF extends Block { +public interface RichTextBlockIF extends Block, ContainerChildBlock { String TYPE = "rich_text"; @Override diff --git a/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/ContainerBlockTest.java b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/ContainerBlockTest.java new file mode 100644 index 00000000..e7af29d1 --- /dev/null +++ b/slack-base/src/test/java/com/hubspot/slack/client/models/blocks/ContainerBlockTest.java @@ -0,0 +1,203 @@ +package com.hubspot.slack.client.models.blocks; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.hubspot.slack.client.jackson.ObjectMapperUtils; +import com.hubspot.slack.client.models.JsonLoader; +import com.hubspot.slack.client.models.blocks.objects.Text; +import com.hubspot.slack.client.models.blocks.objects.TextType; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ContainerBlockTest { + + private static final ObjectMapper MAPPER = ObjectMapperUtils.mapper(); + private static final int FULL_BLOCK_INDEX = 0; + private static final int MINIMAL_BLOCK_INDEX = 1; + private static final int RICH_TEXT_TITLE_INDEX = 2; + private static final int HEADER_DIVIDER_INDEX = 3; + private static ContainerBlock[] blocks; + + @BeforeClass + public static void loadBlocks() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("container_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + blocks = new ContainerBlock[root.size()]; + for (int i = 0; i < root.size(); i++) { + blocks[i] = MAPPER.treeToValue(root.get(i), ContainerBlock.class); + } + } + + @Test + public void itDeserializesAsCorrectBlockType() throws IOException { + String rawJson = JsonLoader.loadJsonFromFile("container_block.json"); + JsonNode root = MAPPER.readTree(rawJson); + Block block = MAPPER.treeToValue(root.get(0), Block.class); + assertThat(block).isInstanceOf(ContainerBlock.class); + } + + @Test + public void itDeserializesAllFields() { + ContainerBlock block = blocks[FULL_BLOCK_INDEX]; + assertThat(block.getBlockId()).hasValue("block123"); + assertThat(block.getTitle()).isPresent(); + assertThat(block.getTitle().get().getText()).isEqualTo("My Container"); + assertThat(block.getTitle().get().getType()).isEqualTo(TextType.PLAIN_TEXT); + assertThat(block.getSubtitle()).isPresent(); + assertThat(block.getSubtitle().get().getText()).isEqualTo("A subtitle here"); + assertThat(block.getIcon()).isPresent(); + assertThat(block.getIcon().get().getImageUrl()) + .isEqualTo("https://example.com/icon.png"); + assertThat(block.getWidth()).hasValue(ContainerBlockWidth.WIDE); + assertThat(block.isCollapsible()).hasValue(true); + assertThat(block.isDefaultCollapsed()).hasValue(false); + assertThat(block.getHasHeaderDivider()).isEmpty(); + assertThat(block.getChildBlocks()).hasSize(2); + assertThat(block.getChildBlocks().get(0)).isInstanceOf(Section.class); + assertThat(block.getChildBlocks().get(1)).isInstanceOf(Divider.class); + } + + @Test + public void itDeserializesMinimalBlock() { + ContainerBlock block = blocks[MINIMAL_BLOCK_INDEX]; + assertThat(block.getTitle()).isPresent(); + assertThat(block.getTitle().get().getText()).isEqualTo("Minimal Container"); + assertThat(block.getChildBlocks()).hasSize(1); + assertThat(block.getRichTextTitle()).isEmpty(); + assertThat(block.getSubtitle()).isEmpty(); + assertThat(block.getIcon()).isEmpty(); + assertThat(block.getWidth()).isEmpty(); + assertThat(block.isCollapsible()).isEmpty(); + assertThat(block.isDefaultCollapsed()).isEmpty(); + assertThat(block.getHasHeaderDivider()).isEmpty(); + assertThat(block.getBlockId()).isEmpty(); + } + + @Test + public void itDeserializesHasHeaderDivider() { + ContainerBlock block = blocks[HEADER_DIVIDER_INDEX]; + assertThat(block.getHasHeaderDivider()).hasValue(true); + assertThat(block.isCollapsible()).isEmpty(); + } + + @Test + public void itDeserializesRichTextTitle() { + ContainerBlock block = blocks[RICH_TEXT_TITLE_INDEX]; + assertThat(block.getTitle()).isEmpty(); + assertThat(block.getRichTextTitle()).isPresent(); + } + + @Test + public void itSerializesAndDeserializes() throws IOException { + ContainerBlock original = ContainerBlock + .builder() + .setTitle(Text.of(TextType.PLAIN_TEXT, "Test")) + .addChildBlocks(Divider.builder().build()) + .build(); + String serialized = MAPPER.writeValueAsString(original); + ContainerBlock deserialized = MAPPER.readValue(serialized, ContainerBlock.class); + assertThat(deserialized).isEqualTo(original); + } + + @Test + public void itFailsWhenNeitherTitleNorRichTextTitlePresent() { + assertThatThrownBy(() -> + ContainerBlock.builder().addChildBlocks(Divider.builder().build()).build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("must have either title or rich_text_title"); + } + + @Test + public void itFailsWhenTitleTypeIsNotPlainText() { + assertThatThrownBy(() -> + ContainerBlock + .builder() + .setTitle(Text.of(TextType.MARKDOWN, "Title")) + .addChildBlocks(Divider.builder().build()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("plain_text formatting"); + } + + @Test + public void itFailsWhenTitleExceedsMaxLength() { + String longTitle = "a".repeat(151); + assertThatThrownBy(() -> + ContainerBlock + .builder() + .setTitle(Text.of(TextType.PLAIN_TEXT, longTitle)) + .addChildBlocks(Divider.builder().build()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("title cannot exceed"); + } + + @Test + public void itFailsWhenSubtitleExceedsMaxLength() { + String longSubtitle = "a".repeat(151); + assertThatThrownBy(() -> + ContainerBlock + .builder() + .setTitle(Text.of(TextType.PLAIN_TEXT, "Title")) + .setSubtitle(Text.of(TextType.PLAIN_TEXT, longSubtitle)) + .addChildBlocks(Divider.builder().build()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("subtitle cannot exceed"); + } + + @Test + public void itFailsWhenChildBlocksExceedMaxCount() { + ImmutableList.Builder childBlocks = ImmutableList.builder(); + for (int i = 0; i < 11; i++) { + childBlocks.add(Divider.builder().build()); + } + assertThatThrownBy(() -> + ContainerBlock + .builder() + .setTitle(Text.of(TextType.PLAIN_TEXT, "Title")) + .setChildBlocks(childBlocks.build()) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("child_blocks cannot exceed"); + } + + @Test + public void itFailsWhenDefaultCollapsedWithoutIsCollapsible() { + assertThatThrownBy(() -> + ContainerBlock + .builder() + .setTitle(Text.of(TextType.PLAIN_TEXT, "Title")) + .addChildBlocks(Divider.builder().build()) + .setDefaultCollapsed(true) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("default_collapsed requires is_collapsible"); + } + + @Test + public void itFailsWhenHasHeaderDividerOnCollapsibleBlock() { + assertThatThrownBy(() -> + ContainerBlock + .builder() + .setTitle(Text.of(TextType.PLAIN_TEXT, "Title")) + .addChildBlocks(Divider.builder().build()) + .setCollapsible(true) + .setHasHeaderDivider(true) + .build() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("has_header_divider does not apply to collapsible blocks"); + } +} diff --git a/slack-base/src/test/resources/container_block.json b/slack-base/src/test/resources/container_block.json new file mode 100644 index 00000000..2beadd34 --- /dev/null +++ b/slack-base/src/test/resources/container_block.json @@ -0,0 +1,81 @@ +[ + { + "type": "container", + "block_id": "block123", + "title": { + "type": "plain_text", + "text": "My Container" + }, + "subtitle": { + "type": "mrkdwn", + "text": "A subtitle here" + }, + "icon": { + "type": "image", + "image_url": "https://example.com/icon.png", + "alt_text": "icon" + }, + "width": "wide", + "is_collapsible": true, + "default_collapsed": false, + "child_blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Hello world" + } + }, + { + "type": "divider" + } + ] + }, + { + "type": "container", + "title": { + "type": "plain_text", + "text": "Minimal Container" + }, + "child_blocks": [ + { + "type": "divider" + } + ] + }, + { + "type": "container", + "rich_text_title": { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "Rich text title" + } + ] + } + ] + }, + "child_blocks": [ + { + "type": "divider" + } + ] + }, + { + "type": "container", + "title": { + "type": "plain_text", + "text": "Non-collapsible Container" + }, + "has_header_divider": true, + "child_blocks": [ + { + "type": "divider" + } + ] + } +]