How to get input from a modal or dialog in a slack workflow step?

I made a slack app to submit helpdesk tickets directly from a form on Slack.

I implemented my app using the interactivity and shortcuts feature, API Gateway and Lambda. Here's an overview of how the app works:

  1. The user clicks on the shortcut to open up a form
  2. The lambda function receives an "shortcut" interaction, and uses the trigger_id to call the views.open method to send the form to the user
  3. User fills and submits the form
  4. Lambda function receives a "view_submission" interaction with the user inputs. The function parses the data sends a POST request to the helpdesk service that creates the ticket

The problem
The app is functional. However, it's required that the shortcut be pinned to the top of the shortcuts menu on a specific support channel on slack. According to this page, this is only seems possible for workflow shortcuts, not for app shortcuts.

So, I'm trying to refactor my app to be triggered by a custom workflow step instead of an app shortcut.

What I managed to do
When adding the workflow step to a workflow, the app needs to handle a workflow_step_edit interaction, send a configuration view, and then handle that view submission.

I'm doing all that with and empty configuration screen that receives no inputs.

from slack_sdk import WebClient
import boto3

import json
import base64
import urllib
import traceback
from datetime import datetime, timedelta
import requests
import os

SLACK_TOKEN_SECRET_NAME = os.environ.get('SLACK_TOKEN_SECRET_NAME')
FRESHSERVICE_TOKEN_SECRET_NAME = os.environ.get('FRESHSERVICE_TOKEN_SECRET_NAME')
FRESHSERVICE_DOMAIN = os.environ.get('FRESHSERVICE_DOMAIN')
SUPPORT_CHANNEL_ID = os.environ.get('SUPPORT_CHANNEL_ID')

def lambda_handler(event, _):
    print(json.dumps(event))

    # Base 64 decode the payload
    payload = base64.b64decode(event['body'])
     
    # Payload is URL encoded parameters, parse it
    payload = payload.decode('utf-8')
    payload = urllib.parse.parse_qs(payload)
    payload = json.loads(payload['payload'][0])
    print(json.dumps(payload))

    # Create a WebClient
    client = WebClient(token=get_secret_value(SLACK_TOKEN_SECRET_NAME)['token'])

    # Invoke the correct method based on the callback id
    try:
        # User tries to add/edit the workflow step to a workflow
        if payload['type'] == 'workflow_step_edit':
            open_configuration_modal(payload, client)
        # User submits the configuration
        elif payload['type'] == 'view_submission' and payload.get('workflow_step'):
            save_configuration(payload, client)
            return {
                'statusCode': 200,
                'body': 'OK'
            }
        # User clicks on the shortcut
        elif payload['type'] == 'shortcut': # Would be modified to handle the "workflow_step_execute" event
            send_form(payload, client)
            return {
                'statusCode': 200
            }
        # User submtis the form
        elif payload['type'] == 'view_submission':
            create_ticket(payload, client)
            return {
                'statusCode': 200
            }

    except Exception as e:
        print(traceback.format_exc())
        return {
            'statusCode': 500
        }

def open_configuration_modal(payload, client):

    # This step requires no configuration
    # Send a basic view with a confirmation message
    view = {
        "type": "workflow_step",
        "submit_disabled": False,
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "plain_text",
                    "text": "This step requires no configuration.",
                }
            }
        ]
    }
    # Send the view
    client.views_open(
        trigger_id=payload['trigger_id'],
        view=view
    )

def save_configuration(payload, client):
    # There is no configuration to save
    # Do a basic call to workflows.updateStep
    client.workflows_updateStep(
        workflow_step_edit_id=payload['workflow_step']['workflow_step_edit_id']
    )

So, righ now I can add the workflow step to a workflow, and use the workflow to trigger my app with an "workflow_step_execute_event".

Where I'm stuck exactly
Unfortunately, the "workflow_step_execution" does not have a "trigger_id" that I can use to open my modal form. So I need a workaround to be able to open that form.

What I also tried
Also tried to ditch my custom form, and add the standard "Send a form" step of the Workflow Builder (not ideal), but I couldn't figure out how to configure my app to receive inputs from a previous step.

