Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Block Kit in modals support #261

Merged
merged 2 commits into from
Sep 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/_basic/listening_modals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: Listening for view submissions
lang: en
slug: view_submissions
order: 11
---

<div class="section-content">
If a <a href="https://api.slack.com/reference/block-kit/views">view payload</a> contains any input blocks, you must listen to <code>view_submission</code> events to receive their values. To listen to <code>view_submission</code> events, you can use the built-in <code>view()</code> method.

<code>view()</code> requires a <code>callback_id</code> of type <code>string</code> or <code>RegExp</code>.

You can access the value of the <code>input</code> blocks by accessing the <code>state</code> object. <code>state</code> contains a <code>values</code> object that uses the <code>block_id</code> and unique <code>action_id</code> to store the input values.

Read more about view submissions in our <a href="https://api.slack.com/block-kit/surfaces/modals#handling_submissions">API documentation</a>.
</div>

```javascript
// Handle a view_submission event
app.view('view_b', async ({ ack, body, view, context }) => {
// Acknowledge the view_submission event
ack();

// Do whatever you want with the input data - here we're saving it to a DB then sending the user a verifcation of their submission

// Assume there's an input block with `block_1` as the block_id and `input_a`
const val = view['state']['values']['block_1']['input_a'];
const user = body['user']['id'];

// Message to send user
let msg = '';
// Save to DB
const results = await db.set(user.input, val);

if (results) {
// DB save was successful
msg = 'Your submission was successful';
} else {
msg = 'There was an error with your submission';
}

// Message the user
try {
app.client.chat.postMessage({
token: context.botToken,
channel: user,
text: msg
});
}
catch (error) {
console.error(error);
}

});
```
2 changes: 1 addition & 1 deletion docs/_basic/listening_responding_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Listening and responding to options
lang: en
slug: options
order: 9
order: 12
---

<div class="section-content">
Expand Down
78 changes: 78 additions & 0 deletions docs/_basic/opening_modals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Opening modals
lang: en
slug: creating-modals
order: 9
---

<div class="section-content">
<a href="https://api.slack.com/block-kit/surfaces/modals">Modals</a> are focused surfaces that allow you to collect user data and display dynamic information. You can open a modal by passing a valid <code>trigger_id</code> and a <a href="https://api.slack.com/reference/block-kit/views">view payload</a> to the built-in client's <a href="https://api.slack.com/methods/views.open"><code>views.open</code></a> method.

Your app receives <code>trigger_id</code>s in payloads sent to your Request URL triggered user invocation like a slash command, button press, or interaction with a select menu.

Read more about modal composition in the <a href="https://api.slack.com/block-kit/surfaces/modals#composing_modal">API documentation</a>.
</div>

```javascript
// Listen for a slash command invocation
app.command('/ticket', ({ ack, payload, context }) => {
// Acknowledge the command request
ack();

try {
const result = app.client.views.open({
token: context.botToken,
type: 'modal',
// Pass a valid trigger_id within 3 seconds of receiving it
trigger_id: payload.trigger_id,
// View payload
view: {
// View identifier
callback_id: 'view_1',
title: {
type: 'plain_text',
text: 'Modal title'
},
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'Welcome to a modal with _blocks_'
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Click me!'
},
action_id: 'button_abc'
}
},
{
type: 'input',
block_id: 'input_c',
label: {
type: 'plain_text',
text: 'What are your hopes and dreams?'
},
element: {
type: 'plain_text_input',
action_id: 'dreamy_input',
multiline: true
}
}
],
submit: {
type: 'plain_text',
text: 'Submit'
}
}
});
console.log(result);
}
catch (error) {
console.error(error);
}
});
```
61 changes: 61 additions & 0 deletions docs/_basic/updating_pushing_modals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: Updating and pushing views
lang: en
slug: updating-pushing-views
order: 10
---

