Redmineに関連チケット作成ボタンを追加する

# やったこと

関連チケット作成ボタンを作成しました。
（[Redmine Advent Calendar 2018 -- 12/3] (https://adventar.org/calendars/3352#list-2018-12-03)　に登録しました）

<img width="1155" alt="関連チケット作成ボタンを作成しました.png" src="https://qiita-image-store.s3.amazonaws.com/0/150778/38e5fe57-d698-ad9b-bea3-d5cd0ceff1aa.png">

今回もONOZATYさんのViewCustomizeの実施例を参考にしました。
（ほとんど真似です！いつもどうもありがとうございます！！）
・REST APIを利用して複数の子チケットをまとめて作成する(Redmine View Customize Plugin)
・http://blog.enjoyxstudy.com

# 背景

チケットの関連付けは、通常では後からチケット同士を関連するチケットとして設定をしますが、例えば、ある不具合を別の機種に横展開するケースなどで、**チケットを関連付けする前提で新規チケットを作成したい**という事です。

現状ですと、

```
不具合チケットの番号を覚えておいて、
新しいチケット作成　=>　新しいチケット内容を記載　=>　送信ボタン押下　で、
新しいチケット登録後に表示された画面で、
先程覚えておいたチケット番号を関連するチケットとして設定する。
```
といった手順になります。

事前にチケット番号をメモしておかないと、
新しいチケットに内容を記載しているうちに番号忘れてしまうし、
でもいちいちメモするのめんどいし、
で、新しいチケットを別タブに開くようにしてやったりしています。

それで。。。
あーー。**関連チケット作成ボタンがほしい。。。**
みたいな感じに思っていたのですが、
上記のONOZATYさんのブログでの小チケット自動作成ボタンを見て、同じ様にすれば出来るじゃん。ってなったのです。

# 画面イメージ

## 関連チケットを作成しますか？　⇛　OK
<img width="1155" alt="関連チケットを作成しますか.png" src="https://qiita-image-store.s3.amazonaws.com/0/150778/7d1104a8-cabe-3fda-357c-c3687e514b26.png">

## 作成された関連チケット
<img width="1158" alt="作成された関連チケット.png" src="https://qiita-image-store.s3.amazonaws.com/0/150778/b3980db6-e214-eae1-82bd-ca2670f47e6f.png">

## 元のチケットにも関連済み

当然ですが。。
<img width="1157" alt="元のチケットにも関連済み.png" src="https://qiita-image-store.s3.amazonaws.com/0/150778/77c6c791-b902-887a-3031-f2afa03f7c69.png">

# ソース実装例

（ViewCustomizePlugin 1.2.0以上でお願いします）

```
パスのパターン: /issues/[0-9]+
挿入位置: チケット詳細の下
種別: JavaScript
```
手順としては、
・説明欄をコピーする為に元のチケットを取得（おまけ）
・新しいチケットの作成
・チケットの関連付け
・関連付けされた新しいチケットを表示
になります。

## コード:

```javascript
$(function() {
  // 関連チケット作成ボタン
  const link = $('<a title="関連チケットの作成" class="icon icon-add" href="#">関連チケットの作成</a>');
  $('#relations').after($('<p>').append(link));

  // ボタンを押した
  link.on('click', function() {
    if (!confirm('関連チケットを作成しますか？')) {
      return;
    }
    $(window).on('beforeunload', function() {
      return '関連チケット作成の処理が完了していません。このまま移動しますか？';
    });

    let myIssueId =  ViewCustomize.context.issue.id;
    let projectId = $('#issue_project_id').val();
    let trackerId = $('#issue_tracker_id').val();
    let subject = $('#issue_subject').val();
    let priorityId = $('#issue_priority_id').val();
    let categoryId = $('#issue_category_id').val();
    let versionId = $('#issue_fixed_version_id').val();

    // 説明欄をコピーする為に取得（この処理はおまけ）
    getIssue(myIssueId)
      .then(function(get_Data) {
        let getData = JSON.parse(get_Data);

        // 関連チケットとして作成する情報
        let relationIssue = 
        {
          "issue": {
            "project_id": projectId,
            "tracker_id": trackerId,
            "subject": subject + " - の関連チケット",
            "priority_id": priorityId,
            "category_id": categoryId,
            "fixed_version_id": versionId,
            "description": "チケット #" + myIssueId + " より\r\n<pre>\r\n" + getData["issue"].description + "\r\n</pre>"
            }
        };

        // 新規チケットの作成
        createIssue(relationIssue)
          .then(function(post_Data) {
            let postData = JSON.parse(post_Data);
            // チケットの関連付け
            createRelation(myIssueId, postData["issue"].id);
          });
      });
  });//on_click

  // 説明欄の取得
  function getIssue(my_issue_id) {
    let dfd = $.Deferred();

    $.ajax({
      type: 'GET',
      url: 'http://192.168.33.10/redmine/issues/' + my_issue_id + '.json',
      headers: {
        'X-Redmine-API-Key': ViewCustomize.context.user.apiKey
      },
      dataType: 'text',
      contentType: 'application/json'
    })
    .done(function(Data) {
        dfd.resolve(Data);
    })
    .fail(function() {
        alert('関連チケットの作成に失敗した可能性があります');
        dfd.reject();
    });

    return dfd.promise();
  }

  // 新規チケットの作成
  function createIssue(new_issue) {
    let dfd = $.Deferred();

    $.ajax({
      type: 'POST',
      url: 'http://192.168.33.10/redmine/issues.json',
      headers: {
        'X-Redmine-API-Key': ViewCustomize.context.user.apiKey
      },
      // 作成時はレスポンスのコンテンツが無く、jsonだとエラーとなるのでtextにしておく
      dataType: 'text',
      contentType: 'application/json',
      data: JSON.stringify(new_issue)
    })
    .done(function(newData) {
        dfd.resolve(newData);
    })
    .fail(function() {
        alert('関連チケットの作成に失敗した可能性があります');
        dfd.reject();
    });

    return dfd.promise();
  }

  // チケットの関連付け
  function createRelation(issue_id, issueTo_Id) {
    let dfd = $.Deferred();

    let json_data = JSON.stringify(
      {
        relation: {
          "issue_id" : issue_id,
          "issue_to_id" : issueTo_Id,
          "relation_type" : "relates"
        }
      }
    );

    $.ajax({
      type: 'POST',
      url: 'http://192.168.33.10/redmine/issues/' + issue_id + '/relations.json',
      headers: {
        'X-Redmine-API-Key': ViewCustomize.context.user.apiKey
      },
      // 作成時はレスポンスのコンテンツが無く、jsonだとエラーとなるのでtextにしておく
      dataType: 'text',
      contentType: 'application/json',
      data: json_data
    })
    .done(function() {
      $(window).off('beforeunload');

      // 成功したら作成した関連チケットページを表示する
      location.href = 'http://192.168.33.10/redmine/issues/' + issueTo_Id;
      dfd.resolve();
    })
    .fail(function() {
      alert('チケットの関連付けに失敗した可能性があります');
      dfd.reject();
    });

    return dfd.promise();
  }
})
```

# あとがき

途中の説明欄をコピーしてくる処理はいらないなら消してしまっていいと思います。
（４行くらい）

あと、ajax途中にページ遷移する場合のポップアップ警告を表示入れましたが、
完全じゃない様な気がします。・・・すみません。

また、今回、初めてDeferred() 使ってみました。(恥かしいですが…)
（・・・いらなかったかもしれません）
使い方に間違いがあるかもしれませんので、どなたかご指摘お願いしたいです。。。(^ _^*)