7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

kintone 2Advent Calendar 2019

Day 22

kintone × Asana

Last updated at Posted at 2019-12-21

kintone Advent Calender 2019 Part2 12月22日

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

Asana ってなに?

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

無料版触ってみた感想

  • リスト、ボード、タイムライン、カレンダー、などがあり、直感的にわかりやすい。※一部有料なのでデモ動画を拝見
    Image 11.png

  • 組織、チーム、プロジェクトと階層が分かれているので、タスクを構造的に把握しやすい。
    組織については、左のサイドメニュー上に表示されず、おそらくメールアドレスのドメインで決まっているっぽいです。
    Image 12.png

  • 自分がアサインされているタスク一覧が見辛い
    Image 10.png

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

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

連携概要

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

必要環境

Asana の設定

パーソナルアクセストークンの発行

https://app.asana.com/0/developer-console
↑にアクセスし、トークンを発行する。
一度しか表示されないので大事にメモしましょう。
Image 13.png

注意文言がありますが、個人で使うだけなので問題なし。

kintone の設定

アプリ作成

アプリストアの To Do アプリを使います。
Asana 側の情報を保持するために、タスクID(taskID) という数値フィールドとプロジェクト名(projectName)という文字列一行フィールドを追加します。
Image 16.png
Image 18.png

さらに、高度な設定で全体の桁数を30に変更しておきます。
Image 17.png

カスタマイズファイル作成

以下のファイルを kintone アプリに適用します。

kintone UI Component
・sample.js

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

まとめ

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

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

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

注意事項

cybozu developer network のコーディングガイドラインに記載されている通り、
本番環境ではちゃんとトークンは隠しましょう。
プラグイン化するなり、あとは OAuth に変更するなりが必要ですね。

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

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

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?