JavaScript
jQuery
backlog
bookmarklet

Backlogのチケット本文とコメントに埋まっているチケットIDのステータスを調べるブックマークレット

JBUGのもくもく会で作ったブックマークレットを公開します。誰かの役に立ちますように。

これは何ですか?

ヌーラボさんのBacklogのチケットのステータスを調べるブックマークレットです。
今、ブラウザで開いているチケットの本文とコメントに書かれているチケットID らしきもの を拾い、ステータスを調べて、そのチケットのHTMLの中に表示します。

2018/6/14 変更点
/api/v2/projectsをコールして、APIキーのユーザにとって有効なプロジェクトキーを取ってきて、チケットIDを探すように修正しました。

2018/7/09 変更点
BacklogのAPIに実行し過ぎないように、非同期メソッドの状態を監視しながら次を呼び出すようにコードを書き換えました。

なぜ作ったのか?

自分の環境では、チケットの本文やチケットコメントに別のプロジェクトのチケットIDが貼られていることがあるのですが、チケットの状態を画面を開いて調べるのが面倒だったので、一発で表示できるようにしてみました。
(チケットにチケットIDを貼らない・書かない、親子チケット使おう、というのはいったん置いときました、とても変態的ソリューションです)

使っているもの

  • BacklogのAPI
  • jQuery
  • ブックマークレット

画面イメージ

本文の枠には、本文内のチケットとコメントのチケットのスタータスが表示されます。
コメントには、そのコメントに含まれているチケットのステータスが表示されます。

実行前の画面

チケット本体
image.png

コメント欄
image.png

実行後の画面

チケット本体
image.png

コメント欄
image.png

画面の表示は変わりますが、コメントを編集しているわけではないのです。再表示(F5)すると元の状態に戻ります。

作成したコード

