2

I am using Microsoft teams bot with nodejs. I am rendering a carousel of adaptive cards with action on each card. My requirement is to delete an individual card out on which the action was clicked. Is it possible?

Current code looks like below. i have given a try to deleteActive but that deletes entire carousel

const {
    TurnContext,
    TeamsActivityHandler,
    CardFactory,
    AttachmentLayoutTypes,
    ActionTypes
} = require('botbuilder');

class TeamsConversationBot extends TeamsActivityHandler {
    constructor() {
        super();
        this.onMessage(async (context:any, next:any) => {
            TurnContext.removeRecipientMention(context.activity);
            console.log("context activigty at the begin is:" + JSON.stringify(context.activity))
            let msg = context.activity.text
            let action = context.activity.value

            if(msg.startsWith('lead')){
                msg = 'lead'
            }

            if(action !== undefined){
                console.log("user did some action on a card")
                msg = action.action
            }

            switch (msg) {
                case 'lead':
                        await this.lead(context)
                        break;
                case 'qualify_lead':
                        await this.qualifyLead(context)
                        break;
            }
            await next();
        });
    }


    /**
     * 
     * @param context this method does a lead qualification
     */
    async qualifyLead(context:any){
        console.log("in qualifyLead:" + JSON.stringify(context.activity))
        //await context.deleteActivity(context.activity.replyToId)

        const leadId = context.activity.value.objectId
        console.log("Lead to qualify is:" + leadId)


        await context.sendActivity('Lead is qualified')
    }


/**
    * Search contact by name
    * @param context
    * @param keyword 
*/ 
async lead(context:any){
    console.log("Start of lead with context:" + JSON.stringify(context))
    const cardArr = []
    let items = [
        {"Name": 'x', "LeadId": "1"},
        {"Name": 'a', "LeadId": "2"},
        {"Name": 'b', "LeadId": "3"},
        {"Name": 'c', "LeadId": "4"},
        {"Name": 'd', "LeadId": "5"}
    ]

     for(const item of items){
        const header =  {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": item.Name
        }



    const actions = [
        {
            "type": "Action.Submit",
            "title": "Qualify",
            "data": { "action" : "qualify_lead", "objectId" : item.LeadId }
        }
       ]


   const acard = CardFactory.adaptiveCard(
    {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.0",
        "body": [
            header,
            ''
            ],
           "actions": actions
         }
     )

    cardArr.push(acard)
    console.log("payload is::::" + JSON.stringify(acard))
        }

    const reply = {
        "attachments" : cardArr,
        "attachmentLayout" : AttachmentLayoutTypes.Carousel
    }

    await context.sendActivity(reply);
}

}

module.exports.TeamsConversationBot = TeamsConversationBot;
Moblize IT
  • 1,140
  • 2
  • 18
  • 44
  • 1
    Never tried this but I think you would need to remove the clicked value from the cardArr array using cardArr.filter or something similar. You'd have to regenerate the carousel, too. – billoverton Dec 19 '19 at 18:47
  • 1
    hmm sounds like i will need to pass the entire cards payload back do what i need to do and send it back with one less card – Moblize IT Dec 19 '19 at 19:24
  • I believe I can help, but can you fix your code sample please? `await next();` is duplicated in your `onMessage` handler, you have extra or missing closing brackets in some places, your card's body has null as one of its elements and "actions" is contained within the body, etc. How can we fix your code if we don't know if these problems are in your actual code or are just artifacts from copying and pasting your code? Make sure you format the code in your editor so that it's easy to read. (Since there are multiple people on this thread, you need to @ mention me so I'll see your reply.) – Kyle Delaney Dec 19 '19 at 19:51
  • @KyleDelaney i have re added the code by fixing issues and simplifying as much as i could. please advise – Moblize IT Dec 20 '19 at 03:14
  • @MoblizeIT, Instead of SendActivity() you can use UpdateActivity()with the updated card payload. – Subhasish Dec 20 '19 at 07:09
  • @Subhasish-MSFT do you have an example on this? i can see updateActivity can help me update the values passed around. but not the visual elements. for example in this case if i want to remove the one card out of 10 then i dont see how – Moblize IT Dec 20 '19 at 16:14
  • @MoblizeIT - Why is there an empty string in the body of your Adaptive Card? Strings aren't card elements. Also your code still isn't formatted. Are you using Visual Studio Code? https://stackoverflow.com/questions/29973357/how-do-you-format-code-in-visual-studio-code-vscode – Kyle Delaney Dec 20 '19 at 18:20
  • yes i use visual studio code – Moblize IT Dec 21 '19 at 03:04
  • 1
    @MoblizeIT - In VS Code you can use alt+shift+F to auto-format your document – Kyle Delaney Dec 23 '19 at 19:17
  • @MoblizeIT - Are you going to accept my answer? – Kyle Delaney Dec 26 '19 at 21:43

