2020/1/14

Backlog の全プロジェクトのタスクをカンバン風にして独自にステータス管理できるようにしてみた #craftcms

先日のカンバンの続きで。

Backlog でのタスクをカンバン風にして見れるようにしてみた。
Backlog でもカンバンの機能がベータで少しずつ展開されてくるらしいのだけど。

【リリース予告】Backlogで「カンバンボード」を使おう! 〜チームや個人の作業を一目で見やすく、わかりやすく〜 #Backlog新機能 | Backlogブログ
https://backlog.com/ja/blog/ka...

Backlog で見るときは基本プロジェクト単位になってしまって、ガントチャートとかであれば人ごととかにみれたような気もするけど、みやすいところにはなかったりするし。

あとは別のスペースにも登録されていたりするとそれらを串刺しては当然みれないからなぁとおもって、まずはそれらを一つの所にためていけるようなのを試しに作ってみようという感じで。

うまくいけば API や RSS とか提供してくれる別のサービスも合わせられるかもしれない。

完成イメージ

とりあえずチーム内部で誰がどのくらい持ってるかを見れるようなイメージで。

やり方は色々ありそうだけど、とりあえず出来るだけ簡単にやるということで、Craft CMS をつかって Issue の情報はエントリとして保持して、表示は CMS のテンプレートである程度やった。

普通にスクラッチで作ったほうが楽なのかもしれないけど、自分の手持ちのカード的に Craft CMS と GraphQL でやった。

Backlog API をつかって Craft CMS に取り込む

Backlog API をつかって Craft CMS にエントリとしてデータを取り込む。

API のエンドポイントはこんな感じだった。

https://hogehoge.backlog.com/api/v2/issues?apiKey=<$APIキー$>&assigneeId[]=<$担当者のID$>&statusId[]=1&statusId[]=2&statusId[]=3&count=100

ほんとは複数メンバーまとめて取り込めればいいなぁと思ったけど、件数が100件までらしいのでユーザーごとにAPIを叩くようにした。
100件以上取れる方法はあるのかな?

Craft CMS の Feed Me プラグインをインストールして、設定していく。

create と update を有効にしておいた。

ここはもう少し方法を考えたいところではあるけれど、Backlog 側で Close にした Issue を kanban 側でどうするかなぁと。

API のリクエストにいれていないから、クローズにした後は取れなくなるので kanban 上で浮くことになると思う。
ひとまず非公開になるようにしておくのがいいかなぁ。この辺は Backlog 上で担当を変えた時の話も近いところがありそうなのでもう少し考えてみる。

件数が100件超えた場合とかも似たような状況になるだろうし。

APIでクローズにした issue だけを取り出すのをつくってそれは1日一回とか1時間に一回とかで Craft CMS に取り込みつつ、エントリのステータスを非公開にするとかを走らせるとかでいいのかもしれないなぁ。

Issue の情報を取り込むフィールド設定

Issue の情報を取り込むフィールドの関連付けとしてはこんな感じで、タイトル、slug を連携させて slug をキーにしておく。

Issue の情報はそれぞれフィールドに入っていくようにしておく。

担当者のIDを使うかNameにするかが悩んだ。

現状はAPIをユーザーごとにしているので、担当者については固定名で決めうちにしてもいいのだが。

スペースによってIDやユーザー名が変わりそうなので、そこの設定をどこかに持たせないといけないだろうなぁ。。。

ユーザーごとのIssueを取り出す

各エントリ(issue)に board の情報を持たせているので、ユーザーごとのタスクを見る画面はそれぞれのユーザーでフィルタをするような感じでクエリにセットする、というだけ。

{% set workingEntries = craft.entries.kanbanBoard('_working').issueAssignee('mersy').orderBy('sortKey asc').all() %}
{% set pendingEntries = craft.entries.kanbanBoard('_pending').issueAssignee('mersy').orderBy('sortKey asc').all() %}
{% set reviewEntries = craft.entries.kanbanBoard('_review').issueAssignee('mersy').orderBy('sortKey asc').all() %}
{% set doneEntries = craft.entries.kanbanBoard('_done').issueAssignee('mersy').orderBy('sortKey asc').all() %}

{% set inboxEntries = craft.entries.kanbanBoard('_inbox').issueAssignee('mersy').orderBy('sortKey asc').all() %}
{% set inboxEntries = craft.entries.kanbanBoard(':empty:').issueAssignee('mersy').orderBy('sortKey asc').all()|merge(inboxEntries) %}

