0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Asanaはタスク管理・プロジェクト管理ツールで、チームのタスクを整理・追跡するのに広く使われています。プリザンターで管理しているレコードをAsanaにも登録したいというケースがある場合、通常は手動でAsana側にタスクを作成する必要があります。

今回は、プリザンターの一覧画面と編集画面のナビバーに「Asanaに登録」ボタンを追加し、ボタンをクリックするとAsanaにタスクを自動登録する機能を、本体コードの改修なしに拡張機能だけで実装してみます。

一覧・編集どちらの画面も、サイト設定のコマンドボタン機能でボタンを追加します。

Asana APIの準備

連携用サービスアカウントの作成

個人アカウントのトークンではなく、連携専用のサービスアカウントを用意することをおすすめします。サービスアカウントにすることで以下のメリットがあります。

  • 担当者の異動・退職でトークンが無効になるリスクを避けられる
  • 連携に必要な最小限の権限だけを付与できる
  • アクセスログでシステム連携と個人操作を区別できる

Asanaの組織管理者がサービスアカウントを作成し、連携対象プロジェクトにメンバーとして招待しておきます。

Asanaのサービスアカウントは、API連携専用のアカウントとしてAPI利用規約で認められています。AWS AppFabricやServiceNowなどの公式連携ガイドでもサービスアカウント経由のトークン発行が正式な手順として紹介されています。ただし、複数の人間が1つのアカウントにログインして共有する使い方はユーザー利用規約で禁止されています。サービスアカウントはあくまでシステム連携専用とし、トークンの安全管理と最小権限の付与を徹底してください。

パーソナルアクセストークンの取得

サービスアカウントでAsanaにログインし、パーソナルアクセストークン(PAT)を発行します。

  1. https://app.asana.com/0/my-apps にアクセス
  2. 「Create new token」をクリック
  3. トークン名を入力して「Create token」をクリック
  4. 生成されたトークンをコピーして安全な場所に保管

プロジェクトGID・メンバーGIDの確認

タスク登録先のAsanaプロジェクトと担当者候補のGID(グローバルID)を確認します。

プロジェクトGID

AsanaのプロジェクトページURLに含まれています。

https://app.asana.com/0/{プロジェクトGID}/list

メンバーGID(user_gid)

担当者として割り当てるユーザのGIDは、Asana APIで取得できます。ワークスペースGIDはAsanaの管理コンソール画面のURLなどから確認できます。

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://app.asana.com/api/1.0/workspaces/{ワークスペースGID}/users?opt_fields=name,email"

レスポンスの gid がユーザのGIDです。

{
  "data": [
    { "gid": "1234567890123", "name": "田中太郎", "email": "tanaka@example.com" },
    { "gid": "1234567890124", "name": "鈴木花子", "email": "suzuki@example.com" }
  ]
}

ここで確認したプロジェクトGIDとメンバーGIDは、後述のコマンドボタンのスクリプトに設定します。

実装してみた

全体の流れ

ボタンのクリックで選択ダイアログを表示し、ユーザがプロジェクトと担当者を選んだ後に更新APIを ?asana=1&project=GID&assignee=GID というクエリパラメータ付きで呼び出します。拡張サーバスクリプトはこのパラメータの有無でAsana連携の実行を判断します。

拡張サーバスクリプト(Asana API呼び出し)

AfterUpdate の拡張サーバスクリプトを1つ追加します。context.QueryStrings.Data('asana') でクエリパラメータを確認し、Asana APIを呼び出します。プロジェクトGIDと担当者GIDはクエリパラメータから動的に受け取ります。

ExtendedServerScripts/AsanaPost.json
{
    "AfterUpdate": true,
    "Body": "-- Write an arbitrary javascript."
}
ExtendedServerScripts/AsanaPost.json.js
// ?asana=1 のときだけ処理
if (context.QueryStrings.Data('asana') !== '1') return;

var personalAccessToken = 'YOUR_SERVICE_ACCOUNT_ACCESS_TOKEN';

// プロジェクトGIDと担当者GIDをクエリパラメータから取得
var projectGid = context.QueryStrings.Data('project');
var assigneeGid = context.QueryStrings.Data('assignee');

if (!projectGid) return;

var taskData = {
    name: model.Title,
    notes: model.Body || '',
    projects: [projectGid]
};

// 担当者が指定されている場合のみ設定
if (assigneeGid) {
    taskData.assignee = assigneeGid;
}

var payload = JSON.stringify({ data: taskData });