1 Answers1

2

As with this other answer, the answer will be similar to this one. I can see you're trying to use TypeScript but your code deviates very little from JavaScript so I'll just write my answer in JavaScript.

First, you'll need a way of saving state for your [carousel] so you can update the [carousel]'s activity.

this.carouselState = this.conversationState.createProperty('carouselState');

You'll want a consistent way to generate your [carousel] that you can use when you send the [carousel] initially and when you update the [carousel].

createCarousel(batchId, leads)
{
    const cardArr = [];

    let items = [
        { "Name": 'x', "LeadId": 1 },
        { "Name": 'a', "LeadId": 2 },
        { "Name": 'b', "LeadId": 3 },
        { "Name": 'c', "LeadId": 4 },
        { "Name": 'd', "LeadId": 5 }
    ];

    items = items.filter(item => leads.includes(item.LeadId));

    for (const item of items) {
        const header = {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": item.Name
        };

        const actions = [
            {
                "type": "Action.Submit",
                "title": "Qualify",
                "data": { [KEYACTION]: ACTIONQUALIFYLEAD, [KEYOBJECTID]: item.LeadId, [KEYBATCHID]: batchId }
            }
        ];

        const acard = CardFactory.adaptiveCard(
            {
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                "type": "AdaptiveCard",
                "version": "1.0",
                "body": [
                    header
                ],
                "actions": actions
            }
        );

        cardArr.push(acard);
    }

    return {
        "type": "message",
        "attachments": cardArr,
        "attachmentLayout": AttachmentLayoutTypes.Carousel
    };
}

This is similar to your code but there are some important differences. First, I'm filtering the items array to allow for fewer items, which is how you'll end up deleting cards from your carousel. Second, I'm including a "batch ID" in each action's data, which is how your bot will know which activity to update when it receives the action's payload. Also, this isn't relevant to your question but I'm using string constants instead of string literals most everywhere I expect to use that string more than once, which is a practice I follow to avoid typo-related bugs etc.

Using this function, you can send the [carousel] initially like this

async testCarousel(turnContext) {
    const batchId = Date.now();
    const leads = [1, 2, 3, 4, 5];
    const reply = this.createCarousel(batchId, leads);
    const response = await turnContext.sendActivity(reply);
    const dict = await this.carouselState.get(turnContext, {});

    dict[batchId] = {
        [KEYACTIVITYID]: response.id,
        [KEYLEADS]: leads
    };
}

And you can update the [carousel] in response to the card's [qualify] submit action like this