{# ベースレイアウトを extends #}
{% extends '_layout' %}

{# main ブロック #}
{% block main %}
{% endblock %}

ユーザーの情報を変数にするとかすればもう少しスマートにかける気がするけどとりあえずはこれで。

これで先日のにも書いたそれぞれの kanban-item を取り出せる。

boards  :[
    {
        'id' : '_inbox',
        'title'  : 'Inbox',
        'class' : 'info',
        'item'  : [
        {% for entry in inboxEntries %}
            {
                "id"      : "{{entry.id}}",
                "title"   : "{{entry.issueKey}}:{{entry.title}}",
                "username": "{{entry.issueAssignee}}",
                "sortOrder": "{{entry.sortKey}}",
                "issueKey": "{{entry.issueKey}}",
                "issueStatus": "{{entry.issueStatus}}",
                "issueDue": "{% if entry.issueDue %}{{entry.issueDue|date('Y-m-d')}}{% else %} no Due {% endif %}",
                {% if entry.issueDue %}"overDue": "{% if entry.issueDue < date() %}true{% else %}false{% endif %}",{% endif %}
                "issueURL": "{{entry.issueSiteURL}}{{entry.issueKey}}"
            }{% if not loop.last %},{%endif%}
        {% endfor %}
        ]
    },
    {
        'id' : '_working',
        'title'  : 'Working',
        'class' : 'success',
        'item'  : [
        {% for entry in workingEntries %}
            {
                "id"      : "{{entry.id}}",
                "title"   : "{{entry.issueKey}}:{{entry.title}}",
                "username": "{{entry.issueAssignee}}",
                "sortOrder": "{{entry.sortKey}}",
                "issueKey": "{{entry.issueKey}}",
                "issueStatus": "{{entry.issueStatus}}",
                "issueDue": "{% if entry.issueDue %}{{entry.issueDue|date('Y-m-d')}}{% else %} no Due {% endif %}",
                {% if entry.issueDue %}"overDue": "{% if entry.issueDue < date() %}true{% else %}false{% endif %}",{% endif %}
                "issueURL": "{{entry.issueSiteURL}}{{entry.issueKey}}"
            }{% if not loop.last %},{%endif%}
        {% endfor %}
        ]
    },
以下略

D&D した board の情報を保持するようにする

カンバンのどの board に表示するか?は Backlog の情報とは連携させず、あくまで独自に持たせる方向にしたので、item をD&Dで board をうつしたらその情報を上書きするようにする。

jKanban - Javascript plugin for Kanban boards
http://www.riccardotartaglia.i...

どのコールバックを使うのがベストか悩んだけど、とりあえず dropEl を使う感じで。

dropEl : function (el, target, source, sibling) {
    // issue のidを取り出す
    let issueID = el.dataset.eid;
    // issue をうつした先のboardID(ステータス)を取り出す
    let issueStatus = target.parentNode.dataset.id;

    changeStatus(issueID,issueStatus);

こんなかんじで、D&Dしたら changeStatus するようにして。

changeStatus はこんな感じで GraphQL で Mutation するようにした。

function changeStatus(issueID,issueStatus){
    axios({
    url: 'https://example.com/api',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer hogehoge'
    },
    method: 'POST',
    data: {
        query: `
            mutation($issueID:Int,$issueStatus:String) {
                upsertBitpartIssues(
                    id: $issueID
                    kanbanBoard: $issueStatus
                ) {
                    id
                    slug
                    kanbanBoard
                }
            }
        `,
        variables: {
            "issueID": issueID,
            "issueStatus": issueStatus
        }
    }
    }).then(function(response) {
    // success
    console.log(response);
    }).catch(function (error) {
    // error
    console.log(error);
    });
}

この辺は普通にフロントエンドだけでやる感じにするなら Vue.js でプロジェクト作って Apollo でどうこうやったほうが楽なのかもしれない。
イマイチまだこの辺とCMSのテンプレート周りの位置付けがしっくりいく感じに理解できていない。

CMS脳、、、古いってことなのかもしれないな。。。(スキル不足
Apollo を CDN 経由で使ってとかできるのかな。。。ちらっとみた気がするけどまだ試していない。
今回は @BUN が紹介してくれた axios 使う方法で。

query の中では渡ってきている変数を取り出せないので、

query: `
            mutation($issueID:Int,$issueStatus:String) {
                upsertBitpartIssues(
                    id: $issueID
                    kanbanBoard: $issueStatus
                ) {
                    id
                    slug
                    kanbanBoard
                }
            }
        `,
        variables: {
            "issueID": issueID,
            "issueStatus": issueStatus
        }

variables を経由して渡すのを忘れないようにする。
最初忘れててどうやるかなぁと考えてしまった。

並び順のデータを保持する

カンバンの board 内で並べ替えした時の並び順をどう持たせるのがいいかなぁ、、、、と思いつつ。
とりあえずこれもどの board にあるか?と同様に各エントリに持たせるようにした。

item を D&D したときと同じ dropEl のコールバックで。

この辺はもっといい書き方がありそうだけど、とりあえずググったりしながら動けばいいくらいで。
いずれレビューしてもらってリファクタリングしてもらおう・・・

sortIssues でD&Dした先のそれぞれのitemを取り出す。

let sortIssues = el.parentNode.childNodes;

var i = 1;
$(sortIssues).each(function(){
    $(this).attr('data-sortorder', i++);
    let sortOrder = $(this).attr('data-sortorder');
    let issueID = $(this).attr('data-eid');
    sortIssue(issueID,sortOrder);
});

一応 data 属性にも並び順の連番をセットしておきつつ sortIssue に渡す。

sortIssue はこんな感じで、 changeStatus とほぼほぼ同じ感じで。

function sortIssue(issueID,sortOrder){
    axios({
    url: 'https://example.com/api',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer hogehoge'
    },
    method: 'POST',
    data: {
        query: `
            mutation($issueID:Int,$sortKey:String) {
                upsertBitpartIssues(
                    id: $issueID
                    sortKey: $sortKey
                ) {
                    id
                    slug
                    sortKey
                }
            }
        `,
        variables: {
            "issueID": issueID,
            "sortKey": sortOrder
        }
    }
    }).then(function(response) {
    // success
    console.log(response);
    }).catch(function (error) {
    // error
    console.log(error);
    });
}

sortKey に board 内での順番を持たせるようにした。

作ってみて

ざっくりとはこんな感じで作ってみて、とりあえずいまいまは問題なさそうな雰囲気ではある。

これから Backlog 上で Issue たてたりステータス変えていったときにおかしくなるところがありそうだからその辺を直していく必要がありそう。

Inbox の board で並び替えしたのと、新規で追加されたのが混ざって表示されるのでわかりやすくする必要もありそう。
kanban 内のアイテム表示のフィルタとかがあっても良さそう。

別のスペースを追加してみての動作も試してみたい。
別のサービスもがっちゃんこしてみたいところ。

見せ方的に kanban にしたのはチケットの中でも優先順位をつけて依頼したい、優先度を見える化したいというのがあったからだけど、一覧でみたいということもあるだろうから、表形式の見せ方とかそういうのも用意できればいいんだろうなぁ。

タスク管理ツールとは違って、プロジェクトをまたいでチーム内での状況を見えるようにするのがとりあえずの目的なので、その辺は専用のツールには使い勝手はかなわないところはありそう。
まぁとりあえずはこれでみたいものは見れるかな、と。

今週 1/16の Craft CMS Meetup Tokyo vol.2 でこもりさんのセッションは Craft CMS + GraphQL 関係の話もありそうなのでどんなかんじなのかとか勉強させてもらおう。

Craft CMS Meetup Tokyo vol.2 Craft CMS の実際のところ | Meetup
https://www.meetup.com/ja-JP/J...

設計時点でどこまで見通せるか?

とりあえず、今回あーでもないこーでもないと試しながら作っていったのだけど。
以前、こんな感じのを作りたいというのをテキストベースにまとめたことはあるけど、設計とかドキュメントだけでまとめきれたかなぁという気がしている。

CMS を使ったWebサイト構築だとあまりない気がするのだけど、どっちかいうとプラグイン開発やシステム開発的なところで自分のスキル的に拾いきれないところが、明確にできないこととかはありそうな気がする。

作りながら見えてきて、都度判断していたところがあるから、チームでやるときにそういうところをどうしたものかな、、、というのがあった。
色々やりながら経験して学んでスキル身につけていかないとだな。。。

設計時点である程度の方向性は定められそうだけど、細かいところは実装しながらになるだろうし。
実装してるなかで検討事項が出てきそうなきがしたから、そういうところは都度チーム内で検討しながら進めていけるような体制・進め方にしておくのがいいんだろうなぁ。

作る側も設計の通りに作るだけではなくて、いい方法や効率的な手段がないか?とかは常に考えながら作っていく必要がありそうな気がした。