slackapi / bolt-python

A framework to build Slack apps using Python
https://slack.dev/bolt-python/
MIT License
1.03k stars 237 forks source link

Clearing Errors in Modal #1048

Closed TechShivvy closed 4 months ago

TechShivvy commented 4 months ago

I'm encountering an issue with clearing errors in Slack Bolt Python modal. When submitting a form in the modal, if there are validation errors [ack(response_action="errors",errors=errors]), the errors persist even after the user starts focusing on a particular field or provides valid input. I want the errors to be cleared as the user interacts with that form field.

Basically, I want to achieve a behavior similar to default fields in a modal. Currently, if I submit the form without filling in some default fields, the system displays "Please complete this required field." If the user starts filling in or selecting values for those fields, the error message goes away. I want the same functionality for custom fields in the modal. For example, in a static_select dropdown with numbers from 1 to 10 as options, if the user selects a value above 5, an error is thrown. However, if the user then selects a value below 5, I don't know how to clear the error.

Is this possible to do without changing the block_id or action_id of that field?

Reproducible in:

The slack_bolt version

slack-bolt==1.18.1 slack_sdk==3.26.2 slackeventsapi==3.0.1

Python runtime version

Python 3.10.13

OS info

Microsoft Windows [Version 10.0.19045.4046]

Steps to reproduce:

I have used the code by @seratch for demo purposes.

from datetime import datetime
from typing import Callable

from slack_bolt import App, Ack, BoltResponse
from slack_sdk import WebClient

from slack_bolt.adapter.socket_mode import SocketModeHandler

logging.basicConfig(level=logging.DEBUG)

import os
from pathlib import Path
from dotenv import load_dotenv

env_path = Path(".") / ".env"

load_dotenv(dotenv_path=env_path)

app = App(token=os.environ.get("SLACK_BOT_TOKEN"), name="Dev Bot")

@app.middleware  # or app.use(log_request)
def log_request(logger: logging.Logger, body: dict, next: Callable[[], BoltResponse]) -> BoltResponse:
    logger.debug(body)
    return next()

step1_modal = {
    "type": "modal",
    "callback_id": "helpdesk-request-modal",
    "title": {"type": "plain_text", "text": "Helpdesk Request"},
    "close": {"type": "plain_text", "text": "Close"},
    "blocks": [
        {
            "type": "section",
            "text": {"type": "mrkdwn", "text": ":wave: Select a category."},
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "static_select",
                    "action_id": "helpdesk-request-modal-category-selection",
                    "options": [
                        {
                            "text": {"type": "plain_text", "text": "Laptop"},
                            "value": "laptop",
                        },
                        {
                            "text": {"type": "plain_text", "text": "Mobile"},
                            "value": "mobile",
                        },
                        {
                            "text": {"type": "plain_text", "text": "Other"},
                            "value": "other",
                        },
                    ],
                }
            ],
        },
    ],
}
step2_laptop_modal = {
    "type": "modal",
    "callback_id": "helpdesk-request-modal",
    "title": {"type": "plain_text", "text": "Helpdesk Request"},
    "submit": {"type": "plain_text", "text": "Submit"},
    "close": {"type": "plain_text", "text": "Close"},
    "blocks": [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "You're making a request on your laptop.",
            },
            "accessory": {
                "type": "button",
                "action_id": "helpdesk-request-modal-reset",
                "text": {"type": "plain_text", "text": "Back"},
                "value": "1",
            },
        },
        {
            "type": "input",
            "block_id": "title",
            "label": {"type": "plain_text", "text": "Title"},
            "element": {
                "type": "plain_text_input",
                "action_id": "element",
                "initial_value": "Laptop Replacement",
            },
        },
        {
            "type": "input",
            "block_id": "laptop-model",
            "label": {"type": "plain_text", "text": "Laptop Model"},
            "element": {
                "type": "static_select",
                "action_id": "element",
                "options": [
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "MacBook Pro (16-inch, 2019)",
                        },
                        "value": "MacBookPro16,1",
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "MacBook Pro (13-inch, 2019, Two Thunderbolt 3 ports)",
                        },
                        "value": "MacBookPro15,4",
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "Surface Book 3 for Business",
                        },
                        "value": "SurfaceBook3",
                    },
                ],
            },
        },
    ],
}

step2_mobile_modal = {
    "type": "modal",
    "callback_id": "helpdesk-request-modal",
    "title": {"type": "plain_text", "text": "Helpdesk Request"},
    "submit": {"type": "plain_text", "text": "Submit"},
    "close": {"type": "plain_text", "text": "Close"},
    "blocks": [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "You're making a request on your mobile devices.",
            },
            "accessory": {
                "type": "button",
                "action_id": "helpdesk-request-modal-reset",
                "text": {"type": "plain_text", "text": "Back"},
                "value": "1",
            },
        },
        {
            "type": "input",
            "block_id": "title",
            "label": {"type": "plain_text", "text": "Title"},
            "element": {
                "type": "plain_text_input",
                "action_id": "element",
                "initial_value": "Mobile Device Replacement",
            },
        },
        {
            "type": "input",
            "block_id": "os",
            "label": {"type": "plain_text", "text": "Mobile OS"},
            "element": {
                "type": "static_select",
                "action_id": "element",
                "placeholder": {"type": "plain_text", "text": "Select an item"},
                "options": [
                    {"text": {"type": "plain_text", "text": "iOS"}, "value": "ios"},
                    {
                        "text": {"type": "plain_text", "text": "Android"},
                        "value": "android",
                    },
                ],
            },
        },
        {
            "type": "input",
            "block_id": "approver",
            "label": {"type": "plain_text", "text": "Approver"},
            "element": {
                "type": "users_select",
                "action_id": "element",
                "placeholder": {"type": "plain_text", "text": "Select your approver"},
            },
        },
        {
            "type": "input",
            "block_id": "due-date",
            "element": {"type": "datepicker", "action_id": "element"},
            "label": {"type": "plain_text", "text": "Due date", "emoji": True},
        },
    ],
}