async handleSubmitAction(turnContext) {
    const value = turnContext.activity.value;

    switch (value[KEYACTION]) {
        case ACTIONQUALIFYLEAD:
            const dict = await this.carouselState.get(turnContext, {});
            const batchId = value[KEYBATCHID];
            const info = dict[batchId];
            if (info) {
                const leads = info[KEYLEADS];
                const objectId = value[KEYOBJECTID];
                var index = leads.indexOf(objectId);
                if (index !== -1) leads.splice(index, 1);
                const update = this.createCarousel(batchId, leads);
                update.id = info[KEYACTIVITYID];
                if (update.attachments.length) {
                    await turnContext.updateActivity(update);
                } else {
                    await turnContext.deleteActivity(update.id);
                }
            }
            break;
    }
}
Kyle Delaney
  • 11,616
  • 6
  • 39
  • 66
  • the very first line which is saving the carousel state. where is that variable defined? how is it able to hold data across multiple interactions? Also essentially you are deleting the particular card and resending the remaining cards (carousel) using updateActvity. Correct ? – Moblize IT Dec 21 '19 at 03:24
  • @MoblizeIT - The first line isn't saving state, it's creating a [state property accessor](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-state#state-property-accessors). Property accessors are usually created in the bot's constructor, as can be seen in samples like [this](https://github.com/microsoft/BotBuilder-Samples/blob/master/samples/javascript_nodejs/03.welcome-users/bots/welcomeBot.js#L20) and [this](https://github.com/microsoft/BotBuilder-Samples/blob/master/samples/javascript_nodejs/13.core-bot/bots/dialogBot.js#L22). – Kyle Delaney Dec 23 '19 at 19:09
  • To understand how bot state is persisted between turns, please refer to the documentation: https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-state. `updateActivity` doesn't "delete" or "resend" anything necessarily. As the name suggests, it is updating an existing activity. In your case, it's modifying the carousel to not include the particular card. The SDK function uses [this REST API call](https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference#update-activity). Remember to accept this answer. – Kyle Delaney Dec 23 '19 at 19:14
  • This is btw a great example on why templating helps. Using Adaptivecards Templating would make 95% of your code obsolete and make it a ton easier to spot issues – Tim Cadenbach Jan 06 '20 at 23:51
  • @TimCadenbach - I wrote an answer involving templating to another question about updating Adaptive Cards, and it's still quite a complicated process. Can you explain what code you would consider to be made obsolete by templating? – Kyle Delaney Jan 07 '20 at 00:00
  • the whole CreateCarusel func can be done with templating. The for loop at least. In general with templating you dont have to create any json yourself. Its pretty much databinding to json. – Tim Cadenbach Jan 07 '20 at 09:58
  • @TimCadenbach - Well if you have a templating solution that would eliminate 95% of my code then I'd love to see it. Feel free to edit the answer. – Kyle Delaney Jan 07 '20 at 18:24
  • 95% was a bit over the top :P however have a look at this: https://jsfiddle.net/vmgd2sup/ This will return exactly what your create function does however its not using any loop, not creating any unnecessary objects which only get serialized anyway and should be easier to use and maintain. All you need to do to remove a card is edit your array with less items and it will produce less cards This has the added benefit, if you ever want to change the card you don't need to change any code just update the card. If you store your card seperately you don't even have to recompile anything... – Tim Cadenbach Jan 07 '20 at 22:50
  • Just have to add, templating is so much better in so many cases compared to creating cards normally...anyone should really stop suggesting to manually create cards. Cards created in your code are unchangeable without a new release, have to be maintained with your code etc etc... with templating your cards can be stored anywhere you like to. :) – Tim Cadenbach Jan 07 '20 at 22:58
  • @TimCadenbach - The code you linked to would not create a carousel of 5 cards, it would create one card with 5 text blocks and 5 buttons – Kyle Delaney Jan 07 '20 at 23:39
  • Can't easily post that here but if you run the jsfiddle and look at console output you'll see a message with 5 attachments, each a seperate card – Tim Cadenbach Jan 10 '20 at 22:34
  • You have to understand how templating works to see that. The $data param at the beginning of the card essentially repeats the full card for every item in the array. To be more exact, $data repeats everything in the same level of hierarchy its in. On the root it repeats the full card, within a button it only repeats the button etc etc – Tim Cadenbach Jan 10 '20 at 22:37
  • @TimCadenbach - Oh that's not bad, but I do need to make one correction. Your `Activity.attachments` needs to be an array of `Attachment` objects, not an array of card objects. I suppose `Array.map` would help here. – Kyle Delaney Jan 11 '20 at 00:19
  • @TimCadenbach - Actually there's no need for `map`. I just moved the $data up a level like you said and was able to generate an array of `Attachment` objects just using `template.expand`. – Kyle Delaney Jan 11 '20 at 00:28
  • Yep glad it worked. Feel the Card lads are not great explaining how powerful that stuff is :) – Tim Cadenbach Jan 11 '20 at 10:46
  • sorry for taking forever to get on this. when i do await context.updateActivity(myCardCarousal) it throws error missing await context.updateActivity() plz advise – Moblize IT Feb 04 '20 at 02:26
  • @MoblizeIT - If you accept this answer and ask a new question that explains your new problem then I'll be able to help you – Kyle Delaney Feb 04 '20 at 18:04
  • @KyleDelaney posted https://stackoverflow.com/questions/60063335/microsoft-bots-to-teams-using-nodejs-fails-with-missing-activityid-when-updating – Moblize IT Feb 04 '20 at 18:16