javascript:(
    function(_backlog_url, _backlog_api_key, undefined){

        //sleep x seconds
        var api_interval_second=1;
        //list of project key. add by handle_project
        var projects = [];
        //list of issue key. add by handle_comnent
        var issues = [];

        console.log('jquery version is %s', $.fn.jquery);

        $('div#__backlog_status').remove();
        $('span.__backlog_status').remove();
        $('div#issueDescription').append('<div id="__backlog_status"></div>');

        // get project list from backlog
        console.log('-1. find project key.');
        dfp = backlog_projects();

        dfp.done( function(){
            current_issue = find_backlog_key(projects, window.location.href);
            console.log('current issue key = %s',current_issue);

            console.log('-2. process description of this issue')
            description = $('div.ticket__description').text();
            issues_in_desc = find_backlog_key(projects, description);
            for( i in issues_in_desc){
                target = {};
                target.issue_key=issues_in_desc[i];
                target.comment_id=0;
                issues.push(target);
            }

            console.log('-3. find %s comment', current_issue);
            dfc = backlog_comments(current_issue);

            console.log('-4. check latest status of all issues');

            dfc.done( function(){
                var dfi = wait(1);
                for( var i=0; i<issues.length;i++){
                    dfi = dfi.then( 
                        function(cnt){
                            return function(){
                                return $.when(backlog_issue(issues[cnt]), wait(api_interval_second));
                            }
                        }(i)
                    );
                }
                dfi.then( function(){console.log('Finish calling all comments')});
            });
        });

        console.log('End of Main thread.');

        function handle_project(param, backlog_result){
            for(p in backlog_result){
                projects.push(backlog_result[p].projectKey);
            }
            console.log('finish handle projects')
        }

        function handle_comment(param, backlog_result){
            for(c in backlog_result){
                issue_keys = find_backlog_key(projects, backlog_result[c].content);
                if( issue_keys.length > 0){
                    console.log(issue_keys);
                    for(i in issue_keys){
                        var target = {};
                        target.issue_key=issue_keys[i];
                        target.comment_id=backlog_result[c].id;
                        issues.push(target);
                    }
                }
            }
        }

        function handle_issue(param, backlog_result){
            console.log("key:%s id:%s status:%s", param.issue_key, param.comment_id, backlog_result.status.id);

            html = '<span class="__backlog_status status status--' + backlog_result.status.id + '">' + 
                backlog_result.status.name + '</span> ' + 
                param.issue_key + ' : ' + backlog_result.summary +'<br>';

            $('div#__backlog_status').append(html);
            if( param.comment_id !=0){
                $('div#comment-'+param.comment_id).append(html);
            }
        }

        function find_backlog_key(projects, s){
            console.debug(projects);
            var ans = [];
            for(p in projects){
                ptn = '(' + projects[p] + '-[0-9]+)';
                reg = new RegExp(ptn, 'g');
                while ((m = reg.exec(s)) != null) {
                    ans.push(m[1]);
                }
            }
            return ans;
        }

        function backlog_projects(){
            url = "https://"+_backlog_url+"/api/v2/projects?apiKey="+_backlog_api_key;
            params = {};
            return backlog_api(url,params, handle_project);
        }
        function backlog_comments(issue_key){
            url = "https://"+_backlog_url+"/api/v2/issues/"+issue_key+"/comments?apiKey="+_backlog_api_key;
            params = {};
            params.issue_key=issue_key;
            return backlog_api(url,params, handle_comment);
        }
        function backlog_issue(issue){
            url = "https://"+_backlog_url+"/api/v2/issues/"+issue.issue_key+"?apiKey="+_backlog_api_key;
            return backlog_api(url,issue, handle_issue);
        }

        function backlog_api(url, params, callback){
            var dfd = new $.Deferred();
            console.log('[BACKLOG] url=%s', url);

            $.ajax({
                type: "get",
                url: url,
                timeout: 20000,
                cache: false,
                async: true,
                data: {},
                dataType: 'json'
            })
            .done(function (response, textStatus, jqXHR) {

                console.log(response);
                if (response.status === "err") {
                    console.error("err: " + response.msg);
                } else {
                    callback(params, response);
                }
                //call resolve when finish both ajax and callback
                dfd.resolve();

            })
            .fail(function (jqXHR, textStatus, errorThrown) {
                console.log(textStatus);
                console.log(jqXHR);

                dummy = {};
                dummy.summary = '取得できませんでした';
                dummy.status = {};
                dummy.status.id = '1';
                dummy.status.name = '不明';

                callback(params, dummy);

                //call resolve not fail. continue to next issue
                dfd.resolve();
            })
            .always(function (data_or_jqXHR, textStatus, jqXHR_or_errorThrown) {
                //do nothing 
            });
            return dfd.promise();
        }

        function wait(sec) {
            var d = $.Deferred();
            setTimeout(function() {
                d.resolve();
            }, sec * 1000);
            return d.promise();
        }
    }
)("xxx.backlog.com","YOUR API KEY");

1

Closure Compiler後のコード

コンパイルしたコードは長いので消しました。
(最後の引数2つを自分の環境に合わせると動くはず。。) 2

手抜きしたところ

1) プロジェクトキーのマッチング

正規表現 [A-Z_]+-[0-9]+ でプロジェクトキーっぽいものを探してます。雑ですね。チケットのコメントにUTF-8って書いてあるとチケットIDと誤解します。

2018/6/14 ちゃんとやりました。

2) 本文(description)を取る部分

/api/v2/issuesで取り出す方法もありましたが、ajaxの非同期待ちが面倒だったので手元のHTMLから取り出しました。

3)チケットの並び順と重複

まったくプログラムしていないので、その日の気分で並ぶと思います。

注意事項

APIのキーは各自で取得してください。APIキーを発行したユーザが参照できないプロジェクトのステータスは表示できません。
APIのキーとURLは公開しないように気を付けてください。Backlogのデータが丸見えになります
BacklogのHTMLのclassやidが変わると動かなくなる部分がありますが、手直ししてください。
ご利用は自己責任でお願いします。

参考にした資料

https://developer.nulab-inc.com/ja/docs/backlog/


  1. コードはgithubに上げるようにしないと。 

  2. Closure Compilerする方法はこの辺参照