Search code examples
slackslack-apibolt

Slack Bolt Clearing view stack


In the view_submission type I set ack to clear the stack like this:

await submissionAck({ response_action: 'clear' } as any)

First question - why do I have to cast it to any? Without it code throws error

Argument of type '{ response_action: "clear"; }' is not assignable to parameter of type '(ViewUpdateResponseAction & void) | (ViewPushResponseAction & void) | (ViewClearResponseAction & void) | (ViewErrorsResponseAction & void) | undefined'.Type '{ response_action: "clear"; }' is not assignable to type 'ViewClearResponseAction & void'.
Type '{ response_action: "clear"; }' is not assignable to type 'void'.

Second question - the stack seems not to be cleared. When I submit modal for the first time it's okay, but if I try next time it throws:

[ERROR]  bolt-app { Error: The receiver's `ack` function was called multiple times.
    at ack (/home/ec2-user/metrics/node_modules/@slack/bolt/src/ExpressReceiver.ts:147:17)
    at /home/ec2-user/metrics/app/actions.ts:43:17
    at Generator.next (<anonymous>)
    at /home/ec2-user/metrics/app/actions.ts:11:71
    at new Promise (<anonymous>)
    at __awaiter (/home/ec2-user/metrics/app/actions.ts:7:12)
    at app.view (/home/ec2-user/metrics/app/actions.ts:40:70)
    at process_1.processMiddleware (/home/ec2-user/metrics/node_modules/@slack/bolt/src/App.ts:660:19)
    at invokeMiddleware (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/process.ts:36:12)
    at next (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/process.ts:28:21)
    at Array.<anonymous> (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/builtin.ts:201:11)
    at invokeMiddleware (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/process.ts:27:47)
    at next (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/process.ts:28:21)
    at Array.exports.onlyViewActions (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/builtin.ts:110:11)
    at invokeMiddleware (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/process.ts:27:47)
    at Object.processMiddleware (/home/ec2-user/metrics/node_modules/@slack/bolt/src/middleware/process.ts:39:10) code: 'slack_bolt_receiver_ack_multiple_error' }

Any ideas? That's how I call these views: (by the way 3rd question - why do I have to cast body to BlockAction? Otherwise it throws error that trigger_id doesn't exists)

  app.action('modify', async ({ body, ack }) => {
    await ack()
    await authenticate(body.team.id, async (customer: Customer) => {
      await app.client.views.open({
        trigger_id: (body as BlockAction).trigger_id,
        token: 'token',
        view: modificationModal,
      })
      app.view(
        {
          type: 'view_submission',
          callback_id: 'yay',
        },
        async ({ body: submissionBody, ack: submissionAck, view }) => {
          const receivedValues = submissionBody.view.state.values
          await submissionAck({ response_action: 'clear' } as any)
        },
      )
    })
  })

I know that in the documentation stands:

view() requires a callback_id of type string or RegExp.

but that doesn't tell me much. What is that string? Is that a function? What should it do?

Sorry for noobish question and thanks for help!


Solution

  • I'll try to answer these in the reverse order, because I think that might make the most sense.

    What is that string? Is that a function? What should it do? (referring to app.view())

    When you create a Modal, you typically create it with a callback_id. You can see a description for that property in the documentation for a view payload.

    That sentence is trying to say this is how you'd listen for a view submission for a view that was created with callback_id set to "some_callback_id":

    app.view('some_callback_id', async () => {
      /* listener logic goes here */
    })
    

    Note: You could also use a regular expression if you wanted the same function to handle view submissions for many views - views whose callback_ids all follow the same pattern. But the regular expression is a pretty advanced case that I don't think we should worry about it for now.

    To create a Modal, you use the views.open method, and that's where you'll set the callback_id in the first place. I'm going to suggest an improvement. All Web API methods are available inside a listener as methods on the client argument. Then you don't need to worry about adding the token. Here's an example of using this:

    // Add the `client` argument
    app.action('modify', async ({ body, ack, client }) => {
      await ack()
      await authenticate(body.team.id, async (customer: Customer) => {
        // Remove `app.`
        await client.views.open({
          // Let's come back to this cast later
          trigger_id: (body as BlockAction).trigger_id,
          // Not sure what's in modificationModal, but to illustrate, I used a literal
          view: {
            // *** Setting the callback_id ***
            callback_id: 'modify_submission',
            title: {
              type: 'plain_text',
              text: 'Modify something'
            },
            blocks: [{ /* add your blocks here */ }],
          },
        })
      })
    })
    

    Next, don't handle a view submission inside another listener. When you do that, each time the outer listener runs, you're (re)registering the view submission listener to run. So the first time it will run once, the second time it will run twice, the third time it will run three times. This explains why the stacktrace is telling you that ack() was called multiple times. Instead, just handle the view submission outside that listener. The "linking" information between the two interactions is the callback_id. Building off the previous example:

    // Further down in the same file, at the same level as the previous code
    // *** Using the callback_id we set previously ***
    app.view('modify_submission', async ({ body, ack, view }) => {
      const receivedValues = body.view.state.values
      // Let's come back to this cast later
      await ack({ response_action: 'clear' } as any)
    })
    

    Okay this should all work, but now let's talk about the casts. When you handle an action with app.action(), the body argument is typed as BlockAction | InteractiveMessage | DialogSubmitAction. Within these interfaces, BlockAction and InteractiveMessage do have a trigger_id property, but DialogSubmitAction does not. So as far as TypeScript is concerned, it can't be sure the property body.trigger_id exists. You might know that the action you're handling is a BlockAction (let's assume it is), but TypeScript does not! However, Bolt was built to allow users to give TypeScript some more information by using a generic parameter. Here's a part of the first example, modified to use the generic parameter.

    import { BlockAction } from '@slack/bolt';
    
    app.action<BlockAction>('modify', async ({ body, ack, client }) => {
       // `body` is now typed as a BlockAction, and therefore body.trigger_id is a string
    });
    

    It's a pretty similar story for app.view() and generic parameters. This time, the body argument is of type ViewSubmitAction | ViewClosedAction. Using a clear response action doesn't make sense for a ViewClosedAction, so we need to constrain the type once again. That's right, the generic parameter is hooked up to more than just the type of body, it can actually change (constrain) any of the listener arguments! In this case, the generic parameter changes the type of ack().

    import { ViewSubmitAction } from '@slack/bolt';
    
    app.view<ViewSubmitAction>('modify_submission', async ({ body, ack, view }) => {
      // No errors now
      await ack({ response_action: 'clear' });
    });
    

    Final note: The way you wrote the view submission handler with a constraints object ({ type: 'view_submission', callback_id: 'yay' } does seem like you've given TypeScript enough information to constrain the types of the listener arguments. That actually would work for app.action({ type: 'block_actions', ... }, ...) because we defined ActionConstraints to be generic. This is an area where Bolt could be improved and all it would take is making ViewConstraints generic in the same way.