From ff0a3dadf202f9ed6ddcf8c7ee77a0e15d398916 Mon Sep 17 00:00:00 2001 From: Yejun Su Date: Sat, 4 Apr 2026 22:52:23 +0800 Subject: [PATCH] fix(adapter-slack): replace empty table cells with single space Slack's Block Kit API rejects cells with empty text fields. Fall back to a single space in both mdastTableToSlackBlock (markdown.ts) and convertTableToBlocks (cards.ts) to satisfy the API constraint. --- packages/adapter-slack/src/cards.test.ts | 27 +++++++++++++++++++++ packages/adapter-slack/src/cards.ts | 4 +-- packages/adapter-slack/src/markdown.test.ts | 17 +++++++++++++ packages/adapter-slack/src/markdown.ts | 2 +- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/adapter-slack/src/cards.test.ts b/packages/adapter-slack/src/cards.test.ts index 7ef435cb..e02eedb5 100644 --- a/packages/adapter-slack/src/cards.test.ts +++ b/packages/adapter-slack/src/cards.test.ts @@ -797,4 +797,31 @@ describe("cardToBlockKit with CardLink", () => { expect(blocks[1].type).toBe("section"); expect(blocks[1].text.text).toContain("```"); }); + + it("replaces empty table cells with a space to satisfy Slack API", () => { + const card = Card({ + children: [ + Table({ + headers: ["Kind", ""], + rows: [ + ["FORM", "Form Submission"], + ["and more...", ""], + ], + }), + ], + }); + + const blocks = cardToBlockKit(card); + const tableBlock = blocks[0]; + expect(tableBlock.type).toBe("table"); + // Every cell must have non-empty text (Slack rejects empty strings) + for (const row of tableBlock.rows) { + for (const cell of row) { + expect(cell.text.length).toBeGreaterThan(0); + } + } + // Empty cells become a space + expect(tableBlock.rows[0][1].text).toBe(" "); // empty header + expect(tableBlock.rows[2][1].text).toBe(" "); // empty data cell + }); }); diff --git a/packages/adapter-slack/src/cards.ts b/packages/adapter-slack/src/cards.ts index 4b39120d..07380658 100644 --- a/packages/adapter-slack/src/cards.ts +++ b/packages/adapter-slack/src/cards.ts @@ -400,13 +400,13 @@ function convertTableToBlocks( // First row is headers, subsequent rows are data const headerRow = element.headers.map((header) => ({ type: "raw_text" as const, - text: convertEmoji(header), + text: convertEmoji(header) || " ", })); const dataRows = element.rows.map((row) => row.map((cell) => ({ type: "raw_text" as const, - text: convertEmoji(cell), + text: convertEmoji(cell) || " ", })) ); diff --git a/packages/adapter-slack/src/markdown.test.ts b/packages/adapter-slack/src/markdown.test.ts index 0911a9df..1a03f3ea 100644 --- a/packages/adapter-slack/src/markdown.test.ts +++ b/packages/adapter-slack/src/markdown.test.ts @@ -214,6 +214,23 @@ describe("SlackMarkdownConverter", () => { expect(blocks?.[1].type).toBe("section"); expect(blocks?.[1].text.text).toContain("```"); }); + + it("should replace empty table cells with a space to satisfy Slack API", () => { + const ast = converter.toAst( + "| Kind | Label |\n|------|-------|\n| FORM | Form Submission |\n| and more... | |" + ); + const blocks = converter.toBlocksWithTable(ast); + const tableBlock = blocks?.[0]; + expect(tableBlock?.type).toBe("table"); + // Every cell must have non-empty text (Slack rejects empty strings) + for (const row of tableBlock?.rows ?? []) { + for (const cell of row) { + expect(cell.text.length).toBeGreaterThan(0); + } + } + // The empty cell should be a space + expect(tableBlock?.rows[2][1].text).toBe(" "); + }); }); describe("nested lists", () => { diff --git a/packages/adapter-slack/src/markdown.ts b/packages/adapter-slack/src/markdown.ts index 3b2eb740..550848ab 100644 --- a/packages/adapter-slack/src/markdown.ts +++ b/packages/adapter-slack/src/markdown.ts @@ -254,7 +254,7 @@ function mdastTableToSlackBlock( for (const row of node.children) { const cells = getNodeChildren(row).map((cell) => ({ type: "raw_text" as const, - text: getNodeChildren(cell).map(cellConverter).join(""), + text: getNodeChildren(cell).map(cellConverter).join("") || " ", })); rows.push(cells); }