Possible solutions I'm looking for
Here are possible solutions to my problem

Upvotes: 0

Views: 1842

Answers (1)

I never found a solution to do exactly what I wanted. But I want to share the workaround that I used because it might be useful for other people that face the same problem.

The workaround is sending the form as an ephemeral message with the chat.postEphemeral method.

This method does not need a trigger_id, therefore can be triggered via a workflow_step_execution event with little trouble.

The limitations of this solution is that an ephemeral message needs to be sent to a specific channel, and that we are sacrificing the modal UI. Those were not problems for my use case, but it's good to keep that in mind.

About the needy greedy, the important part is how to configure the workflow step so your app receives the required arguments for the chat.postEphemeral call upon the execution of the workflow step.

Sending the configuration form
The chat.postEphemeral call needs to receive the user_id of the recipient of the form. So we send this configuration form:

def open_configuration_modal(payload, client):

    # This step requires no configuration
    # Send a basic view with a confirmation message
    view = {
        "type": "workflow_step",
        "submit_disabled": False,
        "blocks": [
            {
                "type": "section",
                "block_id": "form_recipient",
                "text": {
                    "type": "mrkdwn",
                    "text": "Who should be the recipient of the form?"
                },
                "accessory": {
                    "type": "users_select",
                    "placeholder": {
                        "type": "plain_text",
                        "text": "Select a user"
                    },
                    "action_id": "users_select-action"
                }
            }
        ]
    }
    # Send the view
    client.views_open(
        trigger_id=payload['trigger_id'],
        view=view
    )

On Slack, workflow builder, when we add the step to the workflow, we should see this:

enter image description here

In my case, I want the person that clicked on the workflow shortcut to receive the form, so I select the option "Person who clicked" and then "Save".

It's worthy pointing out that the chat.postEphemeral message also needs a channel_id argument. In my case, I hardcoded this id to a specific support channel. But it would be possible to do a very similar thing to the channel to which the form will be posted, by adding a channel_select input to the configuration modal.

Handling the view_submission event
When the workflow builder saves the configuration, the app will receive a view_submission payload, frow which we need to extract the user_id variable name and set is the input to our step. This can be done like so:

def save_configuration(payload, client):
    # There is no configuration to save
    # Do a basic call to workflows.updateStep
    user_ref = payload['view']['state']['values']['form_recipient']['users_select-action']['selected_user']
    client.workflows_updateStep(
        workflow_step_edit_id=payload['workflow_step']['workflow_step_edit_id'],
        inputs = {"recipient":{"value":user_ref, "skip_variable_replacement": False}}
    )

Again, similar thing could be done to also pass a channel_id as input. Respond to the request with a 200 status code, and this completes the workflow step configuration.

Handling the step execution event
Now, when the user triggers the workflow, and the step is executed, the app will receive a workflow_step_execution event, with the user_id as input. After rendering our block kit form, we need to get these variables and pass them on to the chat.postEphemeral method.

def send_form(payload, client):
    # Define the dialog
    view = {
        "type": "modal",
        "callback_id": "receive_ticket",
        "title": {
            "type": "plain_text",
            "text": "Create Ticket"
        },
        "submit": {
            "type": "plain_text",
            "text": "Submit"
        },
        "close": {
            "type": "plain_text",
            "text": "Cancel"
        },
        "blocks": [
            # Form stuff...
            ]
            }
        ]
    }

    # Post ephemeral message
    user_id = payload['event']['workflow_step']['inputs']['recipient']['value']
    print(user_id)
    client.chat_postEphemeral(
        channel=SUPPORT_CHANNEL_ID, # Hardcoded ID
        user=user_id,
        text="Please fill out the form below.",
        blocks=view['blocks']
    )

Handling the form submission
Not going into details from here, but when the user fills and submits the form, your app will have to handle a "block_actions" event.

Our app can now parse the payload that includes the users input and whatever it wants with it. In my case, create a support ticket on our helpdesk system.

I also recommend responding to the event using the response_url to update the ephemeral message/form to a confirmation message to give the users some feedback.

That's all

Upvotes: 1

Related Questions