Slack 次世代プラットフォーム機能を少しずつ試す - 標準フォーム編

こんにちは、Slack の[公式 SDK 開発](https://github.com/slackapi)と日本の Developer Relations を担当している瀬良 (@seratch) と申します :wave:

この記事は [Slack の次世代プラットフォーム機能を少しずつ試しながら、ゆっくりと理解していくシリーズ](https://qiita.com/seratch/items/b1449132add003e61de6)の記事です。

「次世代プラットフォーム機能って何？」という方は、以下の記事で詳しく解説しましたので、まずはそちらをお読みください。

https://qiita.com/seratch/items/ecc16b845483c9d6f9e1

この記事では、標準ファンクションとして提供されている[フォーム（OpenForm）](https://api.slack.com/future/forms)の使い方をご紹介します。

### プロジェクトを作成

いつものようにブランクプロジェクトを作成してゼロからコードを足していきましょう。`slack create` コマンドを実行して、選択肢から「Blank Project」を選択してください。作成したプロジェクトの構成は以下の通りです。

```bash
$ tree
.
├── LICENSE
├── README.md
├── assets
│   └── default_new_app_icon.png
├── deno.jsonc
├── import_map.json
├── manifest.ts
└── slack.json
```

### ワークフローを追加

今回は標準ファンクションだけを使いますので `workflow.ts` というファイルだけを作成します。

```typescript
// ----------------
// ワークフロー定義
// ----------------

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
export const workflow = DefineWorkflow({
  callback_id: "form-demo-workflow",
  title: "OpenForm Demo Workflow",
  input_parameters: {
    properties: {
      interactivity: { type: Schema.slack.types.interactivity },
      user_id: { type: Schema.slack.types.user_id },
      channel_id: { type: Schema.slack.types.channel_id },
    },
    required: ["interactivity", "user_id", "channel_id"],
  },
});

// ここにフォームを使ったステップを追加
const formStep = workflow.addStep(Schema.slack.functions.OpenForm, {
  title: "Send a greeting",
  interactivity: workflow.inputs.interactivity,
  submit_label: "Send greeting",
  fields: {
    elements: [
      {
        name: "recipient",
        title: "Recipient",
        // users_select ブロックに変換される
        type: Schema.slack.types.user_id,
        default: workflow.inputs.user_id,
      },
      {
        name: "channel",
        title: "Channel to send message to",
        // channels_select ブロックに変換される
        type: Schema.slack.types.channel_id,
        default: workflow.inputs.channel_id,
      },
      {
        name: "message",
        title: "Message to recipient",
        // plain_text_input ブロックに変換される
        type: Schema.types.string,
        // multiline: true に変換される
        long: true,
      },
    ],
    required: ["recipient", "channel", "message"],
  },
});

// 正しく outputs を受け取った確認
workflow.addStep(Schema.slack.functions.SendEphemeralMessage, {
  user_id: formStep.outputs.fields.recipient, // name がキー名になる
  channel_id: formStep.outputs.fields.channel, // name がキー名になる
  message: "OpenForm's `outputs.fields`: ```" +
    formStep.outputs.fields +
    "```",
});

// ----------------
// トリガー定義
// ----------------

import { Trigger } from "deno-slack-api/types.ts";
const trigger: Trigger<typeof workflow.definition> = {
  type: "shortcut",
  name: "Form Demo Trigger",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {
    // interactivity は OpenForm を利用するためには必須
    // この記事投稿時点で interactivity を提供できるのはリンクトリガーのみ
    interactivity: { value: "{{data.interactivity}}" },
    // OpenForm を利用する上では必要ないが、受け取った内容の出力をするために利用
    user_id: { value: "{{data.user_id}}" },
    channel_id: { value: "{{data.channel_id}}" },
  },
};
export default trigger;
```

そして `manifest.ts` に今作成したワークフローを登録します。

```typescript
import { Manifest } from "deno-slack-sdk/mod.ts";
import { workflow as DemoWorkflow } from "./workflow.ts";

export default Manifest({
  name: "frosty-mink-263",
  description: "Demo workflow",
  icon: "assets/default_new_app_icon.png",
  workflows: [DemoWorkflow],
  outgoingDomains: [],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});
```

`slack run` でアプリを起動してみて、エラーが発生していないことを確認してください。

### リンクトリガーを作成してワークフローを試す

`slack triggers create --trigger-def ./workflow.ts` を実行して、リンクトリガーを作成して、それをチャンネル内のメッセージかブックマークとして共有してください。

実行すると以下のようにあなただけに見えるメッセージとしてフォームで受け取った情報を返してくれます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/6502af67-66fd-8a79-9002-877778cf97c3.gif" widht=500 />

### 基本的な利用法のまとめ

OpenForm の基本的な利用法のポイントは、

* リンクトリガーを使って `interactivity` をワークフローに渡す
* ワークフローに `Schema.slack.functions.OpenForm` ファンクションを追加
* OpenForm の設定項目は、[こちら](https://api.slack.com/future/forms)や[こちら](https://api.slack.com/future/forms#type-parameters)のページを参照のこと
* `OpenForm` を `workflow#addStep()` した戻り値を変数として保持、その変数の `outputs.fields` に送信された情報は含まれる

という点になります。

ここまで、とりあえず使い方は理解できたと思いますが、もう少し具体例をご紹介していきます。

### 組み込みの入力バリデーションを追加

先ほどのフォームでは、すべての項目を必須項目として設定していました。`"message"` を必須項目から外してみましょう。そのためには `required` として設定されている三つの要素から `"message"` を削除するだけです。

```typescript
    required: ["recipient", "channel"],
```

これで `"message"` に何も入力しなくても送信できるようになります。入力せずに送信した場合、空文字になるわけではなく `outputs.fields` にそのキー自体が存在しなくなりますので、ご注意ください。

次に「`"message"` は必須ではないが、入力するなら 5 文字以上 20 文字以下」という条件をつけてみましょう。以下のように設定することで　1-4 文字、21 文字以上の入力がされた状態ではフォームを送信できなくなります。

```typescript
      {
        name: "message",
        title: "Message to recipient",
        // plain_text_input ブロックに変換される
        type: Schema.types.string,
        // multiline: true に変換される
        long: true,
        minLength: 5,
        maxLength: 20,
      },
```

（まだあまり詳しい説明が書かれていませんが・・・ :bow: ）[こちらのページ](https://api.slack.com/future/forms#type-parameters)に利用可能な属性の一覧がありますので、ご参照ください。

### 組み込みのセレクトメニューの使い方

Block Kit でいうところの `static_select` のような機能は設定方法が若干異なりますので、具体例とともご紹介しておきます。

以下のような要素を追加するとセレクトメニューが生成されます。

```typescript
      {
        name: "favorite",  // 必須
        title: "Favorite",  // 必須
        type: Schema.types.string,  // 必須
        choices: [
          { value: "dog", title: "Dog" },
          { value: "cat", title: "Cat" },
        ],  // 必須
        enum: ["dog", "cat"],  // 必須
        default: "cat",  // 必須でない
      },
```

実際の見た目はこのようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/3ade84ac-93b2-3146-7b8c-67974d69e79f.png" width=500 />

注意点としては、（この記事投稿時点ではまだ機能として利用されていませんが） **`enum` は必須項目となっており、省略した場合、フリーテキストの plain_text_input ブロックにフォールバックされてしまいます**ので、必ず指定するようにしてください。

また、その他のセレクトメニューとしては、上のコード例でもすでに出てきていますが、`user_id` や `channel_id` といった type を指定すると自動的にユーザー一覧からの選択、チャンネル一覧からの選択に変換されるようになっています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/ab32b841-1fe0-5e4a-5f88-27be88f87c0d.png" width=500 />

また、複数選択には `Schema.types.array` とその要素の type を指定します。

```typescript
      {
        name: "people",
        title: "People",
        type: Schema.types.array,
        items: {
          type: Schema.slack.types.user_id,
        },
        default: [workflow.inputs.user_id, "USLACKBOT"],
      },
```

以下のような UI に変換されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/c5ae7c51-3f2e-f32b-de5e-59bfd6258cc2.png" width=500 />

ここまでで試した `fields.elements` をすべて含む完全なコードは以下の通りです。これを貼り付けて試してみてください。

```typescript
// ここにフォームを使ったステップを追加
const formStep = workflow.addStep(Schema.slack.functions.OpenForm, {
  title: "Send a greeting",
  interactivity: workflow.inputs.interactivity,
  submit_label: "Send greeting",
  fields: {
    elements: [
      {
        name: "recipient",
        title: "Recipient",
        // users_select ブロックに変換される
        type: Schema.slack.types.user_id,
        default: workflow.inputs.user_id,
      },
      {
        name: "channel",
        title: "Channel to send message to",
        // channels_select ブロックに変換される
        type: Schema.slack.types.channel_id,
        default: workflow.inputs.channel_id,
      },
      {
        name: "message",
        title: "Message to recipient",
        // plain_text_input ブロックに変換される
        type: Schema.types.string,
        // multiline: true に変換される
        long: true,
        minLength: 5,
        maxLength: 20,
      },
      {
        name: "favorite",
        title: "Favorite",
        // static_select ブロックに変換される
        type: Schema.types.string,
        choices: [
          { value: "dog", title: "Dog" },
          { value: "cat", title: "Cat" },
        ],
        enum: ["dog", "cat"], // 省略しないでください
        default: "cat",
      },
      {
        name: "people",
        title: "People",
        // multi_users_select ブロックに変換される
        type: Schema.types.array,
        items: {
          // ここの型を変えると他のブロックに変化する
          type: Schema.slack.types.user_id,
        },
        default: [workflow.inputs.user_id, "USLACKBOT"],
      },
    ],
    required: ["recipient", "channel"],
  },
});

// 正しく outputs を受け取った確認
workflow.addStep(Schema.slack.functions.SendEphemeralMessage, {
  user_id: formStep.outputs.fields.recipient, // name がキー名になる
  channel_id: formStep.outputs.fields.channel, // name がキー名になる
  message: "OpenForm's `outputs.fields`: ```" +
    formStep.outputs.fields +
    "```",
});
```

繰り返しとなりますが、指定可能な値については[こちらのドキュメント](https://api.slack.com/future/forms#type-parameters)をご参照ください。

### 終わりに

いかがだったでしょうか？制約はあるものの、この標準のフォームで、多くの用途はカバーできるのではないかと思います。特に既存の Block Kit では標準でサポートしてなかった最小値・最大値のバリデーションなどはよくあるユースケースだと思います。

より詳細な情報は、以下のページをご参照ください。

https://api.slack.com/future/forms

別の記事では、制約のない高度なモーダル利用についても解説する予定ですので、そちらの情報をお探しの方は少々お待ちください。

（追記）記事を公開しました。

https://qiita.com/seratch/items/3258a0d2b40d39f5435f

それでは！