Skip to content
Closed
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 @@ -129,4 +129,14 @@ public static ShareShortcutBlock shareShortcut() {
return ShareShortcutBlock.builder().build();
}

// CardBlock
public static CardBlock card(ModelConfigurator<CardBlock.CardBlockBuilder> configurator) {
return configurator.configure(CardBlock.builder()).build();
}

// CarouselBlock
public static CarouselBlock carousel(ModelConfigurator<CarouselBlock.CarouselBlockBuilder> configurator) {
return configurator.configure(CarouselBlock.builder()).build();
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<CardBlock> elements = new ArrayList<>();

private String blockId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ private Class<? extends LayoutBlock> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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));
}
}