はじめに
view customize pluginに関するTweetを漁っていたら興味深い呟きを見つけたので反応した。
これは今ハマっているview customize pluginで実現できそうな予感。
子チケットをクローズする。
↓
親チケットを参照し、それに紐づく子チケットを全て取得する。
↓
取得した子チケットが全てクローズしてたら親チケットをクローズする。
で良いのかな? https://t.co/h5E4FBKIhm— BEKO (@nkwtnb) January 16, 2019
どうやら2年ほど前から要望はあるものの、実装の気配はない状況のみたい。
確かに子チケットが全て終了したら親チケットも終了したい。需要は自分にもある。
view customize pluginはjQueryが利用でき、Redmine APIも呼び出せる = チケットに対する操作が可能。
というわけでやってみた。
完成イメージ
下記のチケット構成を想定。
親チケット ┣子チケット1 ┗子チケット2
子チケット1を終了した時に、子チケット2も終了していたら、親チケットも終了する。
(ダイアログで”OK”クリック時のみ)
環境
各バージョン等は下記の通り。
Redmine
3.4.stable
view customize plugin
1.2.2
設定内容
設定は下記の通りです。
Path pattern
チケットの詳細画面を対象にするパターン
/issues/[0-9]+
Type
JavaScript
Code
コードは下記の通り。
Promiseをたくさん使った。
コールバック地獄にはならないように気をつけたけど、もう少しキレイに書けそうな気がする。。。
// RedmineのAPIキー(適宜、値を変更する) const API_KEY = 'abcde-foo-bar'; /** * チケットを更新する */ const putIssue = function(id) { return new Promise(function(resolve, reject) { $.ajax({ type: 'PUT', url: '/issues/' + id + '.json', headers: { 'X-Redmine-API-Key': API_KEY }, dataType: 'text', data: JSON.stringify({ "issue": { // 「終了」を意味するステータスのID "status_id": "5" } }), contentType: 'application/json', }).done(function(resp) { resolve(resp['issue']); }).fail(function(jqXHR, textStatus, errorThrown){ console.log(jqXHR); console.log(textStatus); console.log(errorThrown); reject(errorThrown); }); }); } /** * チケットを取得する */ const getIssue = function(id) { return new Promise(function(resolve, reject) { $.ajax({ type: 'GET', url: '/issues/' + id + '.json?include=children', headers: { 'X-Redmine-API-Key': API_KEY }, contentType: 'application/json', }).done(function(resp) { resolve(resp['issue']); }).fail(function(error) { console.log(error); reject(error); }); }); }; /** * 同じ親チケットに紐づくチケット(兄弟チケット)を取得する */ const getSiblingsIssue = function(id) { return getIssue(id).then(function(parent) { if (parent['children']) { return parent['children']; } else { return Promise.reject(); } }) .catch(function(error) { console.log(error); }); }; $(function() { // URLの最後のチケット番号を取得 const URL = location.href; const ISSUE_ID = URL.substr(URL.lastIndexOf("/")+1); // submitクリック時 $('#issue-form').submit(function() { var issue = null; // 表示中チケットの情報保持用 var status = $('#issue_status_id').val(); // チケットステータスが「終了」の時 if(status === "5") { getIssue(ISSUE_ID).then(function(respIssue) { issue = respIssue; if (issue['parent']) { return getSiblingsIssue(issue['parent']['id']); } else { return Promise.reject(); } }) .then(function(siblings){ const promises = []; siblings.forEach(function(sibling) { promises.push(getIssue(sibling['id'])); }); return Promise.all(promises); }) .then(function(siblingIssues) { var finished = true; // 取得した兄弟チケット全てが「終了」か判定 siblingIssues.forEach(function(siblingIssue) { if (siblingIssue['status']['id'] !== 5) { finished = false; } }); if (finished) { if(confirm("親チケットも終了しますか?")) { return putIssue(issue['parent']['id']); } else { return Promise.resolve(); } } }) .then(function(resp) { $('#issue-form').off('submit'); $('#issue-form').submit(); }) .catch(function(error) { console.log(error); }); return false; } }); });
ポイント、というか詰まった所1:statusの形式がGET時とPUT時で異なる。
※GET/PUTで形式が違うのは普通の事なのかな?ちょっと経験不足で不明。。。
GET時のレスポンスは、
"issue" : { "id": "5", "name": "終了" }
上記の形式で返却されるのに対し、PUT時は、
"issue": { "status_id": "5" }
この形式でリクエストする。
ポイント、というか詰まった所2:$ajax()のレスポンスが200(成功)でもfailに入ってくる
Redmineのチケット更新APIが成功(ステータスコード:200)で帰ってきているのに、fail()として処理されていた。
これは、$ajax()はレスポンスをjsonにパースする際にエラーとなっていた為。
というのもチケット更新APIの成功時のレスポンスはnullだから。
調べると幾つかブログやstackoverflowがHITし、同様の事象に苦しんだ人が居る事がわかる。
記載された解消方法は、
リクエスト時に、dataType: 'json' を指定しなければ、 レスポンスの型をよしなに判定してくれる。
というものだったが、今回は解消されなかった。
これは恐らくjQueryのバージョンの違いによるものと思われる。
現在のjQueryの最新バージョンは公式サイトによると3.3.1。
対してRedmineに組み込まれているjQueryは1.11.1。(下記キャプチャ、下から2段目)
1.11.1以降のバージョンで、dataTypeが指定されない場合に自動判定されるようになったのではないか、と推測。
で、どのように対応したかというとjsonにパースされたくないので、リクエスト時に
dataType: 'text'
を指定する事でdone()に入ってくるようになった。
おわりに
今回は色々学ぶことが多かった。特に$ajax()のレスポンスの辺り・・・。
コードについてはES6で書くともう少しきれいに書けるのかな?と思う。
同じ要領で関連チケットが全て終了したら・・・もできそう。