httpClient.ResponseHeaders.Clear();
httpClient.RequestUri = 'https://app.asana.com/api/1.0/tasks';
httpClient.Content = payload;
httpClient.MediaType = 'application/json';
httpClient.RequestHeaders['Authorization'] = 'Bearer ' + personalAccessToken;
httpClient.Post();

このサーバスクリプトは更新APIから呼び出されるため、context.AddResponse によるメッセージ返却は使用できません(更新APIのレスポンスは {Id, StatusCode, Message} の固定形式です)。メッセージの表示はクライアント側のスクリプトで行います。

httpClient.ResponseHeaders.Clear() はリクエストの前に必ず呼び出してください。詳しくは「プリザンターのサーバスクリプトでhttpClientを使うときのお約束」を参照してください。

編集画面:コマンドボタンの追加

編集画面のボタンはサイト設定のコマンドボタン機能で追加します。拡張スクリプトや拡張サーバスクリプトを使わずに設定できます。

「テーブルの管理」→「エディタ」を開き、コマンドボタンの「追加」から以下を設定します。

項目 設定値
名称 Asanaに登録
アイコン (任意)
スクリプト 下記参照

スクリプト欄には以下のJavaScriptを設定します。スクリプト先頭の asanaProjectsasanaUsers に、前述のGID確認手順で取得した値を設定してください。

// --- Asana プロジェクト・担当者の設定 ---
var asanaProjects = [
    { gid: '1234567890123', name: 'プロジェクトA' },
    { gid: '1234567890124', name: 'プロジェクトB' }
];
var asanaUsers = [
    { gid: '1234567890125', name: '田中太郎' },
    { gid: '1234567890126', name: '鈴木花子' }
];
// --- 設定ここまで ---

var dialog = document.createElement('dialog');
dialog.style.cssText = 'padding:20px;border:1px solid #ccc;border-radius:8px;min-width:320px';
dialog.innerHTML = '<form method="dialog">'
    + '<h3 style="margin-top:0">Asanaにタスクを登録</h3>'
    + '<div style="margin-bottom:12px"><label>プロジェクト:</label><br>'
    + '<select id="asana-project" style="width:100%;padding:4px">'
    + asanaProjects.map(function (p) {
        return '<option value="' + p.gid + '">' + p.name + '</option>';
    }).join('') + '</select></div>'
    + '<div style="margin-bottom:16px"><label>担当者:</label><br>'
    + '<select id="asana-assignee" style="width:100%;padding:4px">'
    + '<option value="">(未指定)</option>'
    + asanaUsers.map(function (u) {
        return '<option value="' + u.gid + '">' + u.name + '</option>';
    }).join('') + '</select></div>'
    + '<div style="text-align:right">'
    + '<button type="button" onclick="this.closest(\'dialog\').close()"'
    + ' style="margin-right:8px">キャンセル</button>'
    + '<button type="submit" value="ok">登録</button></div></form>';
document.body.appendChild(dialog);
dialog.showModal();
dialog.addEventListener('close', function () {
    var ok = dialog.returnValue === 'ok';
    var projectGid = dialog.querySelector('#asana-project').value;
    var assigneeGid = dialog.querySelector('#asana-assignee').value;
    dialog.remove();
    if (!ok) return;
    var url = $p.apiUrl($p.getId(), 'update')
        + '?asana=1&project=' + projectGid;
    if (assigneeGid) url += '&assignee=' + assigneeGid;
    $.ajax({
        url: url,
        method: 'PUT',
        contentType: 'application/json',
        data: '{}',
        success: function () {
            $p.message('#Message', {
                Text: 'Asanaにタスクを登録しました',
                Css: 'alert-success'
            });
        },
        error: function () {
            $p.message('#Message', {
                Text: 'Asana登録に失敗しました',
                Css: 'alert-error'
            });
        }
    });
});

API URLの組み立てには $p.apiUrl(id, action) を使用しています。この関数は $('#ApplicationPath').val() を内部で参照するため、プリザンターがサブディレクトリ配下に配置されている環境でも正しいパスが生成されます。

一覧画面:コマンドボタンの追加

一覧画面のボタンも同じくコマンドボタン機能で追加します。

「テーブルの管理」→「一覧」を開き、コマンドボタンの「追加」から以下を設定します。

項目 設定値
名称 Asanaに登録
アイコン (任意)
スクリプト 下記参照

スクリプト欄には以下のJavaScriptを設定します。編集画面と同様に、先頭のプロジェクト・担当者設定を環境に合わせて変更してください。

// --- Asana プロジェクト・担当者の設定 ---
var asanaProjects = [
    { gid: '1234567890123', name: 'プロジェクトA' },
    { gid: '1234567890124', name: 'プロジェクトB' }
];
var asanaUsers = [
    { gid: '1234567890125', name: '田中太郎' },
    { gid: '1234567890126', name: '鈴木花子' }
];
// --- 設定ここまで ---