step2_other_modal = {
    "type": "modal",
    "callback_id": "helpdesk-request-modal",
    "title": {"type": "plain_text", "text": "Helpdesk Request"},
    "submit": {"type": "plain_text", "text": "Submit"},
    "close": {"type": "plain_text", "text": "Close"},
    "blocks": [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "You're making a request on your This may be a bit rare one. Please share the details with us as much as possible.",
            },
            "accessory": {
                "type": "button",
                "action_id": "helpdesk-request-modal-reset",
                "text": {"type": "plain_text", "text": "Back"},
                "value": "1",
            },
        },
        {
            "type": "input",
            "block_id": "title",
            "label": {"type": "plain_text", "text": "Title"},
            "element": {
                "type": "plain_text_input",
                "action_id": "element",
                "initial_value": "",
            },
        },
        {
            "type": "input",
            "block_id": "description",
            "label": {"type": "plain_text", "text": "Description"},
            "element": {
                "type": "plain_text_input",
                "action_id": "element",
                "multiline": True,
            },
        },
    ],
}

@app.command("/hello-bolt-python")
def open_modal_step1(ack: Ack, body: dict, client: WebClient):
    ack()
    res = client.views_open(trigger_id=body["trigger_id"], view=step1_modal)

@app.action("helpdesk-request-modal-category-selection")
def show_modal_step2(ack: Ack, body: dict, action: dict, client: WebClient):
    ack()
    category = action["selected_option"]["value"]
    view = {}
    if category == "laptop":
        view = step2_laptop_modal
    elif category == "mobile":
        view = step2_mobile_modal
    else:
        view = step2_other_modal
    res = client.views_update(
        view_id=body["view"]["id"], hash=body["view"]["hash"], view=view
    )

@app.action("helpdesk-request-modal-reset")
def show_modal_step1_again(ack: Ack, body: dict, client: WebClient):
    ack()
    res = client.views_update(
        view_id=body["view"]["id"], hash=body["view"]["hash"], view=step1_modal
    )

@app.view("helpdesk-request-modal")
def accept_view_submission(ack: Ack, body: dict, logger: logging.Logger):
    values = body["view"]["state"]["values"]
    actionId = "element"
    title = values["title"][actionId]["value"] if "title" in values else None
    laptop_model = (
        values["laptop-model"][actionId]["selected_option"]["value"]
        if "laptop-model" in values
        else None
    )
    os = values["os"][actionId]["selected_option"]["value"] if "os" in values else None
    description = (
        values["description"][actionId]["value"] if "description" in values else None
    )
    due_date = (
        values["due-date"][actionId]["selected_date"] if "due-date" in values else None
    )
    approver = (
        values["approver"][actionId]["selected_user"] if "approver" in values else None
    )

    errors = {}
    if title is not None and len(title) <= 5:
        errors["title"] = "Title must be longer than 5 characters"
    if (
        due_date is not None
        and datetime.strptime(due_date, "%Y-%m-%d") <= datetime.today()
    ):
        errors["due-date"] = "Due date must be in the future"

    if len(errors) > 0:
        ack(response_action="errors", errors=errors)
        return

    ack()

    logger.info(
        f"title: {title}, "
        f"laptop_model: {laptop_model}, "
        f"os: {os}, "
        f"description: {description}, "
        f"due_date: {due_date}, "
        f"approver: {approver}"
    )

if __name__ == "__main__":
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()
  1. Trigger the modal by using the /hello-bolt-python command.
  2. Select 'Other' category in the first step.
  3. In the second step, intentionally provide invalid input to generate errors (e.g., submit a title with fewer than 5 characters and don't fill the description as well).
  4. Observe that the error is displayed.
  5. Attempt to correct the error by providing valid input.

Expected result:

I expect that once the user starts focusing on a field or provides valid input, the corresponding error message should be cleared dynamically, similar to the behavior of default fields in Slack Bolt Python modals.

Actual result:

The error persists even after the user interacts with the field or provides valid input.

image image

Requirements

Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.

seratch commented 4 months ago

Hi @TechShivvy, unfortunately, the errors persist until the user submits the modal again. There is no workaround, and we don't have short-term plans to enhance for your need. I understand this is not a great answer for you, but it'd be appreciated if you could understand this.

TechShivvy commented 4 months ago

Ah, cool. I appreciate your quick response and clarification. I did explore available slack resources online before reaching out here, so I understand the current limitations. Thanks for taking the time to address my query, even though there might not be an immediate workaround. I'll keep an eye out for any updates in the future. Thanks again! @seratch