kintone × Asana

**kintone Advent Calender 2019 Part2 12月22日**

去年に続き、今年も参加させて頂きました。
さて、早速ですが今回は、kintone と Asana を連携しようと思います。

# Asana ってなに？
https://asana.com/ja
チームや個人でプロジェクトのタスク管理をできるツールらしいです。
（とりあえず連携ネタを書こうと以前ちょこっとだけ触ったツールを選んだので、良く知らない。）

# 無料版触ってみた感想

- リスト、ボード、タイムライン、カレンダー、などがあり、直感的にわかりやすい。※一部有料なのでデモ動画を拝見
![Image 11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/98b4a687-46f8-b4c5-5a9f-0feecdc9b02c.png)

- 組織、チーム、プロジェクトと階層が分かれているので、タスクを構造的に把握しやすい。 
組織については、左のサイドメニュー上に表示されず、おそらくメールアドレスのドメインで決まっているっぽいです。
![Image 12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/a89434e2-41b4-56ec-304e-b822f0de3b6e.png)

- 自分がアサインされているタスク一覧が見辛い
![Image 10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/3c105a86-3368-b24b-6624-7195e4047b75.png)

kintone は標準の一覧がリッチではないですが、Asana は豊富に用意されていたので、
プロジェクト全体のタスク確認をするのはとても便利そうです。
ただ、自分が持っているタスク一覧は見辛い印象でした。

わかりやすくメリットデメリットを上げたので、↓の用途で連携できないか試してみます。
Asana -> プロジェクトの全体把握
kintone -> 個人のタスク管理

# 連携概要
kintone から Asana のタスクを取得し、ステータスが完了になったら Asana 側のタスクも完了する

# 必要環境
- kintone 開発環境  
開発者ライセンス  
https://developer.cybozu.io/hc/ja/articles/200720464#step4
- Asana  
無料トライアル  
https://asana.com/ja/?utm_source=app.asana.com&utm_campaign=app.asana.com#trial 

# Asana の設定
### パーソナルアクセストークンの発行
https://app.asana.com/0/developer-console
↑にアクセスし、トークンを発行する。
一度しか表示されないので大事にメモしましょう。
![Image 13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/4caf74bc-3be2-f316-9973-0d10def2fc32.png)

注意文言がありますが、個人で使うだけなので問題なし。
# kintone の設定
### アプリ作成
アプリストアの To Do アプリを使います。
Asana 側の情報を保持するために、タスクID(taskID) という数値フィールドとプロジェクト名(projectName)という文字列一行フィールドを追加します。
![Image 16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/4d4b0397-8068-3b49-ceb9-76e43a3786b3.png)
![Image 18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/93afdcaa-ca1b-c1eb-6536-f8e7c9ca21cc.png)


さらに、高度な設定で全体の桁数を30に変更しておきます。
![Image 17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/e124d700-1c28-fd87-add7-e96e3e1e08ee.png)

### カスタマイズファイル作成
以下のファイルを kintone アプリに適用します。

