From 502310634a86aad4eaf583fb7e45d29cdefff5d9 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 28 Aug 2026 16:21:39 -0400 Subject: [PATCH 1/4] feat(model): add TaskCardBlock and UrlSourceElement --- .../com/slack/api/model/block/Blocks.java | 6 ++ .../slack/api/model/block/TaskCardBlock.java | 32 ++++++++ .../model/block/element/UrlSourceElement.java | 21 +++++ .../api/util/json/GsonLayoutBlockFactory.java | 2 + .../api/model/block/BlockKitTest.java | 76 +++++++++++++++++++ .../api/model/block/BlocksTest.java | 5 ++ 6 files changed, 142 insertions(+) create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/TaskCardBlock.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/element/UrlSourceElement.java 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 9abe88458..01352cafa 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 @@ -147,4 +147,10 @@ public static CarouselBlock carousel(ModelConfigurator configurator) { + return configurator.configure(TaskCardBlock.builder()).build(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/TaskCardBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/TaskCardBlock.java new file mode 100644 index 000000000..5400d2f4f --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/TaskCardBlock.java @@ -0,0 +1,32 @@ +package com.slack.api.model.block; + +import com.slack.api.model.block.composition.SlackIconObject; +import com.slack.api.model.block.element.UrlSourceElement; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * https://docs.slack.dev/reference/block-kit/blocks/task-card-block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TaskCardBlock implements LayoutBlock { + public static final String TYPE = "task_card"; + private final String type = TYPE; + + private String taskId; + private String title; + private String status; + private RichTextBlock details; + private RichTextBlock output; + private List sources; + private String blockId; + private SlackIconObject icon; + private Boolean hideTitle; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/element/UrlSourceElement.java b/slack-api-model/src/main/java/com/slack/api/model/block/element/UrlSourceElement.java new file mode 100644 index 000000000..5e60c9799 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/element/UrlSourceElement.java @@ -0,0 +1,21 @@ +package com.slack.api.model.block.element; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * https://docs.slack.dev/reference/block-kit/block-elements/url-source-element + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UrlSourceElement { + public static final String TYPE = "url"; + private final String type = TYPE; + + private String url; + private String text; +} 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 d97b36d28..2917d0823 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 @@ -67,6 +67,8 @@ private Class getLayoutClassInstance(String typeName) { return CardBlock.class; case CarouselBlock.TYPE: return CarouselBlock.class; + case TaskCardBlock.TYPE: + return TaskCardBlock.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 f398c781f..60d7f2bf0 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 @@ -2202,4 +2202,80 @@ public void buildCarouselBlock() { assertThat(parsedCarousel.getElements().get(0).getBlockId(), is("card1")); assertThat(parsedCarousel.getElements().get(0).getActions().size(), is(1)); } + + @Test + public void parseTaskCardBlock() { + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"task_card\",\n" + + " \"task_id\": \"task_1\",\n" + + " \"title\": \"Fetching weather data\",\n" + + " \"status\": \"pending\",\n" + + " \"output\": {\n" + + " \"type\": \"rich_text\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"rich_text_section\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"text\",\n" + + " \"text\": \"Found weather data for Chicago from 2 sources\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"sources\": [\n" + + " {\n" + + " \"type\": \"url\",\n" + + " \"url\": \"https://weather.com/\",\n" + + " \"text\": \"weather.com\"\n" + + " },\n" + + " {\n" + + " \"type\": \"url\",\n" + + " \"url\": \"https://www.accuweather.com/\",\n" + + " \"text\": \"accuweather.com\"\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)); + + TaskCardBlock taskCard = (TaskCardBlock) message.getBlocks().get(0); + assertThat(taskCard.getType(), is("task_card")); + assertThat(taskCard.getTaskId(), is("task_1")); + assertThat(taskCard.getTitle(), is("Fetching weather data")); + assertThat(taskCard.getStatus(), is("pending")); + + assertThat(taskCard.getOutput(), is(notNullValue())); + assertThat(taskCard.getOutput().getType(), is("rich_text")); + + assertThat(taskCard.getSources().size(), is(2)); + assertThat(taskCard.getSources().get(0).getType(), is("url")); + assertThat(taskCard.getSources().get(0).getUrl(), is("https://weather.com/")); + assertThat(taskCard.getSources().get(0).getText(), is("weather.com")); + assertThat(taskCard.getSources().get(1).getUrl(), is("https://www.accuweather.com/")); + assertThat(taskCard.getSources().get(1).getText(), is("accuweather.com")); + } + + @Test + public void buildTaskCardBlock() { + TaskCardBlock taskCard = Blocks.taskCard(t -> t + .taskId("task_42") + .title("Processing data") + .status("in_progress") + .sources(Arrays.asList( + UrlSourceElement.builder().url("https://example.com").text("example").build()))); + assertThat(taskCard, is(notNullValue())); + assertThat(taskCard.getType(), is("task_card")); + assertThat(taskCard.getTaskId(), is("task_42")); + assertThat(taskCard.getTitle(), is("Processing data")); + assertThat(taskCard.getStatus(), is("in_progress")); + assertThat(taskCard.getSources().size(), is(1)); + } } diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java index 332ee593b..4fe2cd8be 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java @@ -117,6 +117,11 @@ public void testCarousel() { ))), is(notNullValue())); } + @Test + public void testTaskCard() { + assertThat(taskCard(t -> t.taskId("task_1").title("Do thing").status("complete")), is(notNullValue())); + } + @Test public void testImage() { assertThat(Blocks.image(i -> i.blockId("block-id").imageUrl("https://www.example.com/")), is(notNullValue())); From 83e4ec2cc48cf053a58a6a44b7d46d36b2591737 Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:43:10 -0400 Subject: [PATCH 2/4] Update slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java Co-authored-by: Eden Zimbelman --- .../test/java/test_locally/api/model/block/BlockKitTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 60d7f2bf0..a0d9710fd 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 @@ -2211,7 +2211,7 @@ public void parseTaskCardBlock() { " \"type\": \"task_card\",\n" + " \"task_id\": \"task_1\",\n" + " \"title\": \"Fetching weather data\",\n" + - " \"status\": \"pending\",\n" + + " \"status\": \"in_progress\",\n" + " \"output\": {\n" + " \"type\": \"rich_text\",\n" + " \"elements\": [\n" + From d089233ccd9485040d4f3406ffd9a4f48126b56b Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 1 Sep 2026 14:50:48 -0400 Subject: [PATCH 3/4] fix: update assertion to match in_progress status in test --- .../test/java/test_locally/api/model/block/BlockKitTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a0d9710fd..963407c1b 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 @@ -2250,7 +2250,7 @@ public void parseTaskCardBlock() { assertThat(taskCard.getType(), is("task_card")); assertThat(taskCard.getTaskId(), is("task_1")); assertThat(taskCard.getTitle(), is("Fetching weather data")); - assertThat(taskCard.getStatus(), is("pending")); + assertThat(taskCard.getStatus(), is("in_progress")); assertThat(taskCard.getOutput(), is(notNullValue())); assertThat(taskCard.getOutput().getType(), is("rich_text")); From f6aad3a2c11b2253ba50ab9131ba8c8235ffbe9d Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 1 Sep 2026 14:58:57 -0400 Subject: [PATCH 4/4] refactor: alphabetize block entries in factory and tests --- .../api/util/json/GsonLayoutBlockFactory.java | 44 +++++++++---------- .../api/model/block/BlocksTest.java | 10 ++--- 2 files changed, 27 insertions(+), 27 deletions(-) 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 2917d0823..93e2ce4ca 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 @@ -33,42 +33,42 @@ public LayoutBlock deserialize(JsonElement json, Type typeOfT, JsonDeserializati private Class getLayoutClassInstance(String typeName) { switch (typeName) { - case SectionBlock.TYPE: - return SectionBlock.class; - case DividerBlock.TYPE: - return DividerBlock.class; - case ImageBlock.TYPE: - return ImageBlock.class; - case ContextBlock.TYPE: - return ContextBlock.class; - case ContextActionsBlock.TYPE: - return ContextActionsBlock.class; - case CallBlock.TYPE: - return CallBlock.class; case ActionsBlock.TYPE: return ActionsBlock.class; + case AlertBlock.TYPE: + return AlertBlock.class; + case CallBlock.TYPE: + return CallBlock.class; + case CardBlock.TYPE: + return CardBlock.class; + case CarouselBlock.TYPE: + return CarouselBlock.class; + case ContextActionsBlock.TYPE: + return ContextActionsBlock.class; + case ContextBlock.TYPE: + return ContextBlock.class; + case DividerBlock.TYPE: + return DividerBlock.class; case FileBlock.TYPE: return FileBlock.class; - case InputBlock.TYPE: - return InputBlock.class; case HeaderBlock.TYPE: return HeaderBlock.class; + case ImageBlock.TYPE: + return ImageBlock.class; + case InputBlock.TYPE: + return InputBlock.class; case MarkdownBlock.TYPE: return MarkdownBlock.class; - case VideoBlock.TYPE: - return VideoBlock.class; case RichTextBlock.TYPE: return RichTextBlock.class; + case SectionBlock.TYPE: + return SectionBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; - case AlertBlock.TYPE: - return AlertBlock.class; - case CardBlock.TYPE: - return CardBlock.class; - case CarouselBlock.TYPE: - return CarouselBlock.class; case TaskCardBlock.TYPE: return TaskCardBlock.class; + case VideoBlock.TYPE: + return VideoBlock.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/BlocksTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java index 4fe2cd8be..790d07ef3 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlocksTest.java @@ -117,11 +117,6 @@ public void testCarousel() { ))), is(notNullValue())); } - @Test - public void testTaskCard() { - assertThat(taskCard(t -> t.taskId("task_1").title("Do thing").status("complete")), is(notNullValue())); - } - @Test public void testImage() { assertThat(Blocks.image(i -> i.blockId("block-id").imageUrl("https://www.example.com/")), is(notNullValue())); @@ -158,6 +153,11 @@ public void testRichText() { ), is(notNullValue())); } + @Test + public void testTaskCard() { + assertThat(taskCard(t -> t.taskId("task_1").title("Do thing").status("complete")), is(notNullValue())); + } + @Test public void testDateTimePicker() { assertThat(datetimePicker(h -> h.actionId("block-id").initialDateTime(12345)), is(notNullValue()));