npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@slack-wrench/bolt-interactions

v2.1.0

Published

Easily create interaction patterns in bolt.

Downloads

333

Readme

Bolt ⚡️ Interactions

Bolt interactions allows you to effortlessly create stateful user action flows with your Bolt app.

Vanilla Bolt gives you some ability to do this already, but its state is scoped to a whole channel. This library keeps the scope to the interaction allowing for faster data fetching and a greater ability to rationalize how your app works.

Install

yarn add @slack-wrench/bolt-interactions
# or
npm install --save @slack-wrench/bolt-interactions

Understanding Flows

This package creates a new concept of a user interaction called a flow. A flow logically represents a set of linked actions typically associated with a user flow. Introducing this new concept makes your application easier to reason about and keeps your focus on the outcome and user interaction.

flows are implemented as a set of bolt event listeners linked by a shared state.

Here's an example:

import { App } from '@slack/bolt';
import {
  interactionFlow,
  InteractionFlow,
} from '@slack-wrench/bolt-interactions';
import FileStore from '@slack-wrench/bolt-storage-file'; // Use whatever ConversationStore you want

// Create and configure your app with a ConversationStore
const convoStore = new FileStore();
InteractionFlow.store = convoStore;
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
  convoStore,
});

// Create a new interaction, usually this will be the default export of its own file
const commandFlow = interactionFlow('yourCommand', (flow, app) => {
  app.command('/bolt-interaction', ({ say }) => {
    // Start a new flow from a command, set an initial state
    const { interactionIds } = flow.start({ clicked: 0 });

    say({
      blocks: [
        {
          type: 'section',
          text: {
            type: 'plain_text',
            text: 'You used a command!',
            emoji: true,
          },
        },
        {
          type: 'actions',
          elements: [
            {
              type: 'button',
              text: {
                type: 'plain_text',
                text: 'Click Me',
                emoji: true,
              },
              // Use flow interaction ids from context
              action_id: interactionIds.appButton,
            },
            {
              type: 'button',
              text: {
                type: 'plain_text',
                text: 'Stop',
                emoji: true,
              },
              // Use flow interaction ids from context
              action_id: interactionIds.stopButton,
            },
          ],
        },
      ],
    });
  });

  flow.action('appButton', async ({ context, say }) => {
    // Access the current state, and functions to set new state, end the flow,
    // or get interactionIds for continuing the flow
    const { state, setState, endFlow, interactionIds } = context;

    state.clicked += state.clicked;

    say(`The button has been clicked ${state.clicked}`);

    // Update the state for new buttons
    await setState(state);
  });

  flow.action('stopButton', async ({ context, say }) => {
    const { endFlow } = context;

    say('You ended the flow');

    // Cleans up state in the database
    await endFlow();
  });
});

// Register your flows to the app
commandFlow(app);

(async () => {
  // Start the app
  await app.start(process.env.PORT || 3000);

  console.log('⚡️ Bolt app is running!');
})();

Working with your Flow

Flow Listeners

Flow listeners are a special set of bolt event listeners. They can be created with the flow counter part of bolt app listeners (flow.action and app.action for example). They can do everything that normal bolt listeners can plus they can interact with the flow they're a part of.

Flow Context

Flow listeners have their context extended with some extra functions and data. You can also get these through flow.start.

When you create a new flow with flow.start, you've created a "flow instance" that is unique to that interaction.

  • state (FlowState) - The current state of that flow instance.
  • setState ((state: any, expiresAt?: number) => Promise) - Function to update the state
  • endFlow (() => Promise) - Function to end the flow, and clean up its state
  • interactionIds (Record<string, string)) - ids to pass to block kit for various actions, usually action_id.

interactionFlow

interactionFlow<FlowState>((flow, app) => {
  // Flow code
});
// => : (App) => InteractionFlow

Helper function to create a new InteractionFlow. It allows you to separate your interaction flow from the Bolt app to help organize your code base.

flow.start

At any time, you can start a new flow. You can do it when a non-stateful slack action happens (like a command, or a message), or when something outside of slack happens, like a webhook from github.

interactionFlow<FlowState>((flow, app) => {
  flow.start(initialState); // Can be called any time
  // => : Promise<Interaction.FlowContext<FlowState>>
});

Starts a new flow and sets the initial state. You can also pass an optional instanceId.

Arguments:

  • initialState (any): The starting state of the flow. Flow middleware will get this value whenever they're called.
  • instanceUd (string) Optional: If you have an instance id that you want to use, otherwise it will be generated with InteractionFlow.interactionIdGenerator

Returns: The flow context

flow.action

interactionFlow<FlowState>((flow, app) => {
  flow.action(...listeners, { context, ... });
});

Create a flow listener that listens for actions

flow.view

In handling views, you'll need to pass an interactionId as the callback and reference the same string in a flow.view action handler. An example of handling view submissions with a view that is opened when an overflow action is clicked:

interactionFlow<FlowState>((flow, app) => {
  const callback_id = 'edit';

  flow.action<BlockOverflowAction>(
    'openEdit',
    async ({
      action: { action_id },
      body: { trigger_id },
      context: { token, interactionIds },
    }) => {
      await flow.client.views.open({
        trigger_id,
        token,
        view: {
          type: 'modal',
          // setup callback_id with unique interactionId based on string
          callback_id: interactionIds[callback_id],
          title: PlainText('Edit Story'),
          submit: PlainText('Update Story'),
          close: PlainText('Cancel'),
          blocks: [
            /* ... */
          ],
        },
      });
    },
  );

  // handle all submissions of above; callback_id is used to recapture context
  flow.view<ViewSubmitAction>(callback_id, async ({ view, context, ... }) => {
    /* do things when a SlackViewAction is triggered */
  });
});

Working with Ids

If you are working with your own ids for flow instances, it can be helpful to parse them. There some utilities to help with that.

InteractionFlow.parseFlowId

const parsedFlowId = interactionFlow.parseFlowId('flow_12345');

expect(parsedFlowId).toEqual({
  flowId: 'flow_12345',
  name: 'flow',
  instanceId: '12345',
});

InteractionFlow.parseInteractionId

const parsedFlowId = interactionFlow.parseFlowId('flow_12345:::interaction');

expect(parsedFlowId).toEqual({
  flowId: 'flow_12345',
  name: 'flow',
  instanceId: '12345',
  interaction: 'interaction',
  interactionId: 'flow_12345:::interaction',
});

Testing your Flow

When testing your flows, it's helpful to have some predictability and not need whole databases. bolt-interactions exports a few hooks that you can use to make this happen.

import { MemoryStore } from '@slack/bolt';
import { InteractionFlow } from '@slack-wrench/bolt-interactions`;

// Update the store that Interaction flows use
InteractionFlow.store = new MemoryStore();

// Change the function that randomly generates ids to something a
// bit more predictable
InteractionFlow.interactionIdGenerator = () => 'a-random-string';