Slack 下一代平台 - 高级模态
作者:互联网
创建一个空白项目
当你开始一个新项目时,你可以运行slack create
命令。在本教程中,您将从头开始构建一个应用程序。所以从列表中选择“空白项目”:
$ slack create
? Select a template to build from:
Hello World
A simple workflow that sends a greeting
Scaffolded project
A solid foundational project that uses a Slack datastore
> Blank project
A, well.. blank project
To see all available samples, visit github.com/slack-samples.
生成项目后,让我们检查slack run
命令是否正常运行。此命令将新应用程序的“开发”版本安装到连接的 Slack 工作区中。现在您的应用程序的机器人用户位于工作区中,并且您的应用程序具有用于 API 调用的机器人令牌。
$ cd sharp-chipmunk-480
$ slack run
? Choose a workspace seratch T03E94MJU
App is not installed to this workspace
Updating dev app install for workspace "Acme Corp"
⚠️ Outgoing domains
No allowed outgoing domains are configured
If your function makes network requests, you will need to allow the outgoing domains
Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog
✨ seratch of Acme Corp
Connected, awaiting events
如果您看到Connected, awaiting events
日志消息,则应用已成功连接到 Slack。您可以按“Ctrl + C”来终止本地应用程序进程。
定义工作流和触发器
让我们从定义一个简单的演示工作流及其链接触发器开始。一如既往,将源代码保存为workflow_and_trigger.ts
:
// ----------------
// Workflow Definition
// ----------------
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
export const workflow = DefineWorkflow({
callback_id: "modal-demo-workflow",
title: "Modal Demo Workflow",
input_parameters: {
properties: { interactivity: { type: Schema.slack.types.interactivity } },
required: ["interactivity"],
},
});
// Add your custom function to open and handle a modal
import { def as ModalDemo } from "./function.ts";
workflow.addStep(ModalDemo, {
interactivity: workflow.inputs.interactivity,
});
// ----------------
// Trigger Definition
// ----------------
import { Trigger } from "deno-slack-api/types.ts";
const trigger: Trigger<typeof workflow.definition> = {
type: "shortcut", // link trigger
name: "Modal Demo Trigger",
workflow: `#/workflows/${workflow.definition.callback_id}`,
inputs: {
// Modal interactions require `interactivity` input parameter.
// As of this writing, only link triggers can provide the value.
interactivity: { value: "{{data.interactivity}}" },
},
};
export default trigger;
由于您还function.ts
没有,编译应该会失败。让我们添加以下源代码function.ts
:
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { FunctionSourceFile } from "https://deno.land/x/deno_slack_source_file_resolver@0.1.5/mod.ts";
export const def = DefineFunction({
callback_id: "modal-example",
title: "Modal interaction example",
source_file: FunctionSourceFile(import.meta.url),
input_parameters: {
properties: { interactivity: { type: Schema.slack.types.interactivity } },
required: ["interactivity"],
},
output_parameters: { properties: {}, required: [] },
});
export default SlackFunction(
def,
// ---------------------------
// The first handler function that opens a modal.
// This function can be called when the workflow executes the function step.
// ---------------------------
async ({ inputs, client }) => {
// Open a new modal with the end-user who interacted with the link trigger
const response = await client.views.open({
interactivity_pointer: inputs.interactivity.interactivity_pointer,
view: {
"type": "modal",
// Note that this ID can be used for dispatching view submissions and view closed events.
"callback_id": "first-page",
// This option is required to be notified when this modal is closed by the user
"notify_on_close": true,
"title": { "type": "plain_text", "text": "My App" },
"submit": { "type": "plain_text", "text": "Next" },
"close": { "type": "plain_text", "text": "Close" },
"blocks": [
{
"type": "input",
"block_id": "first_text",
"element": { "type": "plain_text_input", "action_id": "action" },
"label": { "type": "plain_text", "text": "First" },
},
],
},
});
if (response.error) {
const error =
`Failed to open a modal in the demo workflow. Contact the app maintainers with the following information - (error: ${response.error})`;
return { error };
}
return {
// To continue with this interaction, return false for the completion
completed: false,
};
},
)
// ---------------------------
// The handler that can be called when the above modal data is submitted.
// It saves the inputs from the first page as private_metadata,
// and then displays the second-page modal view.
// ---------------------------
.addViewSubmissionHandler(["first-page"], ({ view }) => {
// Extract the input values from the view data
const firstText = view.state.values.first_text.action.value;
// Input validations
if (firstText.length < 5) {
return {
response_action: "errors",
// The key must be a valid block_id in the blocks on a modal
errors: { first_text: "Must be 5 characters or longer" },
};
}
// Successful. Update the modal with the second page presentation
return {
response_action: "update",
view: {
"type": "modal",
"callback_id": "second-page",
// This option is required to be notified when this modal is closed by the user
"notify_on_close": true,
"title": { "type": "plain_text", "text": "My App" },
"submit": { "type": "plain_text", "text": "Next" },
"close": { "type": "plain_text", "text": "Close" },
// Hidden string data, which is not visible to end-users
// You can use this property to transfer the state of interaction
// to the following event handlers.
// (Up to 3,000 characters allowed)
"private_metadata": JSON.stringify({ firstText }),
"blocks": [
// Display the inputs from "first-page" modal view
{
"type": "section",
"text": { "type": "mrkdwn", "text": `First: ${firstText}` },
},
// New input block to receive text
{
"type": "input",
"block_id": "second_text",
"element": { "type": "plain_text_input", "action_id": "action" },
"label": { "type": "plain_text", "text": "Second" },
},
],
},
};
})
// ---------------------------
// The handler that can be called when the second modal data is submitted.
// It displays the completion page view with the inputs from
// the first and second pages.
// ---------------------------
.addViewSubmissionHandler(["second-page"], ({ view }) => {
// Extract the first-page inputs from private_metadata
const { firstText } = JSON.parse(view.private_metadata!);
// Extract the second-page inputs from the view data
const secondText = view.state.values.second_text.action.value;
// Displays the third page, which tells the completion of the interaction
return {
response_action: "update",
view: {
"type": "modal",
"callback_id": "completion",
// This option is required to be notified when this modal is closed by the user
"notify_on_close": true,
"title": { "type": "plain_text", "text": "My App" },
// This modal no longer accepts further inputs.
// So, the "Submit" button is intentionally removed from the view.
"close": { "type": "plain_text", "text": "Close" },
// Display the two inputs
"blocks": [
{
"type": "section",
"text": { "type": "mrkdwn", "text": `First: ${firstText}` },
},
{
"type": "section",
"text": { "type": "mrkdwn", "text": `Second: ${secondText}` },
},
],
},
};
})
// ---------------------------
// The handler that can be called when the second modal data is closed.
// If your app runs some resource-intensive operations on the backend side,
// you can cancel the ongoing process and/or tell the end-user
// what to do next in DM and so on.
// ---------------------------
.addViewClosedHandler(
["first-page", "second-page", "completion"],
({ view }) => {
console.log(`view_closed handler called: ${JSON.stringify(view)}`);
return { completed: true };
},
);
稍后我会解释细节,但关键点是:
- 第一个处理程序为最终用户打开一个模式
addViewSubmissionHandler()
使用处理程序注册 + 模态的调度模态数据提交事件callback_id
addViewClosedHandler()
使用的处理程序注册 + 模态的调度模态关闭事件