var checkedRows = $('.grid-check:checked');
if (checkedRows.length === 0) {
    $p.message('#Message', { Text: 'レコードをチェックしてください', Css: 'alert-warning' });
    return false;
}

var dialog = document.createElement('dialog');
dialog.style.cssText = 'padding:20px;border:1px solid #ccc;border-radius:8px;min-width:320px';
dialog.innerHTML = '<form method="dialog">'
    + '<h3 style="margin-top:0">Asanaにタスクを登録('
    + checkedRows.length + '件)</h3>'
    + '<div style="margin-bottom:12px"><label>プロジェクト:</label><br>'
    + '<select id="asana-project" style="width:100%;padding:4px">'
    + asanaProjects.map(function (p) {
        return '<option value="' + p.gid + '">' + p.name + '</option>';
    }).join('') + '</select></div>'
    + '<div style="margin-bottom:16px"><label>担当者:</label><br>'
    + '<select id="asana-assignee" style="width:100%;padding:4px">'
    + '<option value="">(未指定)</option>'
    + asanaUsers.map(function (u) {
        return '<option value="' + u.gid + '">' + u.name + '</option>';
    }).join('') + '</select></div>'
    + '<div style="text-align:right">'
    + '<button type="button" onclick="this.closest(\'dialog\').close()"'
    + ' style="margin-right:8px">キャンセル</button>'
    + '<button type="submit" value="ok">登録</button></div></form>';
document.body.appendChild(dialog);
dialog.showModal();
dialog.addEventListener('close', function () {
    var ok = dialog.returnValue === 'ok';
    var projectGid = dialog.querySelector('#asana-project').value;
    var assigneeGid = dialog.querySelector('#asana-assignee').value;
    dialog.remove();
    if (!ok) return;
    var processed = 0;
    var failed = 0;
    checkedRows.each(function () {
        var itemId = $(this).closest('tr').data('id');
        var url = $p.apiUrl(itemId, 'update')
            + '?asana=1&project=' + projectGid;
        if (assigneeGid) url += '&assignee=' + assigneeGid;
        $.ajax({
            url: url,
            method: 'PUT',
            contentType: 'application/json',
            data: '{}',
            error: function () { failed++; },
            complete: function () {
                processed++;
                if (processed === checkedRows.length) {
                    if (failed > 0) {
                        alert(failed + '件の登録に失敗しました');
                    }
                    location.reload();
                }
            }
        });
    });
});

チェックした行から data('id') でレコードIDを取得し、選択されたプロジェクト・担当者で更新APIを1件ずつ呼び出します。API URLは $p.apiUrl(itemId, 'update') で組み立てるため、サブディレクトリ配置でも正しく動作します。すべての処理が完了したら画面をリロードします。

URLに関する補足

スクリプト内でプリザンターのAPI URLを組み立てる場合は、パスの直書きを避け、プリザンターが提供するヘルパーを使用してください。

場面 スクリプト(クライアント側) サーバスクリプト
API URL $p.apiUrl(id, 'update') context.ApplicationPath + 'api/items/' + id + '/update'
ルートパス $('#ApplicationPath').val() context.ApplicationPath
コントローラーベース $('#BaseUrl').val() context.ApplicationPath + context.Controller + '/'

プリザンターがサブディレクトリ配下(例: https://example.com/pleasanter/)にデプロイされている場合、/api/items/... のように / 始まりで直接指定するとパスが一致しなくなります。$p.apiUrl()$('#ApplicationPath').val() を使えば ApplicationPath(例: /pleasanter/)が自動的に補完されるため、配置構成に依存しないスクリプトになります。

まとめ

今回はプリザンターからAsanaへのタスク連携機能を、拡張機能のみで実装しました。

  • 一覧・編集どちらの画面もサイト設定のコマンドボタン機能でボタンを追加
  • HTML <dialog> による選択ダイアログで、プロジェクトと担当者を動的に選択できるようにした
  • クエリパラメータ ?asana=1&project=GID&assignee=GID でサーバスクリプトのAsana登録処理をトリガー
  • APIトークンはサーバスクリプト(サーバ側)にのみ保存されるためブラウザに露出しない
  • 連携用サービスアカウントのトークンを使うことで、担当者変更の影響を受けない運用ができる
  • API URLの組み立てには $p.apiUrl() を使い、サブディレクトリ配置にも対応

同じ手法を応用すれば、JiraやBacklogなど他のタスク管理ツールへの連携も実装できます。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?