<div class="section-content">
Modals contain a stack of views. When you call `views.open`, you add the root view to the modal. After the initial call, you can dynamically update a view by calling <a href="https://api.slack.com/methods/views.update">`views.update`</a>, or stack a new view on top of the root view by calling <a href="https://api.slack.com/methods/views.push">`views.push`</a>.

<strong><code>views.update</code></strong><br>
To update a view, you can use the built-in client to call <code>views.update</code> with the <code>view_id</code> that was generated when you opened the view, and the updated <code>blocks</code> array. If you're updating the view when a user interacts with an element inside of an existing view, the <code>view_id</code> will be available in the <code>body</code> of the request.

<strong><code>views.push</code></strong><br>
To push a new view onto the view stack, you can use the built-in client to call <code>views.push</code> with a valid <code>trigger_id</code> a new <a href="https://api.slack.com/reference/block-kit/views">view payload</a>. The arguments for `views.push` is the same as <a href="#creating-modals">opening modals</a>. After you open a modal, you may only push two additional views onto the view stack.

Learn more about updating and pushing views in our <a href="https://api.slack.com/block-kit/surfaces/modals#updating_views">API documentation</a>.
</div>

```javascript
// Listen for a button invocation with action_id `button_abc` (assume it's inside of a modal)
app.action('button_abc', ({ ack, body, context }) => {
// Acknowledge the button request
ack();

try {
const result = app.client.views.update({
token: context.botToken,
// Pass the view_id
view_id: body.view.id,
// View payload with updated blocks
view: {
// View identifier
callback_id: 'view_1',
title: {
type: 'plain_text',
text: 'Updated modal'
},
blocks: [
{
type: 'section',
text: {
type: 'plain_text',
text: 'You updated the modal!'
}
},
{
type: 'image',
image_url: 'https://media.giphy.com/media/SVZGEcYt7brkFUyU90/giphy.gif',
alt_text: 'Yay! The modal was updated'
}
]
}
});
console.log(result);
}
catch (error) {
console.error(error);
}
});
```
24 changes: 18 additions & 6 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
onlyEvents,
matchEventType,
matchMessage,
onlyViewSubmits,
matchCallbackId,
} from './middleware/builtin';
import { processMiddleware } from './middleware/process';
import { ConversationStore, conversationContext, MemoryStore } from './conversation-store';
Expand All @@ -22,6 +24,7 @@ import {
SlackCommandMiddlewareArgs,
SlackEventMiddlewareArgs,
SlackOptionsMiddlewareArgs,
SlackViewMiddlewareArgs,
SlackAction,
Context,
SayFn,
Expand Down Expand Up @@ -301,6 +304,12 @@ export default class App {
);
}

public view(callbackId: string | RegExp, ...listeners: Middleware<SlackViewMiddlewareArgs>[]): void {
this.listeners.push(
[onlyViewSubmits, matchCallbackId(callbackId), ...listeners] as Middleware<AnyMiddlewareArgs>[],
);
}

