diff --git a/docs/_basic/listening_modals.md b/docs/_basic/listening_modals.md
new file mode 100644
index 000000000..fb16fb216
--- /dev/null
+++ b/docs/_basic/listening_modals.md
@@ -0,0 +1,55 @@
+---
+title: Listening for view submissions
+lang: en
+slug: view_submissions
+order: 11
+---
+
+
+If a view payload contains any input blocks, you must listen to view_submission events to receive their values. To listen to view_submission events, you can use the built-in view() method.
+
+view() requires a callback_id of type string or RegExp.
+
+You can access the value of the input blocks by accessing the state object. state contains a values object that uses the block_id and unique action_id to store the input values.
+
+Read more about view submissions in our API documentation.
+
+
+```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);
+ }
+
+});
+```
diff --git a/docs/_basic/listening_responding_options.md b/docs/_basic/listening_responding_options.md
index f70c6929e..0099bae6d 100644
--- a/docs/_basic/listening_responding_options.md
+++ b/docs/_basic/listening_responding_options.md
@@ -2,7 +2,7 @@
title: Listening and responding to options
lang: en
slug: options
-order: 9
+order: 12
---
+Modals are focused surfaces that allow you to collect user data and display dynamic information. You can open a modal by passing a valid trigger_id and a view payload to the built-in client's views.open method.
+
+Your app receives trigger_ids 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 API documentation.
+
+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 `views.update`, or stack a new view on top of the root view by calling `views.push`.
+
+views.update
+To update a view, you can use the built-in client to call views.update with the view_id that was generated when you opened the view, and the updated blocks array. If you're updating the view when a user interacts with an element inside of an existing view, the view_id will be available in the body of the request.
+
+views.push
+To push a new view onto the view stack, you can use the built-in client to call views.push with a valid trigger_id a new view payload. The arguments for `views.push` is the same as opening modals. 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 API documentation.
+
+
+```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);
+ }
+});
+```
diff --git a/src/App.ts b/src/App.ts
index 4178ed037..1669d6c9e 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -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';
@@ -22,6 +24,7 @@ import {
SlackCommandMiddlewareArgs,
SlackEventMiddlewareArgs,
SlackOptionsMiddlewareArgs,
+ SlackViewMiddlewareArgs,
SlackAction,
Context,
SayFn,
@@ -301,6 +304,12 @@ export default class App {
);
}
+ public view(callbackId: string | RegExp, ...listeners: Middleware[]): void {
+ this.listeners.push(
+ [onlyViewSubmits, matchCallbackId(callbackId), ...listeners] as Middleware[],
+ );
+ }
+
public error(errorHandler: ErrorHandler): void {
this.errorHandler = errorHandler;
}
@@ -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.');
@@ -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['body']).actions[0] :
(bodyArg as (
- Exclude |
+ Exclude |
SlackActionMiddlewareArgs>
)['body']),
};
@@ -389,6 +398,9 @@ export default class App {
} else if (type === IncomingEventType.Options) {
const optionListenerArgs = listenerArgs as SlackOptionsMiddlewareArgs;
optionListenerArgs.options = optionListenerArgs.payload;
+ } else if (type === IncomingEventType.ViewSubmitAction) {
+ const viewListenerArgs = listenerArgs as SlackViewMiddlewareArgs;
+ viewListenerArgs.view = viewListenerArgs.payload;
}
// Set say() utility
@@ -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) ?
@@ -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,
diff --git a/src/helpers.ts b/src/helpers.ts
index cf25a3386..f427732f6 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -15,6 +15,7 @@ export enum IncomingEventType {
Action,
Command,
Options,
+ ViewSubmitAction,
}
/**
@@ -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['body']);
return {
type: IncomingEventType.Options,
- conversationId:
- (body as SlackOptionsMiddlewareArgs['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['body']);
return {
type: IncomingEventType.Action,
- conversationId: (body as SlackActionMiddlewareArgs['body']).channel.id,
+ conversationId: actionBody.channel !== undefined ? actionBody.channel.id : undefined,
+ };
+ }
+ if (body.type === 'view_submission') {
+ return {
+ type: IncomingEventType.ViewSubmitAction,
};
}
return {};
diff --git a/src/middleware/builtin.ts b/src/middleware/builtin.ts
index 0a6d90f3c..28458bda2 100644
--- a/src/middleware/builtin.ts
+++ b/src/middleware/builtin.ts
@@ -8,12 +8,15 @@ import {
SlackEvent,
SlackAction,
SlashCommand,
+ ViewSubmitAction,
OptionsRequest,
InteractiveMessage,
DialogSubmitAction,
MessageAction,
BlockElementAction,
ContextMissingPropertyError,
+ SlackViewMiddlewareArgs,
+ ViewOutput,
} from '../types';
import { ActionConstraints } from '../App';
import { ErrorCode, errorWithCode } from '../errors';
@@ -70,6 +73,47 @@ export const onlyEvents: Middleware
next();
};
+/**
+ * Middleware that filters out any event that isn't a view_submission
+ */
+export const onlyViewSubmits: Middleware = ({ view, next }) => {
+ // Filter out anything that isn't a view_submission
+ if (view === undefined) {
+ return;
+ }
+
+ // It matches so we should continue down this middleware listener chain
+ next();
+};
+
+// TODO: is there a way to consolidate the next two methods without too much type refactoring?
+export function matchCallbackId(
+ callbackId: string | RegExp,
+): Middleware {
+ return ({ payload, next, context }) => {
+ let tempMatches: RegExpMatchArray | null;
+
+ if (!isCallbackIdentifiedBody(payload)) {
+ return;
+ }
+ if (typeof callbackId === 'string') {
+ if (payload.callback_id !== callbackId) {
+ return;
+ }
+ } else {
+ tempMatches = payload.callback_id.match(callbackId);
+
+ if (tempMatches !== null) {
+ context['callbackIdMatches'] = tempMatches;
+ } else {
+ return;
+ }
+ }
+
+ next();
+ };
+}
+
/**
* Middleware that checks for matches given constraints
*/
@@ -292,11 +336,12 @@ function isBlockPayload(
type CallbackIdentifiedBody =
| InteractiveMessage
| DialogSubmitAction
+ | ViewOutput
| MessageAction
| OptionsRequest<'interactive_message' | 'dialog_suggestion'>;
function isCallbackIdentifiedBody(
- body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'],
+ body: SlackActionMiddlewareArgs['body'] | SlackOptionsMiddlewareArgs['body'] | SlackViewMiddlewareArgs['payload'],
): body is CallbackIdentifiedBody {
return (body as CallbackIdentifiedBody).callback_id !== undefined;
}
diff --git a/src/types/index.ts b/src/types/index.ts
index 2c11a8c20..2d05a8ded 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -4,4 +4,5 @@ export * from './actions';
export * from './command';
export * from './events';
export * from './options';
+export * from './view';
export * from './receiver';
diff --git a/src/types/middleware.ts b/src/types/middleware.ts
index 6741e838f..f6f195d00 100644
--- a/src/types/middleware.ts
+++ b/src/types/middleware.ts
@@ -3,10 +3,12 @@ import { SlackEventMiddlewareArgs } from './events';
import { SlackActionMiddlewareArgs } from './actions';
import { SlackCommandMiddlewareArgs } from './command';
import { SlackOptionsMiddlewareArgs } from './options';
+import { SlackViewMiddlewareArgs } from './view';
import { CodedError, ErrorCode } from '../errors';
export type AnyMiddlewareArgs =
- SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackCommandMiddlewareArgs | SlackOptionsMiddlewareArgs;
+ SlackEventMiddlewareArgs | SlackActionMiddlewareArgs | SlackCommandMiddlewareArgs |
+ SlackOptionsMiddlewareArgs | SlackViewMiddlewareArgs;
export interface PostProcessFn {
(error: Error | undefined, done: (error?: Error) => void): unknown;
diff --git a/src/types/options/index.ts b/src/types/options/index.ts
index 3c8741319..25e0503b4 100644
--- a/src/types/options/index.ts
+++ b/src/types/options/index.ts
@@ -27,7 +27,7 @@ export interface OptionsRequest