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..0ebf6469d 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,14 @@ public static ShareShortcutBlock shareShortcut() { return ShareShortcutBlock.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(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/CardBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/CardBlock.java new file mode 100644 index 000000000..7db66235e --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/CardBlock.java @@ -0,0 +1,67 @@ +package com.slack.api.model.block; + +import com.slack.api.model.block.element.ImageElement; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A card is a layout block used to display a compact, structured summary of content. It can be used on its own + * or grouped together inside a {@link CarouselBlock carousel}. At least one of {@code heroImage}, {@code title}, + * {@code actions}, or {@code body} must be provided. + * + * https://docs.slack.dev/reference/block-kit/blocks/card-block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CardBlock implements LayoutBlock { + public static final String TYPE = "card"; + private final String type = TYPE; + + /** + * Link to the top image used on the card. Max length of 3000 characters. + */ + private ImageElement heroImage; + + /** + * Small image displayed beside the title and subtitle. Max length of 3000 characters. + * Mutually exclusive with {@code slackIcon}. + */ + private ImageElement icon; + + /** + * The name of a built-in Slack icon to display beside the title and subtitle. + * Mutually exclusive with {@code icon}. + */ + private String slackIcon; + + /** + * The title of the card. 150 character maximum. + */ + private String title; + + /** + * The subtitle of the card. 150 character maximum. + */ + private String subtitle; + + /** + * The main text of the card. 200 character maximum. + */ + private String body; + + /** + * Secondary text displayed beneath the body. 200 character maximum. + */ + private String subtext; + + /** + * An {@link ActionsBlock actions block} containing a maximum of 3 buttons. + */ + private ActionsBlock actions; + + private String blockId; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java new file mode 100644 index 000000000..335c53c79 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/CarouselBlock.java @@ -0,0 +1,32 @@ +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; + +/** + * A carousel is a layout block used to display a horizontally scrollable collection of + * {@link CardBlock cards}. It must contain between 1 and 10 cards. + * + * https://docs.slack.dev/reference/block-kit/blocks/carousel-block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CarouselBlock implements LayoutBlock { + public static final String TYPE = "carousel"; + private final String type = TYPE; + + /** + * An array of {@link CardBlock card} blocks. Must contain between 1 and 10 cards. + */ + @Builder.Default + private List elements = new ArrayList<>(); + + private String blockId; +} 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..1f006860a 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,10 @@ private Class getLayoutClassInstance(String typeName) { return RichTextBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; + case CardBlock.TYPE: + return CardBlock.class; + case CarouselBlock.TYPE: + return CarouselBlock.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 1fc12d58d..a7d013154 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 @@ -15,8 +15,10 @@ import static com.slack.api.model.block.Blocks.*; import static com.slack.api.model.block.composition.BlockCompositions.asSectionFields; import static com.slack.api.model.block.composition.BlockCompositions.plainText; +import static com.slack.api.model.block.element.BlockElements.button; import static com.slack.api.model.block.element.BlockElements.checkboxes; import static com.slack.api.model.block.element.BlockElements.conversationsSelect; +import static com.slack.api.model.block.element.BlockElements.image; import static com.slack.api.model.view.Views.view; import static com.slack.api.model.view.Views.viewSubmit; import static org.hamcrest.CoreMatchers.*; @@ -1940,4 +1942,99 @@ public void parseRichTextElements() { RichTextSectionElement section = (RichTextSectionElement) ((RichTextBlock) message.getBlocks().get(0)).getElements().get(0); assertThat(section.getElements().size(), is(10)); } + + @Test + public void parseCarouselBlock() { + // https://docs.slack.dev/reference/block-kit/blocks/carousel-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"carousel\",\n" + + " \"block_id\": \"carousel-1\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"card\",\n" + + " \"block_id\": \"card-1\",\n" + + " \"title\": \"First card\",\n" + + " \"subtitle\": \"Subtitle one\",\n" + + " \"body\": \"Body of the first card.\",\n" + + " \"hero_image\": {\n" + + " \"type\": \"image\",\n" + + " \"image_url\": \"https://example.com/hero.png\",\n" + + " \"alt_text\": \"hero\"\n" + + " },\n" + + " \"icon\": {\n" + + " \"type\": \"image\",\n" + + " \"image_url\": \"https://example.com/icon.png\",\n" + + " \"alt_text\": \"icon\"\n" + + " },\n" + + " \"actions\": {\n" + + " \"type\": \"actions\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"button\",\n" + + " \"action_id\": \"open\",\n" + + " \"text\": {\"type\": \"plain_text\", \"text\": \"Open\"}\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\": \"card\",\n" + + " \"title\": \"Second card\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + Message message = GsonFactory.createSnakeCase().fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + + CarouselBlock carousel = (CarouselBlock) message.getBlocks().get(0); + assertThat(carousel.getType(), is("carousel")); + assertThat(carousel.getBlockId(), is("carousel-1")); + assertThat(carousel.getElements().size(), is(2)); + + CardBlock firstCard = carousel.getElements().get(0); + assertThat(firstCard.getType(), is("card")); + assertThat(firstCard.getBlockId(), is("card-1")); + assertThat(firstCard.getTitle(), is("First card")); + assertThat(firstCard.getSubtitle(), is("Subtitle one")); + assertThat(firstCard.getBody(), is("Body of the first card.")); + assertThat(firstCard.getHeroImage().getImageUrl(), is("https://example.com/hero.png")); + assertThat(firstCard.getIcon().getImageUrl(), is("https://example.com/icon.png")); + assertThat(firstCard.getActions().getElements().size(), is(1)); + + CardBlock secondCard = carousel.getElements().get(1); + assertThat(secondCard.getTitle(), is("Second card")); + assertThat(secondCard.getBlockId(), is(nullValue())); + } + + @Test + public void buildCarouselBlock() { + CarouselBlock block = carousel(c -> c + .blockId("carousel-1") + .elements(Arrays.asList( + card(card -> card + .blockId("card-1") + .title("Promo") + .body("Check this out") + .heroImage(image(i -> i.imageUrl("https://example.com/hero.png").altText("hero"))) + .actions(actions(Arrays.asList( + button(b -> b.actionId("open").text(plainText("Open"))))))), + card(card -> card.title("Second"))))); + assertThat(block.getType(), is("carousel")); + assertThat(block.getElements().size(), is(2)); + + Gson gson = GsonFactory.createSnakeCase(); + String output = gson.toJson(block); + CarouselBlock parsed = gson.fromJson(output, CarouselBlock.class); + assertThat(parsed.getBlockId(), is("carousel-1")); + assertThat(parsed.getElements().size(), is(2)); + assertThat(parsed.getElements().get(0).getType(), is("card")); + assertThat(parsed.getElements().get(0).getTitle(), is("Promo")); + assertThat(parsed.getElements().get(0).getHeroImage().getImageUrl(), is("https://example.com/hero.png")); + assertThat(parsed.getElements().get(0).getActions().getElements().size(), is(1)); + } }