From 67a41b6eb41c376688d31e5f376ca990e3e427e3 Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Thu, 13 Aug 2020 10:13:05 -0700 Subject: [PATCH 1/9] WorkflowStep class + associated types --- src/App.ts | 12 ++ src/WorkflowStep.ts | 364 +++++++++++++++++++++++++++++++++++++ src/errors.ts | 6 + src/index.ts | 2 + src/types/actions/index.ts | 1 + src/types/middleware.ts | 3 +- src/types/view/index.ts | 23 ++- 7 files changed, 404 insertions(+), 7 deletions(-) create mode 100644 src/WorkflowStep.ts diff --git a/src/App.ts b/src/App.ts index 696dd2429..e342a6091 100644 --- a/src/App.ts +++ b/src/App.ts @@ -21,6 +21,7 @@ import { } from './middleware/builtin'; import { processMiddleware } from './middleware/process'; import { ConversationStore, conversationContext, MemoryStore } from './conversation-store'; +import { WorkflowStep } from './WorkflowStep'; import { Middleware, AnyMiddlewareArgs, @@ -303,6 +304,17 @@ export default class App { return this; } + /** + * Register WorkflowStep middleware + * + * @param workflowStep global workflow step middleware function + */ + public step(workflowStep: WorkflowStep): this { + const m = workflowStep.getMiddleware(); + this.middleware.push(m); + return this; + } + /** * Convenience method to call start on the receiver * diff --git a/src/WorkflowStep.ts b/src/WorkflowStep.ts new file mode 100644 index 000000000..30b3f04eb --- /dev/null +++ b/src/WorkflowStep.ts @@ -0,0 +1,364 @@ +import { WebAPICallResult } from '@slack/web-api'; +import { + Middleware, + AllMiddlewareArgs, + AnyMiddlewareArgs, + SlackActionMiddlewareArgs, + SlackViewMiddlewareArgs, + WorkflowStepEdit, + Context, + SlackEventMiddlewareArgs, + ViewWorkflowStepSubmitAction, + WorkflowStepExecuteEvent, +} from './types'; +import { processMiddleware } from './middleware/process'; +import { WorkflowStepInitializationError } from './errors'; + +/** Interfaces */ + +export interface StepConfigureArguments { + blocks: []; + callback_id?: string; + private_metadata?: string; + submit_disabled?: boolean; +} + +export interface StepUpdateArguments { + inputs?: {}; + outputs?: []; + step_name?: string; + step_image_url?: string; +} + +export interface StepCompleteArguments { + inputs?: { + [key: string]: { + value: string; + }; + }; + outputs?: { + type: string; + name: string; + label: string; + }[]; +} + +export interface StepFailArguments { + error: { + message: string; + }; +} + +export interface StepConfigureFn { + (config: StepConfigureArguments): Promise; +} + +export interface StepUpdateFn { + (config: StepUpdateArguments): Promise; +} + +export interface StepCompleteFn { + (config: StepCompleteArguments): Promise; +} + +export interface StepFailFn { + (config: StepFailArguments): Promise; +} + +interface WorkflowStepOptions { + edit: WorkflowStep['edit']; + save: WorkflowStep['save']; + execute: WorkflowStep['execute']; +} + +/** Types */ + +export type SlackWorkflowStepMiddlewareArgs = + | SlackActionMiddlewareArgs + | SlackViewMiddlewareArgs + | SlackEventMiddlewareArgs<'workflow_step_execute'>; + +type WorkflowStepMiddleware = + | Middleware>[] + | Middleware>[] + | Middleware>[]; + +type AllWorkflowStepMiddlewareArgs = T & + AllMiddlewareArgs & { + step: T extends SlackActionMiddlewareArgs + ? WorkflowStepEdit['workflow_step'] + : T extends SlackViewMiddlewareArgs + ? ViewWorkflowStepSubmitAction['workflow_step'] + : WorkflowStepExecuteEvent['workflow_step']; + configure?: StepConfigureFn; + update?: StepUpdateFn; + complete?: StepCompleteFn; + fail?: StepFailFn; + }; + +/** Class */ + +export class WorkflowStep { + /** Step callback_id */ + public callbackId: string; + + /** Step Add/Edit :: 'workflow_step_edit' action */ + public edit: Middleware>[]; + + /** Step Config Save :: 'view_submission' */ + public save: Middleware>[]; + + /** Step Executed/Run :: 'workflow_step_execute' event */ + public execute: Middleware>[]; + + constructor(callbackId: string, config: WorkflowStepOptions) { + validate(callbackId, config); + + const { save, edit, execute } = config; + + this.callbackId = callbackId; + this.save = Array.isArray(save) ? save : [save]; + this.edit = Array.isArray(edit) ? edit : [edit]; + this.execute = Array.isArray(execute) ? execute : [execute]; + } + + public getMiddleware(): Middleware { + return async (args): Promise => { + if (isStepEvent(args) && this.matchesConstraints(args)) { + return this.processEvent(args); + } + return args.next!(); + }; + } + + private matchesConstraints(args: SlackWorkflowStepMiddlewareArgs): boolean { + return args.payload.callback_id === this.callbackId; + } + + private async processEvent(args: AllWorkflowStepMiddlewareArgs): Promise { + const { payload } = args; + const stepArgs = prepareStepArgs(args); + const stepMiddleware = this.getStepMiddleware(payload); + return processStepMiddleware(stepArgs, stepMiddleware); + } + + private getStepMiddleware(payload: AllWorkflowStepMiddlewareArgs['payload']): WorkflowStepMiddleware { + switch (payload.type) { + case 'workflow_step_edit': + return this.edit; + case 'workflow_step': + return this.save; + case 'workflow_step_execute': + return this.execute; + default: + return []; // TODO :: throw error if it gets to this point? + } + } +} + +/** Helper Functions */ + +export function validate(callbackId: string, config: WorkflowStepOptions): void { + // Ensure callbackId is valid + if (typeof callbackId !== 'string') { + const errorMsg = 'WorkflowStep expects a callback_id as the first argument'; + throw new WorkflowStepInitializationError(errorMsg); + } + + // Ensure step config object is passed in + if (typeof config !== 'object') { + const errorMsg = 'WorkflowStep expects a configuration object as the second argument'; + throw new WorkflowStepInitializationError(errorMsg); + } + + // Check for missing required keys + const requiredKeys: (keyof WorkflowStepOptions)[] = ['save', 'edit', 'execute']; + const missingKeys: (keyof WorkflowStepOptions)[] = []; + requiredKeys.forEach((key) => { + if (config[key] === undefined) { + missingKeys.push(key); + } + }); + + if (missingKeys.length > 0) { + const errorMsg = `WorkflowStep is missing required keys: ${missingKeys.join(', ')}`; + throw new WorkflowStepInitializationError(errorMsg); + } + + // Ensure a callback or an array of callbacks is present + const requiredFns: (keyof WorkflowStepOptions)[] = ['save', 'edit', 'execute']; + requiredFns.forEach((fn) => { + if (typeof config[fn] !== 'function' && !Array.isArray(config[fn])) { + const errorMsg = `WorkflowStep ${fn} property must be a function or an array of functions`; + throw new WorkflowStepInitializationError(errorMsg); + } + }); +} + +/** + * `processStepMiddleware()` invokes each callback for lifecycle event + * @param args workflow_step_edit action + */ +export async function processStepMiddleware( + args: AllWorkflowStepMiddlewareArgs, + middleware: WorkflowStepMiddleware, +): Promise { + const { context, client, logger } = args; + // TODO :: revisit type used below (look into contravariance) + const callbacks = [...middleware] as Middleware[]; + const lastCallback = callbacks.pop(); + + if (lastCallback !== undefined) { + await processMiddleware(callbacks, args, context, client, logger, async () => + lastCallback({ ...args, context, client, logger }), + ); + } +} + +function isStepEvent(args: AnyMiddlewareArgs): args is AllWorkflowStepMiddlewareArgs { + const validTypes = new Set(['workflow_step_edit', 'workflow_step', 'workflow_step_execute']); + return validTypes.has(args.payload.type); +} + +function selectToken(context: Context): string | undefined { + return context.botToken !== undefined ? context.botToken : context.userToken; +} + +/** + * Factory for `configure()` utility + * @param args workflow_step_edit action + */ +function createStepConfigure( + args: AllWorkflowStepMiddlewareArgs>, +): StepConfigureFn { + const { + context, + client, + body: { callback_id, trigger_id }, + } = args; + const token = selectToken(context); + + return (config: Parameters[0]) => { + const { blocks, private_metadata } = config; + return client.views.open({ + token, + trigger_id, + view: { blocks, private_metadata, callback_id, type: 'workflow_step' }, + }); + }; +} + +/** + * Factory for `update()` utility + * @param args view_submission event + */ +function createStepUpdate( + args: AllWorkflowStepMiddlewareArgs>, +): StepUpdateFn { + const { + context, + client, + body: { + workflow_step: { workflow_step_edit_id }, + }, + } = args; + const token = selectToken(context); + + return (config: Parameters[0]) => { + const { step_name = '', step_image_url = '', inputs = {}, outputs = [] } = config; + return client.workflows.updateStep({ + token, + step_name, + step_image_url, + inputs, + outputs, + workflow_step_edit_id, + }); + }; +} + +/** + * Factory for `complete()` utility + * @param args workflow_step_execute event + */ +function createStepComplete( + args: AllWorkflowStepMiddlewareArgs>, +): StepCompleteFn { + const { + context, + client, + payload: { + workflow_step: { workflow_step_execute_id }, + }, + } = args; + const token = selectToken(context); + + return (config: Parameters[0]) => { + const { outputs = [] } = config; + return client.workflows.stepCompleted({ + token, + outputs, + workflow_step_execute_id, + }); + }; +} + +/** + * Factory for `fail()` utility + * @param args workflow_step_execute event + */ +function createStepFail( + args: AllWorkflowStepMiddlewareArgs>, +): StepFailFn { + const { + context, + client, + payload: { + workflow_step: { workflow_step_execute_id }, + }, + } = args; + const token = selectToken(context); + + return (config: Parameters[0]) => { + const { error } = config; + return client.workflows.stepFailed({ + token, + error, + workflow_step_execute_id, + }); + }; +} + +/** + * `prepareStepArgs()` takes in a workflow step's args and: + * 1. removes the next() passed in from App-level middleware processing + * - events will *not* continue down global middleware chain to subsequent listeners + * 2. augments args with step lifecycle-specific properties/utilities + * */ +export function prepareStepArgs( + args: SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs, +): AllWorkflowStepMiddlewareArgs { + const { next, ...stepArgs } = args; + // const preparedArgs: AllWorkflowStepMiddlewareArgs = { ...stepArgs }; // FIXME :: will take more work to get this proper + const preparedArgs: any = { ...stepArgs }; // TODO :: remove any + + switch (preparedArgs.payload.type) { + case 'workflow_step_edit': + preparedArgs.step = preparedArgs.action.workflow_step; + preparedArgs.configure = createStepConfigure(preparedArgs); + break; + case 'workflow_step': + preparedArgs.step = preparedArgs.body.workflow_step; + preparedArgs.update = createStepUpdate(preparedArgs); + break; + case 'workflow_step_execute': + preparedArgs.step = preparedArgs.event.workflow_step; + preparedArgs.complete = createStepComplete(preparedArgs); + preparedArgs.fail = createStepFail(preparedArgs); + break; + default: + break; + } + + return preparedArgs; +} diff --git a/src/errors.ts b/src/errors.ts index 9b4ea32d0..d45f7477c 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -19,6 +19,8 @@ export enum ErrorCode { * in terms of CodedError. */ UnknownError = 'slack_bolt_unknown_error', + + WorkflowStepInitializationError = 'slack_bolt_workflow_step_initialization_error', } export function asCodedError(error: CodedError | Error): CodedError { @@ -93,3 +95,7 @@ export class UnknownError extends Error implements CodedError { this.original = original; } } + +export class WorkflowStepInitializationError extends Error implements CodedError { + public code = ErrorCode.WorkflowStepInitializationError; +} diff --git a/src/index.ts b/src/index.ts index ed5c02935..3a83ffeb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,3 +22,5 @@ export * from './middleware/builtin'; export * from './types'; export { ConversationStore, MemoryStore } from './conversation-store'; + +export { WorkflowStep } from './WorkflowStep'; diff --git a/src/types/actions/index.ts b/src/types/actions/index.ts index 67c6ce536..879969648 100644 --- a/src/types/actions/index.ts +++ b/src/types/actions/index.ts @@ -1,6 +1,7 @@ export * from './block-action'; export * from './interactive-message'; export * from './dialog-action'; +export * from './workflow-step-edit'; import { BlockAction } from './block-action'; import { InteractiveMessage } from './interactive-message'; diff --git a/src/types/middleware.ts b/src/types/middleware.ts index 0e45f7146..7a05f1734 100644 --- a/src/types/middleware.ts +++ b/src/types/middleware.ts @@ -17,7 +17,8 @@ export type AnyMiddlewareArgs = | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs; -interface AllMiddlewareArgs { +// TODO :: decide if we really want to export this! +export interface AllMiddlewareArgs { context: Context; logger: Logger; client: WebClient; diff --git a/src/types/view/index.ts b/src/types/view/index.ts index efa19bef1..35e51e842 100644 --- a/src/types/view/index.ts +++ b/src/types/view/index.ts @@ -5,7 +5,7 @@ import { AckFn } from '../utilities'; /** * Known view action types */ -export type SlackViewAction = ViewSubmitAction | ViewClosedAction; +export type SlackViewAction = ViewSubmitAction | ViewClosedAction | ViewWorkflowStepSubmitAction; // /** * Arguments which listeners and middleware receive to process a view submission event from Slack. @@ -44,11 +44,6 @@ export interface ViewSubmitAction { view: ViewOutput; api_app_id: string; token: string; - workflow_step?: { - workflow_step_edit_id: string; - workflow_id: string; - step_id: string; - }; } /** @@ -80,6 +75,22 @@ export interface ViewClosedAction { }; } +/** + * A Slack view_submission Workflow Step event + * + * This describes the additional JSON-encoded body details for a step view_submission event + */ + +export interface ViewWorkflowStepSubmitAction extends ViewSubmitAction { + trigger_id: string; + response_urls: []; + workflow_step: { + workflow_step_edit_id: string; + workflow_id: string; + step_id: string; + }; +} + export interface ViewOutput { id: string; callback_id: string; From b97878a357019232175566b1707b04940cbdd90d Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Thu, 20 Aug 2020 17:08:11 -0700 Subject: [PATCH 2/9] add support for configure() > submit_disabled + external_id arg --- src/WorkflowStep.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/WorkflowStep.ts b/src/WorkflowStep.ts index 30b3f04eb..c96654baa 100644 --- a/src/WorkflowStep.ts +++ b/src/WorkflowStep.ts @@ -18,9 +18,9 @@ import { WorkflowStepInitializationError } from './errors'; export interface StepConfigureArguments { blocks: []; - callback_id?: string; private_metadata?: string; submit_disabled?: boolean; + external_id?: string; } export interface StepUpdateArguments { @@ -239,11 +239,21 @@ function createStepConfigure( const token = selectToken(context); return (config: Parameters[0]) => { - const { blocks, private_metadata } = config; + const { blocks, private_metadata, submit_disabled = false, external_id } = config; + const view = { callback_id, blocks, private_metadata, submit_disabled, type: 'workflow_step' }; + + if (external_id !== undefined) { + // TODO :: remove ignore when external_id is added to types > View + // @ts-ignore + view.external_id = external_id; + } + return client.views.open({ token, trigger_id, - view: { blocks, private_metadata, callback_id, type: 'workflow_step' }, + // TODO :: remove ignore when external_id is added to types > View + // @ts-ignore + view, }); }; } From 87a56125d74a5d0eaf0cdd903d06525c4deb2aa9 Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Thu, 20 Aug 2020 17:31:54 -0700 Subject: [PATCH 3/9] adjust configure() + update() to conditionally include user-provided props --- src/WorkflowStep.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/WorkflowStep.ts b/src/WorkflowStep.ts index c96654baa..5063e0d43 100644 --- a/src/WorkflowStep.ts +++ b/src/WorkflowStep.ts @@ -239,21 +239,21 @@ function createStepConfigure( const token = selectToken(context); return (config: Parameters[0]) => { - const { blocks, private_metadata, submit_disabled = false, external_id } = config; - const view = { callback_id, blocks, private_metadata, submit_disabled, type: 'workflow_step' }; - - if (external_id !== undefined) { - // TODO :: remove ignore when external_id is added to types > View - // @ts-ignore - view.external_id = external_id; - } - + const { blocks, private_metadata, submit_disabled, external_id } = config; return client.views.open({ token, trigger_id, // TODO :: remove ignore when external_id is added to types > View // @ts-ignore - view, + view: { + callback_id, + blocks, + type: 'workflow_step', + // only include private_metadata, submit_disabled + external_id if passed by user + ...(private_metadata !== undefined && { private_metadata }), + ...(submit_disabled !== undefined && { submit_disabled }), + ...(external_id !== undefined && { external_id }), + }, }); }; } @@ -275,14 +275,15 @@ function createStepUpdate( const token = selectToken(context); return (config: Parameters[0]) => { - const { step_name = '', step_image_url = '', inputs = {}, outputs = [] } = config; + const { step_name, step_image_url, inputs = {}, outputs = [] } = config; return client.workflows.updateStep({ token, - step_name, - step_image_url, + workflow_step_edit_id, inputs, outputs, - workflow_step_edit_id, + // only include step_name + step_image_url if passed by user + ...(step_name !== undefined && { step_name }), + ...(step_image_url !== undefined && { step_image_url }), }); }; } From 7f67ab8b6f4592e487f749639d02f89d45ab241c Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Sun, 23 Aug 2020 15:42:39 -0700 Subject: [PATCH 4/9] update workflow step documentation to reflect api changes --- docs/_steps/adding_editing_workflow_step.md | 68 ++++++++++++ docs/_steps/configuring_workflow_steps.md | 112 -------------------- docs/_steps/creating_workflow_step.md | 31 ++++++ docs/_steps/executing_workflow_steps.md | 41 ++++--- docs/_steps/saving_workflow_step.md | 59 +++++++++++ docs/_steps/workflow_steps_beta.md | 6 +- 6 files changed, 186 insertions(+), 131 deletions(-) create mode 100644 docs/_steps/adding_editing_workflow_step.md delete mode 100644 docs/_steps/configuring_workflow_steps.md create mode 100644 docs/_steps/creating_workflow_step.md create mode 100644 docs/_steps/saving_workflow_step.md diff --git a/docs/_steps/adding_editing_workflow_step.md b/docs/_steps/adding_editing_workflow_step.md new file mode 100644 index 000000000..5221225ac --- /dev/null +++ b/docs/_steps/adding_editing_workflow_step.md @@ -0,0 +1,68 @@ +--- +title: Adding or editing workflow steps +lang: en +slug: adding-editing-steps +order: 3 +beta: true +--- + +
+ +When a builder adds (or later edits) your step in their workflow, your app will receive a `workflow_step_edit` action. The callback assigned to the `edit` property of the `WorkflowStep` configuration object passed in during instantiation will run when this action occurs. + +Whether a builder is adding or editing a step, you need to provide them with a special `workflow_step` modal — a workflow step configuration modal — where step-specific settings are chosen. Since the purpose of this modal is tied to a workflow step's configuration, it has more restrictions than typical modals—most notably, you cannot include `title​`, `submit​`, or `close`​ properties in the payload. + +By default, the `callback_id` for this modal will be the same as that of the workflow step. For more advanced use cases, you can override this by setting an additional property on the `WorkflowStep` configuration object, `viewWorkflowStep`, with an alternative `callback_id`. + +Within the `edit` callback, the `configure()` utility can be used to easily open your step's configuration modal by passing in an object with your view's `blocks`. To disable configuration save before certain conditions are met, pass in `submit_disabled` with a value of `true`. + +To learn more about workflow step configuration modals, [read the documentation](https://api.slack.com/reference/workflows/configuration-view). + +
+ +```javascript +const ws = new WorkflowStep('add_task', { + edit: async ({ ack, step, configure }) => { + await ack(); + + const blocks = [ + { + 'type': 'input', + 'block_id': 'task_name_input', + 'element': { + 'type': 'plain_text_input', + 'action_id': 'name', + 'placeholder': { + 'type': 'plain_text', + 'text': 'Add a task name' + } + }, + 'label': { + 'type': 'plain_text', + 'text': 'Task name' + } + }, + { + 'type': 'input', + 'block_id': 'task_description_input', + 'element': { + 'type': 'plain_text_input', + 'action_id': 'description', + 'placeholder': { + 'type': 'plain_text', + 'text': 'Add a task description' + } + }, + 'label': { + 'type': 'plain_text', + 'text': 'Task description' + } + }, + ]; + + await configure({ blocks }); + }, + save: async ({ ack, step, update }) => {}, + execute: async ({ step, complete, fail }) => {}, +}); +``` \ No newline at end of file diff --git a/docs/_steps/configuring_workflow_steps.md b/docs/_steps/configuring_workflow_steps.md deleted file mode 100644 index 519d91848..000000000 --- a/docs/_steps/configuring_workflow_steps.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: Configuring and updating workflow steps -lang: en -slug: configuring-steps -order: 2 -beta: true ---- -{% raw %} -
-When a builder is adding your step to a new or existing workflow, your app will need to configure and update that step: - -**1. Listening for `workflow_step_edit` action** - -When a builder initially adds (or later edits) your step in their workflow, your app will receive a `workflow_step_edit` action. Your app can listen to `workflow_step_edit` using `action()` and the `callback_id` in your app's configuration. - -**2. Opening and listening to configuration modal** - -The `workflow_step_edit` action will contain a `trigger_id` which your app will use to call `views.open` to open a modal of type `workflow_step`. This configuration modal has more restrictions than typical modals—most notably you cannot include `title`, `submit`, or `close` properties in the payload. - -To learn more about configuration modals, [read the documentation](https://api.slack.com/workflows/steps#handle_config_view). - -Similar to other modals, your app can listen to this `view_submission` payload with the built-in [`views()` method](#view_submissions). - -**3. Updating the builder's workflow** - -After your app listens to the `view_submission`, you'll call [`workflows.updateStep`](https://api.slack.com/methods/workflows.updateStep) with the unique `workflow_step_id` (found in the `body`'s `workflow_step` object) to save the configuration for that builder's specific workflow. Two important parameters: -- `inputs` is an object with keyed child objects representing the data your app expects to receive from the user upon workflow step execution. You can include handlebar-style syntax (`{{ variable }}`) for variables that are collected earlier in a workflow. -- `outputs` is an array of objects indicating the data your app will provide upon workflow step completion. - -Read the documentation for [`input` objects](https://api.slack.com/reference/workflows/workflow_step#input) and [`output` objects](https://api.slack.com/reference/workflows/workflow_step#output) to learn more about how to structure these parameters. - -
-{% endraw %} - -```javascript -// Your app will be called when user adds your step to their workflow -app.action({ type: 'workflow_step_edit', callback_id: 'add_task' }, async ({ body, ack, client }) => { - // Acknowledge the event - ack(); - // Open the configuration modal using `views.open` - client.views.open({ - trigger_id: body.trigger_id, - view: { - type: 'workflow_step', - // callback_id to listen to view_submission - callback_id: 'add_task_config', - blocks: [ - // Input blocks will allow users to pass variables from a previous step to your's - { 'type': 'input', - 'block_id': 'task_name_input', - 'element': { - 'type': 'plain_text_input', - 'action_id': 'name', - 'placeholder': { - 'type': 'plain_text', - 'text': 'Add a task name' - } - }, - 'label': { - 'type': 'plain_text', - 'text': 'Task name' - } - }, - { 'type': 'input', - 'block_id': 'task_description_input', - 'element': { - 'type': 'plain_text_input', - 'action_id': 'description', - 'placeholder': { - 'type': 'plain_text', - 'text': 'Add a task description' - } - }, - 'label': { - 'type': 'plain_text', - 'text': 'Task description' - } - } - ] - } - }); -}); - -app.views('add_task_config', async ({ ack, view, body, client }) => { - // Acknowledge the submission - ack(); - // Unique workflow edit ID - let workflowEditId = body.workflow_step.workflow_step_edit_id; - // Input values found in the view's state object - let taskName = view.state.values.task_name_input.name; - let taskDescription = view.state.values.task_description_input.description; - - client.workflows.updateStep({ - workflow_step_edit_id: workflowEditId, - inputs: { - taskName: { value: (taskName || '') }, - taskDescription: { value: (taskDescription || '') } - }, - outputs: [ - { - name: 'taskName', - type: 'text', - label: 'Task name', - }, - { - name: 'taskDescription', - type: 'text', - label: 'Task description', - } - ] - }); -``` \ No newline at end of file diff --git a/docs/_steps/creating_workflow_step.md b/docs/_steps/creating_workflow_step.md new file mode 100644 index 000000000..55c7a90f9 --- /dev/null +++ b/docs/_steps/creating_workflow_step.md @@ -0,0 +1,31 @@ +--- +title: Creating a workflow step +lang: en +slug: creating-steps +order: 2 +beta: true +--- + +
+ +To create a new workflow step, Bolt provides the `WorkflowStep` class. + +When instantiating a new `WorkflowStep`, pass in the step's `callback_id`, which is defined in your app configuration, and a step configuration object. + +The configuration object for a `WorkflowStep` contains three properties: `edit`, `save`, and `execute`. Each of these properties must either hold a value of a single callback or an array of callbacks. All callbacks have access to a `step` object that contains information about the workflow step event, as well as one or more utility functions. + +After instantiating your workflow step, pass it the instance into `app.step()`. Behind the scenes, your app will listen and respond to the workflow step’s events using the callbacks provided in the configuration object. + +
+ +```javascript +const { WorkflowStep } = require('@slack/bolt'); + +const ws = new WorkflowStep('add_task', { + edit: async ({ ack, step, configure }) => {}, + save: async ({ ack, step, update }) => {}, + execute: async ({ step, complete, fail }) => {}, +}); + +app.step(ws); +``` \ No newline at end of file diff --git a/docs/_steps/executing_workflow_steps.md b/docs/_steps/executing_workflow_steps.md index 740c963d2..5bd96e6b9 100644 --- a/docs/_steps/executing_workflow_steps.md +++ b/docs/_steps/executing_workflow_steps.md @@ -2,30 +2,37 @@ title: Executing workflow steps lang: en slug: executing-steps -order: 3 +order: 5 beta: true ---
-When your workflow is executed by an end user, your app will receive a `workflow_step_execute` event. This event includes the user's `inputs` and a unique workflow execution ID. Your app must either call [`workflows.stepCompleted`](https://api.slack.com/methods/workflows.stepCompleted) with the `outputs` you specified in `workflows.updateStep`, or [`workflows.stepFailed`](https://api.slack.com/methods/workflows.stepFailed) to indicate the step failed. +When your workflow step is executed by an end user, your app will receive a `workflow_step_execute` event. The method assigned to the `execute` property of the `WorkflowStep` configuration object passed in during instantiation will run when this event occurs. + +Using the `inputs` from the configuration modal submission in the `save` callback, this is where we make third-party API calls, save things to a database, update the end user's Home Tab, and/or decide what outputs will be available to subsequent workflow steps by mapping values to the `outputs` object. + +Within the `execute` callback, your app must either call `complete()` to indicate that the step's execution was successful, or `fail()` to indicate that the step's execution failed. +
```javascript -app.event('workflow_step_execute', async ({ event, client }) => { - // Unique workflow edit ID - let workflowExecuteId = event.workflow_step.workflow_step_execute_id; - let inputs = event.workflow_step.inputs; - - client.workflows.stepCompleted({ - workflow_step_execute_id: workflowExecuteId, - outputs: { - taskName: inputs.taskName.value, - taskDescription: inputs.taskDescription.value - } - }); - - // You can do anything else you want here. Some ideas: - // Display results on the user's home tab, update your database, or send a message into a channel +const ws = new WorkflowStep('add_task', { + edit: async ({ ack, step, configure }) => {}, + save: async ({ ack, step, update }) => {}, + execute: async ({ step, complete, fail }) => { + const { inputs } = step; + + const outputs = { + taskName: inputs.taskName.value.value, + taskDescription: inputs.taskDescription.value.value, + }; + + // if everything was successful + await complete({ outputs }); + + // if something went wrong + // fail({ error: { message: "Just testing step failure!" } }); + }, }); ``` \ No newline at end of file diff --git a/docs/_steps/saving_workflow_step.md b/docs/_steps/saving_workflow_step.md new file mode 100644 index 000000000..5a137adbe --- /dev/null +++ b/docs/_steps/saving_workflow_step.md @@ -0,0 +1,59 @@ +--- +title: Saving the step configuration +lang: en +slug: saving-steps +order: 4 +beta: true +--- + +
+ +When the workflow step's configuration has been saved (using the step configuration modal from the `edit` callback), your app will listen for the `view_submission` event. The method assigned to the `save` property of the `WorkflowStep` configuration object passed in during instantiation will run when this event occurs. + +Once the configuration for the workflow step has been determined, builders often use that configuration to craft the custom outputs and behavior that occurs when the end user executes the step. + +Within the `save` callback, the `update()` method can be used to save the builder's step configuration by passing in the following arguments: `inputs`, `outputs`, `step_name` and `step_image_url`. + +`inputs` is an object representing the data your app expects to receive from the user upon workflow step execution. To use variables that were collected earlier in the workflow, you can include handlebar-style syntax (`{{ variable }}`). During the workflow step's execution, those variables will be replaced with their actual runtime value. + +`outputs` is an array of objects containing data that your app will provide upon the workflow step's completion. Outputs can then be used in subsequent steps of the workflow. + +`step_name` and `step_image_url` are available for a more customized look and feel of your workflow step. + +To learn more about how to structure these parameters, [read the documentation](https://api.slack.com/reference/workflows/workflow_step). + +
+ +```javascript +const ws = new WorkflowStep('add_task', { + edit: async ({ ack, step, configure }) => {}, + save: async ({ ack, step, update }) => { + await ack(); + + const { values } = view.state; + const taskName = values.task_name_input.name; + const taskDescription = values.task_description_input.description; + + const inputs = { + taskName: { value: taskName }, + taskDescription: { value: taskDescription } + }; + + const outputs = [ + { + type: 'text', + name: 'taskName', + label: 'Task name', + }, + { + type: 'text', + name: 'taskDescription', + label: 'Task description', + } + ]; + + await update({ inputs, outputs }); + }, + execute: async ({ step, complete, fail }) => {}, +}); +``` \ No newline at end of file diff --git a/docs/_steps/workflow_steps_beta.md b/docs/_steps/workflow_steps_beta.md index 63968f62e..991c9e189 100644 --- a/docs/_steps/workflow_steps_beta.md +++ b/docs/_steps/workflow_steps_beta.md @@ -7,7 +7,9 @@ beta: true ---
-⚠️ Workflow [steps from apps](https://api.slack.com/workflows/steps) is a beta feature. As the feature is developed, **Bolt for JavaScript's API will change to add better native support.** To develop with workflow steps in Bolt, use the `@slack/bolt@feat-workflow-steps` version of the package rather than the standard `@slack/bolt`. +Workflow Steps from apps allow your app to create and process custom workflow steps that users can add using [Workflow Builder](https://api.slack.com/workflows). -The [API documentation](https://api.slack.com/workflows/steps) includes more information on setting up a beta app. +A workflow step is made up of three distinct user events: workflow builders adding or editing the step, saving or updating the step's configuration, and the end user's execution of the step. All three events must be handled for a workflow step to function. + +The [API documentation](https://api.slack.com/workflows/steps) includes more information on setting up your app.
From 0fed4952f0dd030d8f0c9838e53e1b81f3b2c92f Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Mon, 24 Aug 2020 13:59:12 -0700 Subject: [PATCH 5/9] create ViewWorkflowStepClosedAction interface --- src/types/view/index.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/types/view/index.ts b/src/types/view/index.ts index 35e51e842..ba4b75353 100644 --- a/src/types/view/index.ts +++ b/src/types/view/index.ts @@ -5,7 +5,11 @@ import { AckFn } from '../utilities'; /** * Known view action types */ -export type SlackViewAction = ViewSubmitAction | ViewClosedAction | ViewWorkflowStepSubmitAction; +export type SlackViewAction = + | ViewSubmitAction + | ViewClosedAction + | ViewWorkflowStepSubmitAction + | ViewWorkflowStepClosedAction; // /** * Arguments which listeners and middleware receive to process a view submission event from Slack. @@ -78,7 +82,7 @@ export interface ViewClosedAction { /** * A Slack view_submission Workflow Step event * - * This describes the additional JSON-encoded body details for a step view_submission event + * This describes the additional JSON-encoded body details for a step's view_submission event */ export interface ViewWorkflowStepSubmitAction extends ViewSubmitAction { @@ -91,6 +95,19 @@ export interface ViewWorkflowStepSubmitAction extends ViewSubmitAction { }; } +/** + * A Slack view_closed Workflow Step event + * + * This describes the additional JSON-encoded body details for a step's view_closed event + */ +export interface ViewWorkflowStepClosedAction extends ViewClosedAction { + workflow_step: { + workflow_step_edit_id: string; + workflow_id: string; + step_id: string; + }; +} + export interface ViewOutput { id: string; callback_id: string; From dde364ce1947963bb5bff44389499dd0cc31edc3 Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Mon, 24 Aug 2020 19:31:41 -0700 Subject: [PATCH 6/9] WorkflowStep tests + adjust class types --- src/WorkflowStep.spec.ts | 241 +++++++++++++++++++++++++++++++++++++++ src/WorkflowStep.ts | 40 ++++--- src/types/middleware.ts | 1 - 3 files changed, 263 insertions(+), 19 deletions(-) create mode 100644 src/WorkflowStep.spec.ts diff --git a/src/WorkflowStep.spec.ts b/src/WorkflowStep.spec.ts new file mode 100644 index 000000000..51020a9aa --- /dev/null +++ b/src/WorkflowStep.spec.ts @@ -0,0 +1,241 @@ +import 'mocha'; +import { assert } from 'chai'; +import sinon from 'sinon'; +import rewiremock from 'rewiremock'; +import { + WorkflowStep, + SlackWorkflowStepMiddlewareArgs, + AllWorkflowStepMiddlewareArgs, + WorkflowStepMiddleware, + WorkflowStepOptions, +} from './WorkflowStep'; +import { Override } from './test-helpers'; +import { AllMiddlewareArgs, AnyMiddlewareArgs, WorkflowStepEdit, Middleware } from './types'; +import { WorkflowStepInitializationError } from './errors'; + +async function importWorkflowStep(overrides: Override = {}): Promise { + return rewiremock.module(() => import('./WorkflowStep'), overrides); +} + +const MOCK_FN = async () => { + return; +}; + +const MOCK_CONFIG_SINGLE = { + edit: MOCK_FN, + save: MOCK_FN, + execute: MOCK_FN, +}; + +const MOCK_CONFIG_MULTIPLE = { + edit: [MOCK_FN, MOCK_FN], + save: [MOCK_FN], + execute: [MOCK_FN, MOCK_FN, MOCK_FN], +}; + +describe('WorkflowStep', () => { + describe('constructor', () => { + it('should accept config as single functions', async () => { + const ws = new WorkflowStep('test_callback_id', MOCK_CONFIG_SINGLE); + assert.isNotNull(ws); + }); + + it('should accept config as multiple functions', async () => { + const ws = new WorkflowStep('test_callback_id', MOCK_CONFIG_MULTIPLE); + assert.isNotNull(ws); + }); + }); + + describe('validate', () => { + it('should throw an error if callback_id is not valid', async () => { + const { validate } = await importWorkflowStep(); + + // intentionally casting to string to trigger failure + const badId = {} as string; + const validationFn = () => validate(badId, MOCK_CONFIG_SINGLE); + + const expectedMsg = 'WorkflowStep expects a callback_id as the first argument'; + assert.throws(validationFn, WorkflowStepInitializationError, expectedMsg); + }); + + it('should throw an error if required keys are missing', async () => { + const { validate } = await importWorkflowStep(); + + // intentionally casting to WorkflowStepOptions to trigger failure + const badConfig = ({ + edit: async () => {}, + } as unknown) as WorkflowStepOptions; + + const validationFn = () => validate('callback_id', badConfig); + const expectedMsg = 'WorkflowStep is missing required keys: save, execute'; + assert.throws(validationFn, WorkflowStepInitializationError, expectedMsg); + }); + + it('should throw an error if lifecycle props are not a single callback or an array of callbacks', async () => { + const { validate } = await importWorkflowStep(); + + // intentionally casting to WorkflowStepOptions to trigger failure + const badConfig = ({ + edit: async () => {}, + save: {}, + execute: async () => {}, + } as unknown) as WorkflowStepOptions; + + const validationFn = () => validate('callback_id', badConfig); + const expectedMsg = 'WorkflowStep save property must be a function or an array of functions'; + assert.throws(validationFn, WorkflowStepInitializationError, expectedMsg); + }); + }); + + describe('isStepEvent', () => { + it('should return true if recognized workflow step payload type', async () => { + const fakeEditArgs = (createFakeStepEditAction() as unknown) as SlackWorkflowStepMiddlewareArgs & + AllMiddlewareArgs; + const fakeViewArgs = (createFakeStepViewEvent() as unknown) as SlackWorkflowStepMiddlewareArgs & + AllMiddlewareArgs; + const fakeExecuteArgs = (createFakeStepExecuteEvent() as unknown) as SlackWorkflowStepMiddlewareArgs & + AllMiddlewareArgs; + + const { isStepEvent } = await importWorkflowStep(); + + const editIsStepEvent = isStepEvent(fakeEditArgs); + const viewIsStepEvent = isStepEvent(fakeViewArgs); + const executeIsStepEvent = isStepEvent(fakeExecuteArgs); + + assert.isTrue(editIsStepEvent); + assert.isTrue(viewIsStepEvent); + assert.isTrue(executeIsStepEvent); + }); + + it('should return false if not a recognized workflow step payload type', async () => { + const fakeEditArgs = (createFakeStepEditAction() as unknown) as AnyMiddlewareArgs; + fakeEditArgs.payload.type = 'invalid_type'; + + const { isStepEvent } = await importWorkflowStep(); + const actionIsStepEvent = isStepEvent(fakeEditArgs); + + assert.isFalse(actionIsStepEvent); + }); + }); + + describe('prepareStepArgs', () => { + it('should remove next() from all original event args', async () => { + const fakeEditArgs = (createFakeStepEditAction() as unknown) as SlackWorkflowStepMiddlewareArgs & + AllMiddlewareArgs; + const fakeViewArgs = (createFakeStepViewEvent() as unknown) as SlackWorkflowStepMiddlewareArgs & + AllMiddlewareArgs; + const fakeExecuteArgs = (createFakeStepExecuteEvent() as unknown) as SlackWorkflowStepMiddlewareArgs & + AllMiddlewareArgs; + + const { prepareStepArgs } = await importWorkflowStep(); + + const editStepArgs = prepareStepArgs(fakeEditArgs); + const viewStepArgs = prepareStepArgs(fakeViewArgs); + const executeStepArgs = prepareStepArgs(fakeExecuteArgs); + + assert.notExists(editStepArgs.next); + assert.notExists(viewStepArgs.next); + assert.notExists(executeStepArgs.next); + }); + + it('should augment workflow_step_edit args with step and configure()', async () => { + const fakeArgs = (createFakeStepEditAction() as unknown) as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; + const { prepareStepArgs } = await importWorkflowStep(); + const stepArgs = prepareStepArgs(fakeArgs); + + assert.exists(stepArgs.step); + assert.exists(stepArgs.configure); + }); + + it('should augment view_submission with step and update()', async () => { + const fakeArgs = (createFakeStepViewEvent() as unknown) as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; + const { prepareStepArgs } = await importWorkflowStep(); + const stepArgs = prepareStepArgs(fakeArgs); + + assert.exists(stepArgs.step); + assert.exists(stepArgs.update); + }); + + it('should augment workflow_step_execute with step, complete() and fail()', async () => { + const fakeArgs = (createFakeStepExecuteEvent() as unknown) as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; + const { prepareStepArgs } = await importWorkflowStep(); + const stepArgs = prepareStepArgs(fakeArgs); + + assert.exists(stepArgs.step); + assert.exists(stepArgs.complete); + assert.exists(stepArgs.fail); + }); + }); + + describe('processStepMiddleware', () => { + it('should call each callback in user-provided middleware', async () => { + const { next, ...fakeArgs } = (createFakeStepEditAction() as unknown) as AllWorkflowStepMiddlewareArgs; + const { processStepMiddleware } = await importWorkflowStep(); + + const fn1 = sinon.spy((async ({ next }) => { + await next!(); + }) as Middleware); + const fn2 = sinon.spy(async () => {}); + const fakeMiddleware = [fn1, fn2] as WorkflowStepMiddleware; + + await processStepMiddleware(fakeArgs, fakeMiddleware); + + assert(fn1.called); + assert(fn2.called); + }); + }); +}); + +function createFakeStepEditAction() { + return { + body: { + callback_id: 'foo', + trigger_id: 'bar', + }, + payload: { + type: 'workflow_step_edit', + }, + action: { + workflow_step: {}, + }, + context: {}, + next: sinon.fake(), + }; +} + +function createFakeStepViewEvent() { + return { + body: { + callback_id: 'foo', + trigger_id: 'bar', + workflow_step: { + workflow_step_edit_id: '', + }, + }, + payload: { + type: 'workflow_step', + }, + context: {}, + next: sinon.fake(), + }; +} + +function createFakeStepExecuteEvent() { + return { + body: { + callback_id: 'foo', + trigger_id: 'bar', + }, + event: { + workflow_step: {}, + }, + payload: { + type: 'workflow_step_execute', + workflow_step: { + workflow_step_execute_id: '', + }, + }, + context: {}, + next: sinon.fake(), + }; +} diff --git a/src/WorkflowStep.ts b/src/WorkflowStep.ts index 5063e0d43..6f0f5c5e2 100644 --- a/src/WorkflowStep.ts +++ b/src/WorkflowStep.ts @@ -65,10 +65,10 @@ export interface StepFailFn { (config: StepFailArguments): Promise; } -interface WorkflowStepOptions { - edit: WorkflowStep['edit']; - save: WorkflowStep['save']; - execute: WorkflowStep['execute']; +export interface WorkflowStepOptions { + edit: WorkflowStepEditMiddleware | WorkflowStepEditMiddleware[]; + save: WorkflowStepSaveMiddleware | WorkflowStepSaveMiddleware[]; + execute: WorkflowStepExecuteMiddleware | WorkflowStepExecuteMiddleware[]; } /** Types */ @@ -78,12 +78,18 @@ export type SlackWorkflowStepMiddlewareArgs = | SlackViewMiddlewareArgs | SlackEventMiddlewareArgs<'workflow_step_execute'>; -type WorkflowStepMiddleware = - | Middleware>[] - | Middleware>[] - | Middleware>[]; +export type WorkflowStepEditMiddleware = Middleware>; +export type WorkflowStepSaveMiddleware = Middleware>; +export type WorkflowStepExecuteMiddleware = Middleware>; -type AllWorkflowStepMiddlewareArgs = T & +export type WorkflowStepMiddleware = + | WorkflowStepEditMiddleware[] + | WorkflowStepSaveMiddleware[] + | WorkflowStepExecuteMiddleware[]; + +export type AllWorkflowStepMiddlewareArgs< + T extends SlackWorkflowStepMiddlewareArgs = SlackWorkflowStepMiddlewareArgs +> = T & AllMiddlewareArgs & { step: T extends SlackActionMiddlewareArgs ? WorkflowStepEdit['workflow_step'] @@ -100,16 +106,16 @@ type AllWorkflowStepMiddlewareArgs>[]; + private edit: WorkflowStepEditMiddleware[]; /** Step Config Save :: 'view_submission' */ - public save: Middleware>[]; + private save: WorkflowStepSaveMiddleware[]; /** Step Executed/Run :: 'workflow_step_execute' event */ - public execute: Middleware>[]; + private execute: WorkflowStepExecuteMiddleware[]; constructor(callbackId: string, config: WorkflowStepOptions) { validate(callbackId, config); @@ -151,7 +157,7 @@ export class WorkflowStep { case 'workflow_step_execute': return this.execute; default: - return []; // TODO :: throw error if it gets to this point? + return []; } } } @@ -215,7 +221,7 @@ export async function processStepMiddleware( } } -function isStepEvent(args: AnyMiddlewareArgs): args is AllWorkflowStepMiddlewareArgs { +export function isStepEvent(args: AnyMiddlewareArgs): args is AllWorkflowStepMiddlewareArgs { const validTypes = new Set(['workflow_step_edit', 'workflow_step', 'workflow_step_execute']); return validTypes.has(args.payload.type); } @@ -243,8 +249,6 @@ function createStepConfigure( return client.views.open({ token, trigger_id, - // TODO :: remove ignore when external_id is added to types > View - // @ts-ignore view: { callback_id, blocks, @@ -350,7 +354,7 @@ export function prepareStepArgs( args: SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs, ): AllWorkflowStepMiddlewareArgs { const { next, ...stepArgs } = args; - // const preparedArgs: AllWorkflowStepMiddlewareArgs = { ...stepArgs }; // FIXME :: will take more work to get this proper + // const preparedArgs: AllWorkflowStepMiddlewareArgs = { ...stepArgs }; const preparedArgs: any = { ...stepArgs }; // TODO :: remove any switch (preparedArgs.payload.type) { diff --git a/src/types/middleware.ts b/src/types/middleware.ts index 7a05f1734..23ad74b5b 100644 --- a/src/types/middleware.ts +++ b/src/types/middleware.ts @@ -17,7 +17,6 @@ export type AnyMiddlewareArgs = | SlackViewMiddlewareArgs | SlackShortcutMiddlewareArgs; -// TODO :: decide if we really want to export this! export interface AllMiddlewareArgs { context: Context; logger: Logger; From a5c89c18456b35d34a337a91564ff5edd802ba1d Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Wed, 2 Sep 2020 12:18:03 -0700 Subject: [PATCH 7/9] remove doc reference to optional callback_id override --- docs/_steps/adding_editing_workflow_step.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/_steps/adding_editing_workflow_step.md b/docs/_steps/adding_editing_workflow_step.md index 5221225ac..acef1f33a 100644 --- a/docs/_steps/adding_editing_workflow_step.md +++ b/docs/_steps/adding_editing_workflow_step.md @@ -10,9 +10,7 @@ beta: true When a builder adds (or later edits) your step in their workflow, your app will receive a `workflow_step_edit` action. The callback assigned to the `edit` property of the `WorkflowStep` configuration object passed in during instantiation will run when this action occurs. -Whether a builder is adding or editing a step, you need to provide them with a special `workflow_step` modal — a workflow step configuration modal — where step-specific settings are chosen. Since the purpose of this modal is tied to a workflow step's configuration, it has more restrictions than typical modals—most notably, you cannot include `title​`, `submit​`, or `close`​ properties in the payload. - -By default, the `callback_id` for this modal will be the same as that of the workflow step. For more advanced use cases, you can override this by setting an additional property on the `WorkflowStep` configuration object, `viewWorkflowStep`, with an alternative `callback_id`. +Whether a builder is adding or editing a step, you need to provide them with a special `workflow_step` modal — a workflow step configuration modal — where step-specific settings are chosen. Since the purpose of this modal is tied to a workflow step's configuration, it has more restrictions than typical modals—most notably, you cannot include `title​`, `submit​`, or `close`​ properties in the payload. By default, the `callback_id` used for this modal will be the same as that of the workflow step. Within the `edit` callback, the `configure()` utility can be used to easily open your step's configuration modal by passing in an object with your view's `blocks`. To disable configuration save before certain conditions are met, pass in `submit_disabled` with a value of `true`. From d2b06903f21ec395a3340f504ec208cca58f6e2f Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Wed, 2 Sep 2020 12:20:42 -0700 Subject: [PATCH 8/9] adjust utility configs, add blocks type, make validTypes const --- src/WorkflowStep.ts | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/WorkflowStep.ts b/src/WorkflowStep.ts index 6f0f5c5e2..6c555ea71 100644 --- a/src/WorkflowStep.ts +++ b/src/WorkflowStep.ts @@ -1,4 +1,4 @@ -import { WebAPICallResult } from '@slack/web-api'; +import { WebAPICallResult, KnownBlock, Block } from '@slack/web-api'; import { Middleware, AllMiddlewareArgs, @@ -17,7 +17,7 @@ import { WorkflowStepInitializationError } from './errors'; /** Interfaces */ export interface StepConfigureArguments { - blocks: []; + blocks: (KnownBlock | Block)[]; private_metadata?: string; submit_disabled?: boolean; external_id?: string; @@ -102,6 +102,10 @@ export type AllWorkflowStepMiddlewareArgs< fail?: StepFailFn; }; +/** Constants */ + +const VALID_PAYLOAD_TYPES = new Set(['workflow_step_edit', 'workflow_step', 'workflow_step_execute']); + /** Class */ export class WorkflowStep { @@ -222,8 +226,7 @@ export async function processStepMiddleware( } export function isStepEvent(args: AnyMiddlewareArgs): args is AllWorkflowStepMiddlewareArgs { - const validTypes = new Set(['workflow_step_edit', 'workflow_step', 'workflow_step_execute']); - return validTypes.has(args.payload.type); + return VALID_PAYLOAD_TYPES.has(args.payload.type); } function selectToken(context: Context): string | undefined { @@ -245,18 +248,13 @@ function createStepConfigure( const token = selectToken(context); return (config: Parameters[0]) => { - const { blocks, private_metadata, submit_disabled, external_id } = config; return client.views.open({ token, trigger_id, view: { callback_id, - blocks, type: 'workflow_step', - // only include private_metadata, submit_disabled + external_id if passed by user - ...(private_metadata !== undefined && { private_metadata }), - ...(submit_disabled !== undefined && { submit_disabled }), - ...(external_id !== undefined && { external_id }), + ...config, }, }); }; @@ -278,16 +276,11 @@ function createStepUpdate( } = args; const token = selectToken(context); - return (config: Parameters[0]) => { - const { step_name, step_image_url, inputs = {}, outputs = [] } = config; + return (config: Parameters[0] = {}) => { return client.workflows.updateStep({ token, workflow_step_edit_id, - inputs, - outputs, - // only include step_name + step_image_url if passed by user - ...(step_name !== undefined && { step_name }), - ...(step_image_url !== undefined && { step_image_url }), + ...config, }); }; } @@ -308,12 +301,11 @@ function createStepComplete( } = args; const token = selectToken(context); - return (config: Parameters[0]) => { - const { outputs = [] } = config; + return (config: Parameters[0] = {}) => { return client.workflows.stepCompleted({ token, - outputs, workflow_step_execute_id, + ...config, }); }; } From 50286b692beb452ca2638c453a008f074ba1c7e9 Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Wed, 2 Sep 2020 18:38:39 -0700 Subject: [PATCH 9/9] bump version to reflect merge --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index defee440b..9dc361cfb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slack/bolt", - "version": "2.2.3-workflowStepsBeta.1", + "version": "2.3.0-workflowStepsBeta.1", "description": "A framework for building Slack apps, fast.", "author": "Slack Technologies, Inc.", "license": "MIT",