・[kintone UI Component](https://github.com/kintone/kintone-ui-component)
・sample.js

```javascript:sample.js
(() => {

  const ASANA_TOKEN = 'xxxx'; // 先ほどメモしたパーソナルアクセストークン
  const APP_ID = kintone.app.getId();
  const ASANA_API_BASE_URL = 'https://app.asana.com/api/1.0';
  const ASANA_ID = 'xxxx'; // Asana のログインID
  const HEADER = {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': `Bearer ${ASANA_TOKEN}`
  };

  const GET_TASKS_BUTTON = new kintoneUIComponent.Button({
    text: 'タスクを取得する',
    type: 'submit'
  });
  const SPINNER = new kintoneUIComponent.Spinner({});

  /**
   * Asana API 実行用
   * @param {string} urlPath パス
   * @param {string} method メソッド
   * @param {object} header ヘッダー
   * @param {object} body ボディ
   * @returns {Promise} kintone.proxy()
   */
  const runAsanaApi = function(urlPath, method, header, body) {
    return kintone.proxy(ASANA_API_BASE_URL + urlPath, method, header, body);
  };

  /**
   * Asana のタスクを取得する関数
   * @returns {Promise} タスク
   */
  const fetchAsanaTasks = async function() {
    try {
      // ワークスペースすべて取得
      const worksaceResp = await runAsanaApi('/workspaces', 'GET', HEADER, {});
      if (worksaceResp[1] !== 200) throw worksaceResp;
      const tasksPromises = JSON.parse(worksaceResp[0]).data.map(val => {
        return runAsanaApi(`/tasks?workspace=${val.gid}&assignee=${ASANA_ID}`, 'GET', HEADER, {});
      });

      // タスクすべて取得
      const tasksResp = await kintone.Promise.all(tasksPromises);
      if (tasksResp[0][1] !== 200) throw tasksResp;
      const tasksDetailPromises = JSON.parse(tasksResp[0][0]).data.map(val => {
        return runAsanaApi(`/tasks/${val.gid}`, 'GET', HEADER, {});
      });

      // タスク詳細取得
      const detailResp = await kintone.Promise.all(tasksDetailPromises);
      if (detailResp[0][1] !== 200) throw detailResp;
      return detailResp.map(task => {
        return JSON.parse(task[0]).data;
      });
    } catch (err) {
      console.log(err);
      return new Error('Asana のタスク取得に失敗しました。');
    }
  };

  /**
   * タスクから kintone にインサートする関数
   * @param {Object} tasks Asana のタスク
   */
  const insertKintoneRecord = function(tasks) {
    const promises = tasks.map(async task => {
      const query = `taskID = "${task.gid}"`;
      const body = {
        app: APP_ID,
        query: query
      };

      const getRecordsResp = await kintone.api(kintone.api.url('/k/v1/records'), 'GET', body);
      if (!getRecordsResp.records.length) {
        const postBody = {
          app: APP_ID,
          record: {
            To_Do: {
              value: task.name,
            },
            taskID: {
              value: task.gid
            },
            Assignees: {
              value: [
                {code: kintone.getLoginUser().code}
              ]
            },
            Duedate: {
              value: task.due_on
            },
            projectName: {
              value: task.projects[0].name
            }
          }
        };
        return kintone.api(kintone.api.url('/k/v1/record'), 'POST', postBody);
      }
      return kintone.Promise.resolve('新規タスクなし');
    });
    return kintone.Promise.all(promises);
  };

  /**
   * レコード情報から Asana タスクを更新する
   * @param {Object} opt_record kintone イベントオブジェクトのレコード
   */
  const updateAsanaTask = function(opt_record) {
    const record = opt_record;
    const body = {
      data: {
        completed: true
      }
    };
    return runAsanaApi(`/tasks/${record.taskID.value}`, 'PUT', HEADER, body);
  };

  document.getElementsByTagName('BODY')[0].appendChild(SPINNER.render());

  GET_TASKS_BUTTON.on('click', async () => {
    SPINNER.show();
    try {
      // タスク取得して kintone に登録
      const tasks = await fetchAsanaTasks();
      await insertKintoneRecord(tasks);
      SPINNER.hide();
      location.reload();
    } catch (err) {
      SPINNER.hide();
      window.alert(err);
    }
  });

  kintone.events.on('app.record.index.show', event => {
    kintone.app.getHeaderMenuSpaceElement().appendChild(GET_TASKS_BUTTON.render());
    return event;
  });

  kintone.events.on('app.record.detail.process.proceed', async event => {
    if (event.nextStatus.value !== '完了') return event;
    SPINNER.show();
    try {
      const resp = await updateAsanaTask(event.record);
      if (resp[1] !== 200) throw resp;
      SPINNER.hide();
      return event;
    } catch (err) {
      event.error = 'Asana タスク更新失敗しました。';
      SPINNER.hide();
      return event;
    }
  });
})();

```

# 動作確認
![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/3acb2242-c68d-4c8e-026e-ae48c7469b54.gif)

# まとめ

いろいろと改良点がありますが、とりあえずは Asana との連携が実装できました。
無料版だと「未完了のタスクを取得する」といったリクエストが書けず、全タスクを取得してインサートする、という処理になってしまったのが残念ポイントです。
タスクの詳細情報も1回のリクエストで取得できなかったので↓のように3回リクエストする必要がありました。
ワークスペース取得 -> タスク概要取得 -> タスク詳細取得

Premium access 
https://developers.asana.com/docs/#search-tasks-in-a-workspace
この API でクエリとかが書けそう？

async await のエラーハンドリングわからん。

# 注意事項
[cybozu developer network のコーディングガイドライン](https://developer.cybozu.io/hc/ja/articles/201919400#step8)に記載されている通り、
本番環境ではちゃんとトークンは隠しましょう。
プラグイン化するなり、あとは OAuth に変更するなりが必要ですね。

この記事はあくまで動作確認 or 個人用ということで。

Asana の開発用環境とか取得できたら嬉しいなー。