public error(errorHandler: ErrorHandler): void {
this.errorHandler = errorHandler;
}
Expand All @@ -312,10 +321,8 @@ export default class App {
// TODO: when generating errors (such as in the say utility) it may become useful to capture the current context,
// or even all of the args, as properties of the error. This would give error handling code some ability to deal
// with "finally" type error situations.

// Introspect the body to determine what type of incoming event is being handled, and any channel context
const { type, conversationId } = getTypeAndConversation(body);

// If the type could not be determined, warn and exit
if (type === undefined) {
this.logger.warn('Could not determine the type of an incoming event. No listeners will be called.');
Expand Down Expand Up @@ -363,11 +370,13 @@ export default class App {
payload:
(type === IncomingEventType.Event) ?
(bodyArg as SlackEventMiddlewareArgs['body']).event :
(type === IncomingEventType.ViewSubmitAction) ?
(bodyArg as SlackViewMiddlewareArgs['body']).view :
(type === IncomingEventType.Action &&
isBlockActionOrInteractiveMessageBody(bodyArg as SlackActionMiddlewareArgs['body'])) ?
(bodyArg as SlackActionMiddlewareArgs<BlockAction | InteractiveMessage>['body']).actions[0] :
(bodyArg as (
Exclude<AnyMiddlewareArgs, SlackEventMiddlewareArgs | SlackActionMiddlewareArgs> |
Exclude<AnyMiddlewareArgs, SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackViewMiddlewareArgs> |
SlackActionMiddlewareArgs<Exclude<SlackAction, BlockAction | InteractiveMessage>>
)['body']),
};
Expand All @@ -389,6 +398,9 @@ export default class App {
} else if (type === IncomingEventType.Options) {
const optionListenerArgs = listenerArgs as SlackOptionsMiddlewareArgs<OptionsSource>;
optionListenerArgs.options = optionListenerArgs.payload;
} else if (type === IncomingEventType.ViewSubmitAction) {
const viewListenerArgs = listenerArgs as SlackViewMiddlewareArgs;
viewListenerArgs.view = viewListenerArgs.payload;
}

// Set say() utility
Expand Down Expand Up @@ -465,11 +477,11 @@ function buildSource(
const source: AuthorizeSourceData = {
teamId:
((type === IncomingEventType.Event || type === IncomingEventType.Command) ? (body as (SlackEventMiddlewareArgs | SlackCommandMiddlewareArgs)['body']).team_id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs)['body']).team.id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewSubmitAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).team.id as string :
assertNever(type)),
enterpriseId:
((type === IncomingEventType.Event || type === IncomingEventType.Command) ? (body as (SlackEventMiddlewareArgs | SlackCommandMiddlewareArgs)['body']).enterprise_id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs)['body']).team.enterprise_id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewSubmitAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).team.enterprise_id as string :
undefined),
userId:
((type === IncomingEventType.Event) ?
Expand All @@ -478,7 +490,7 @@ function buildSource(
((body as SlackEventMiddlewareArgs['body']).event.channel !== undefined && (body as SlackEventMiddlewareArgs['body']).event.channel.creator !== undefined) ? (body as SlackEventMiddlewareArgs['body']).event.channel.creator as string :
((body as SlackEventMiddlewareArgs['body']).event.subteam !== undefined && (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by !== undefined) ? (body as SlackEventMiddlewareArgs['body']).event.subteam.created_by as string :
undefined) :
(type === IncomingEventType.Action || type === IncomingEventType.Options) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs)['body']).user.id as string :
(type === IncomingEventType.Action || type === IncomingEventType.Options || type === IncomingEventType.ViewSubmitAction) ? (body as (SlackActionMiddlewareArgs | SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs)['body']).user.id as string :
(type === IncomingEventType.Command) ? (body as SlackCommandMiddlewareArgs['body']).user_id as string :
undefined),
conversationId: channelId,
Expand Down
13 changes: 10 additions & 3 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum IncomingEventType {
Action,
Command,
Options,
ViewSubmitAction,
}

/**
Expand All @@ -40,16 +41,22 @@ export function getTypeAndConversation(body: any): { type?: IncomingEventType, c
};
}
if (body.name !== undefined || body.type === 'block_suggestion') {
const optionsBody = (body as SlackOptionsMiddlewareArgs<OptionsSource>['body']);
return {
type: IncomingEventType.Options,
conversationId:
(body as SlackOptionsMiddlewareArgs<OptionsSource>['body']).channel.id,
conversationId: optionsBody.channel ? optionsBody.channel.id : undefined,
};
}
if (body.actions !== undefined || body.type === 'dialog_submission' || body.type === 'message_action') {
const actionBody = (body as SlackActionMiddlewareArgs<SlackAction>['body']);
return {
type: IncomingEventType.Action,
conversationId: (body as SlackActionMiddlewareArgs<SlackAction>['body']).channel.id,
conversationId: actionBody.channel !== undefined ? actionBody.channel.id : undefined,
};
}
if (body.type === 'view_submission') {
return {
type: IncomingEventType.ViewSubmitAction,
};
}
return {};
Expand Down
Loading