Redmineのチケットのコメント追加時に親チケットにもコメントをコピーするView Customizeレシピ

以前作成した「[RedmineのJavaScriptからREST APIを使用する](https://qiita.com/forenoonM/items/6121399cb9f78b8da9ca)」の実践内容。
[redmine.tokyo 第14回勉強会](https://redmine.tokyo/projects/shinared/wiki/%E7%AC%AC14%E5%9B%9E%E5%8B%89%E5%BC%B7%E4%BC%9A)のLTで発表した。

# 目的

WBS状にチケットを切ると親チケットと子チケットができる。
この時、子チケットの兄弟間では情報が分断される。

WBS的にはそれが望ましい姿だと思うが、チケットをWBS成果物としてではなく作業者のタスクとして見ると都合が悪いところが出てくる。
多くのタスクは他のタスクと前後関係があったり、そうでなくても共有すべき情報が出てくる。

1. 情報管理の観点でみると親チケットの詳細やコメント欄、Wikiに集約するのが理想。
1. タスク処理としてみると、子チケット担当者の仕事は子チケットの中で簡潔させるのが理想。

そして仕事の上では、余計な仕事を増やしたくないという事情も出てくる。

そこで集約先を親チケットのコメント欄と定め、子チケットのコメント追加時に親チケットのコメント欄に同時にコピーされるようにする。
全ての子チケットで何が起こったか知りたければ、親チケットのコメント欄を見れば時系列で何があったかわかる。

# View Customizeの設定とコード

View Customizeプラグインの埋め込みコードを1つ追加する。

* Path pattern: /issues/[0-9]+
* Type: JavaScript

```javascript
/* ------ view-customizes:
CollectIssueComment (Details)
Path pattern: /issues/[0-9]+
----------------------- */
$(function() {
    // RedmineURL
    const urlRedmineRoot = '/redmine'.replace(/(\/*)$/, "/");
    let id = "";
    let notes = "";
    let apikey = "";

    // フォームのsubmitの実行を一旦止めて、更新チケットのコメントを親チケットにコピーする。
    $("#issue-form").submit((e) => {
        const form = e.currentTarget;
        id = $("#issue-form").attr("action").match(/(\d+)$/)[0];
        notes = $("#issue_notes")[0].value;
        
        // コメントが空なら何もせず抜ける
        if(!notes){
            return ;
        }
        e.preventDefault();
        
        // 「個人設定」ページをスクレイピングしてapikeyを取得(Redmine3.1以降)
        $.get(`${urlRedmineRoot}my/api_key`)
        .then(
            (html) => {
                apikey = $('#content > div.box > pre', $(html)).first().text();
                return apikey;
            },
            (response) => {
                console.error(`#ajax ${urlRedmineRoot}my/api_key: ${response}`);
                form.submit();
            }
        )
        // 「個人設定」ページをスクレイピングしてapikeyを取得(Redmine3.1以前)
        // $.get(`${urlRedmineRoot}my/account`)
        // .then(
        //     (html) => {
        //         apikey = $("#api-access-key", $(html)).first().text();
        //         return apikey;
        //     },
        //     (response) => {
        //         console.error(`#ajax ${urlRedmineRoot}my/account: ${response}`);
        //         form.submit();
        //     }
        // )
        .then(
            // 更新チケットの詳細データを取得
            (apikey) => {
                if(!apikey) return;
                
                const url = `/issues/${id}.json`;
                const data = {};
                return $.ajax({
                    type: 'get',
                    url: urlRedmineRoot + url.replace(/^(\/*)/, "").replace(/(\.\w+)?$/, ".json"),
                    data: data,
                    headers: {'X-Redmine-API-Key': apikey},
                    dataType: 'json',
                    contentType: 'application/json',
                });
            },
            (response) => {
                console.error(`#scraping apikey: ${response}`);
                form.submit();
            }
        )
        .then(
            // 親チケットが存在していたらコメントをコピー
            (djsonIssue) => {
                if(!djsonIssue || !djsonIssue.issue.parent) return;
                console.debug(djsonIssue);

                let subject = djsonIssue.issue.subject;
                let idParent = djsonIssue.issue.parent.id;

                const url = `/issues/${idParent}.json`;
                const data = {issue:{
                    notes: `***子チケットのコメントを自動コピー*** #${id} ${subject}\n\n${notes}`
                }};
                return $.ajax({
                    type: 'put',
                    url: urlRedmineRoot + url.replace(/^(\/*)/, "").replace(/(\.\w+)?$/, ".json"),
                    data: JSON.stringify(data),
                    headers: {'X-Redmine-API-Key': apikey},
                    dataType: 'text',
                    contentType: 'application/json',
                });
            },
            (response) => {
                console.error(`#get issues: ${response}`);
                form.submit();
            }
        )
        .then(
            // フォームのSubmitを実行
            (djsonPutIssue) => {
                form.submit();
            },
            (response) => {
                console.error(`#put issues: ${response}`);
                form.submit();
            }
        )

        return false;
    });
})
```

# 実行結果

以下のチケット構造を例とする。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/85304/e431a822-8d84-071b-e6c4-c57cad5b3915.png)

子チケット側でこのようなコメントを追加する。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/85304/c8c8b71b-4dc0-a2af-4e09-565cf0516f36.png)

すると親チケット側にはこのように全ての子チケットのコメントが集約される。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/85304/80223dba-d25b-3dd4-560e-22e47450fb12.png)