kintone Advent Calender 2019 Part2 12月22日
去年に続き、今年も参加させて頂きました。
さて、早速ですが今回は、kintone と Asana を連携しようと思います。
Asana ってなに?
https://asana.com/ja
チームや個人でプロジェクトのタスク管理をできるツールらしいです。
(とりあえず連携ネタを書こうと以前ちょこっとだけ触ったツールを選んだので、良く知らない。)
無料版触ってみた感想
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
↑にアクセスし、トークンを発行する。
一度しか表示されないので大事にメモしましょう。
注意文言がありますが、個人で使うだけなので問題なし。
kintone の設定
アプリ作成
アプリストアの To Do アプリを使います。
Asana 側の情報を保持するために、タスクID(taskID) という数値フィールドとプロジェクト名(projectName)という文字列一行フィールドを追加します。
カスタマイズファイル作成
以下のファイルを kintone アプリに適用します。
・kintone UI Component
・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;
}
});
})();
動作確認
まとめ
いろいろと改良点がありますが、とりあえずは Asana との連携が実装できました。
無料版だと「未完了のタスクを取得する」といったリクエストが書けず、全タスクを取得してインサートする、という処理になってしまったのが残念ポイントです。
タスクの詳細情報も1回のリクエストで取得できなかったので↓のように3回リクエストする必要がありました。
ワークスペース取得 -> タスク概要取得 -> タスク詳細取得
Premium access
https://developers.asana.com/docs/#search-tasks-in-a-workspace
この API でクエリとかが書けそう?
async await のエラーハンドリングわからん。
注意事項
cybozu developer network のコーディングガイドラインに記載されている通り、
本番環境ではちゃんとトークンは隠しましょう。
プラグイン化するなり、あとは OAuth に変更するなりが必要ですね。
この記事はあくまで動作確認 or 個人用ということで。
Asana の開発用環境とか取得できたら嬉しいなー。