Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/teams-dialog-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@chat-adapter/teams": minor
"chat": minor
---

Add Teams dialog (task module) support with `actionType: "modal"` on buttons and `onOpenModal` webhook hook
9 changes: 5 additions & 4 deletions examples/nextjs-chat/src/lib/bot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ bot.onNewMention(async (thread, message) => {
<Button id="ephemeral">Ephemeral response</Button>
<Button id="info">Show Info</Button>
<Button id="choose_plan">Choose Plan</Button>
<Button id="feedback">Send Feedback</Button>
<Button actionType="modal" id="feedback">
Send Feedback
</Button>
<Button id="messages">Fetch Messages</Button>
<Button id="channel-post">Channel Post</Button>
<Button id="show-table">Show Table</Button>
<Button id="report" value="bug">
<Button actionType="modal" id="report" value="bug">
Report Bug
</Button>
<LinkButton url="https://vercel.com">Open Link</LinkButton>
Expand Down Expand Up @@ -211,7 +213,7 @@ bot.onAction("ephemeral", async (event) => {
</Text>
<Text>Try opening a modal from this ephemeral:</Text>
<Actions>
<Button id="ephemeral_modal" style="primary">
<Button actionType="modal" id="ephemeral_modal" style="primary">
Open Modal
</Button>
</Actions>
Expand All @@ -237,7 +239,6 @@ bot.onAction("ephemeral_modal", async (event) => {
);
});

// @ts-expect-error async void handler vs ModalSubmitHandler return type
bot.onModalSubmit("ephemeral_modal_form", async (event) => {
await event.relatedMessage?.edit(
<Card title={`${emoji.check} Submitted!`}>
Expand Down
1 change: 1 addition & 0 deletions packages/adapter-teams/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@chat-adapter/shared": "workspace:*",
"@microsoft/teams.api": "^2.0.6",
"@microsoft/teams.apps": "^2.0.6",
"@microsoft/teams.cards": "^2.0.6",
"@microsoft/teams.graph-endpoints": "^2.0.6",
"chat": "workspace:*"
},
Expand Down
6 changes: 4 additions & 2 deletions packages/adapter-teams/src/bridge-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ export class BridgeHttpAdapter implements IHttpServerAdapter {
try {
const serverResponse = await this.handler({ body: parsedBody, headers });

const hasBody =
serverResponse.body !== undefined && serverResponse.body !== null;
return new Response(
serverResponse.body ? JSON.stringify(serverResponse.body) : "{}",
hasBody ? JSON.stringify(serverResponse.body) : null,
{
status: serverResponse.status,
headers: { "Content-Type": "application/json" },
headers: hasBody ? { "Content-Type": "application/json" } : undefined,
}
);
} catch (error) {
Expand Down
63 changes: 43 additions & 20 deletions packages/adapter-teams/src/cards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ describe("cardToAdaptiveCard", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.body).toHaveLength(1);
expect(adaptive.body[0]).toEqual({
expect(adaptive.body[0]).toMatchObject({
type: "TextBlock",
text: "Welcome Message",
weight: "bolder",
size: "large",
weight: "Bolder",
size: "Large",
wrap: true,
});
});
Expand All @@ -49,7 +49,7 @@ describe("cardToAdaptiveCard", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.body).toHaveLength(2);
expect(adaptive.body[1]).toEqual({
expect(adaptive.body[1]).toMatchObject({
type: "TextBlock",
text: "Your package is on its way",
isSubtle: true,
Expand All @@ -65,10 +65,10 @@ describe("cardToAdaptiveCard", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.body).toHaveLength(2);
expect(adaptive.body[1]).toEqual({
expect(adaptive.body[1]).toMatchObject({
type: "Image",
url: "https://example.com/product.png",
size: "stretch",
size: "Stretch",
});
});

Expand All @@ -84,20 +84,20 @@ describe("cardToAdaptiveCard", () => {

expect(adaptive.body).toHaveLength(3);

expect(adaptive.body[0]).toEqual({
expect(adaptive.body[0]).toMatchObject({
type: "TextBlock",
text: "Regular text",
wrap: true,
});

expect(adaptive.body[1]).toEqual({
expect(adaptive.body[1]).toMatchObject({
type: "TextBlock",
text: "Bold text",
wrap: true,
weight: "bolder",
weight: "Bolder",
});

expect(adaptive.body[2]).toEqual({
expect(adaptive.body[2]).toMatchObject({
type: "TextBlock",
text: "Muted text",
wrap: true,
Expand All @@ -114,11 +114,11 @@ describe("cardToAdaptiveCard", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.body).toHaveLength(1);
expect(adaptive.body[0]).toEqual({
expect(adaptive.body[0]).toMatchObject({
type: "Image",
url: "https://example.com/img.png",
altText: "My image",
size: "auto",
size: "Auto",
});
});

Expand All @@ -129,7 +129,7 @@ describe("cardToAdaptiveCard", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.body).toHaveLength(1);
expect(adaptive.body[0]).toEqual({
expect(adaptive.body[0]).toMatchObject({
type: "Container",
separator: true,
items: [],
Expand Down Expand Up @@ -157,21 +157,21 @@ describe("cardToAdaptiveCard", () => {
expect(adaptive.body).toHaveLength(0);
expect(adaptive.actions).toHaveLength(3);

expect(adaptive.actions?.[0]).toEqual({
expect(adaptive.actions?.[0]).toMatchObject({
type: "Action.Submit",
title: "Approve",
data: { actionId: "approve", value: undefined },
style: "positive",
});

expect(adaptive.actions?.[1]).toEqual({
expect(adaptive.actions?.[1]).toMatchObject({
type: "Action.Submit",
title: "Reject",
data: { actionId: "reject", value: "data-123" },
style: "destructive",
});

expect(adaptive.actions?.[2]).toEqual({
expect(adaptive.actions?.[2]).toMatchObject({
type: "Action.Submit",
title: "Skip",
data: { actionId: "skip", value: undefined },
Expand All @@ -193,7 +193,7 @@ describe("cardToAdaptiveCard", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.actions).toHaveLength(1);
expect(adaptive.actions?.[0]).toEqual({
expect(adaptive.actions?.[0]).toMatchObject({
type: "Action.OpenUrl",
title: "View Docs",
url: "https://example.com/docs",
Expand All @@ -213,7 +213,7 @@ describe("cardToAdaptiveCard", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.body).toHaveLength(1);
expect(adaptive.body[0]).toEqual({
expect(adaptive.body[0]).toMatchObject({
type: "FactSet",
facts: [
{ title: "Status", value: "Active" },
Expand All @@ -230,7 +230,7 @@ describe("cardToAdaptiveCard", () => {

expect(adaptive.body).toHaveLength(1);
expect(adaptive.body[0].type).toBe("Container");
expect(adaptive.body[0].items).toHaveLength(1);
expect((adaptive.body[0] as { items: unknown[] }).items).toHaveLength(1);
});

it("converts a complete card", () => {
Expand Down Expand Up @@ -300,6 +300,29 @@ describe("cardToFallbackText", () => {
});
});

describe("cardToAdaptiveCard with modal buttons", () => {
it("adds msteams task/fetch hint for actionType modal", () => {
const card = Card({
children: [
Actions([
Button({ id: "open-dialog", label: "Open", actionType: "modal" }),
]),
],
});
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.actions).toHaveLength(1);
expect(adaptive.actions?.[0]).toMatchObject({
type: "Action.Submit",
title: "Open",
data: {
actionId: "open-dialog",
msteams: { type: "task/fetch" },
},
});
});
});

describe("cardToAdaptiveCard with CardLink", () => {
it("converts CardLink to a TextBlock with markdown link", () => {
const card = Card({
Expand All @@ -309,7 +332,7 @@ describe("cardToAdaptiveCard with CardLink", () => {
const adaptive = cardToAdaptiveCard(card);

expect(adaptive.body).toHaveLength(1);
expect(adaptive.body[0]).toEqual({
expect(adaptive.body[0]).toMatchObject({
type: "TextBlock",
text: "[Click here](https://example.com)",
wrap: true,
Expand Down
Loading
Loading