view customize pluginでもっと遊ぶ

[Redmine Advent Calendar 2020](https://adventar.org/calendars/5392)　12日目参加です。
今年も、楽しすぎる[view customize plugin]( 
https://github.com/onozaty/redmine-view-customize) [^1]で遊びつつ、Redmineをカスタマイズしています。
今回機能は、[【Redmineで複数人の工数を同時に入力する】](https://trialanderror.jp/viewcustomize-time-entry-related/) Naoki（[@nkwtnb](https://twitter.com/nkwtnb)）さんの記事を参考にさせていただきました。
ありがとうございます。

#目次
1.[複数人の作業工数を一気に登録](#1-複数人の作業工数を一気に登録)
1-1.[ユーザ一覧作成時にグループをはじきたい＋ユーザ数に合わせてプルダウンの高さを変更したい](#1-1-ユーザ一覧作成時にグループをはじきたいユーザ数に合わせてプルダウンの高さを変更したい)
1-2.[ViewCustomizeのバージョンが古いチームがいっぱいあるので新旧どちらもいけるようにしたい](#1-3-ViewCustomizeのバージョンが古いチームがいっぱいあるので新旧どちらもいけるようにしたい)
1-3.[登録に成功したら作業時間詳細（一覧）画面に遷移したい](#1-4-登録に成功したら作業時間詳細（一覧）画面に遷移したい)
2.[コード全体](#2-コード全体)
3.[おしまいに](#3-おしまいに)

---
#1．複数人の作業工数を一気に登録 [^2]
数人のリーダーさんに、メンバーの作業工数って自分がまとめて入れられないの？
と聞かれたので調べてみました。[^3]
ほとんど理想の機能がすぐに見つかったので、少々自分ち用にメンテナンス。
ありがたい。

***<font color=#4689FF>◆本家から変更したいところ</font>***
1-1. ユーザ一覧作成時にグループをはじきたい＋ユーザ数に合わせてプルダウンの高さを変更したい
1-2. ViewCustomizeのバージョンが古いチームがいっぱいあるので新旧どちらもいけるようにしたい
1-3. 登録に成功したら作業時間詳細（一覧）画面に遷移したい

##1-1. ユーザ一覧作成時にグループをはじきたい＋ユーザ数に合わせてプルダウンの高さを変更したい
```ruby:qiita.javascript
const makeUserList = function() {
    /* 作成済み要素を削除 */
    $('#time_entry_related_user').remove();
    /* チケット番号取得 */
    const issueId = $('#time_entry_issue_id').val();
　　... 略 ... 
   }).then(function(resp) {
        /* [table class=list members]の[td class=group members]だったらはじきたい
           ---> idの前と、データ3つ目にgroupがある */
        const userPromise = [];

        /* 配列の中身に「group」があるかどうか確認したいのでLoopをfor文にする */
-       //resp.memberships.forEach(function(member) {

+       /* 配列分Loop */
+       for(var i=0; i<resp.memberships.length; i++){
+              const member = resp.memberships[i];
+
+              /* 配列目名取り出し */
+              const ggg = Object.keys(member);
+              /* groupデータだったら処理しない */
+              if(-1 == ggg.indexOf('group')){
+                     userPromise.push(getUser(member.user.id));
+              }
+        }
+        return Promise.all(userPromise);
    }).then(function(resp) {
+        /* プロジェクトのメンバー情報を全て取得後、select要素を生成する
+           ---> ログインユーザーの取得（DOM要素から無理やり取ってくるよ）*/
+        const loginUser = $('#loggedas .user.active').text();
+        let users = "";
+        resp.forEach(function(userInfo) {

-        /* 上でLoop処理変更したので、ここでログインユーザをはじく処理を削除しておく */
-        // if (loginUser === userInfo.user.login) {
-        //    return;
-        // }

            users += '<option value="' + userInfo.user.login + '">'
                    +   userInfo.user.lastname + userInfo.user.firstname
                    + '</option>';
        });

        /* 関連ユーザセレクトボックス作成 */
        const source = '<p>'
        + '<label for="time_entry_user">関連ユーザー</label>'
        + '<select name="time_entry[user]" id="multiSelect" multiple="multiple">'
        +   '<option value="">--- 選んでください ---</option>'
        +       users
        +   '</select>'
        +  '<input id="button-time-entry-user" type="button" name="button" value="工数登録" style="vertical-align:top; margin: 0px 4px">'
        +  '</p>';

        $(".box.tabular").append(source);

+        /* optionの数に合わせてリストのサイズを可変する */
+        const l = document.getElementById("multiSelect");
+        l.setAttribute("size", l.length + 1);
    }).catch(function(e) {
        console.log(e);
    });
}

```

プルダウン可変はこんな感じ。
![WS000001.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332990/782df4d3-63c5-c166-c66a-ce05d38ef61b.jpeg)
![WS000002.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332990/5330d5ac-476e-06de-0698-48d60e9df8b1.jpeg)



##1-2. ViewCustomizeのバージョンが古いチームがいっぱいあるので新旧どちらもいけるようにしたい
つーか、はよバージョン上げてください。

```ruby:qiita.javascript
/*
 * Redmine API 実行用関数
 * @param _param 
*/

const executeApi = function(_param) {

+   /* API_KEY取得：viewcustomize1.2以下の場合 */
+   const l = document.getElementById("multiSelect");
+   let apikey = "";
+   $.get('/my/api_key').done(function(data){
+      apikey = $('#content > div.box > pre', $(data)).first().text();
+   });
+   /* ココマデ */

+   /* API_KEY取得：viewcustomize1.2以上の場合 */
+   // const API_KEY = ViewCustomize.context.user.apiKey;
+   /* ココマデ */

    /* API_KEYは共通なので、実行用関数で設定 */
    if (_param.headers) {
        _param.headers['X-Redmine-API-Key'] = apikey    //API_KEY
    } else {
        _param['headers'] = {
            'X-Redmine-API-Key': apikey   //API_KEY
        }
    }
　　... 略 ... 
};
```

##1-3. 登録に成功したら作業時間詳細（一覧）画面に遷移したい
こっちもRedmine3.x使ってるチーム用に手を入れてます。
はよ4.xにしましょう。むしろRedMica使いましょう。

```ruby:qiita.javascript
/*
 * 工数登録ボタンクリックイベント
*/

$(document).on('click', '#button-time-entry-user', function(e) {
　　     ... 略 ...
⁺        /* 成功したら作業時間－詳細（一覧）画面に飛ぶ処理を追加
⁺           ---> url取得＆加工 */

⁺        /* Redmineが3.xの場合の処理 : 4.xの場合は以下コメントアウトすること */
⁺        /* 現在のページ（作業工数入力画面）取得 */
⁺        var url = location.href;
⁺        /* url最後の'/'以降削除設定 */
⁺        var index = url.lastIndexOf("/");
⁺        /* 遷移先url取得 */
⁺        url = url.substring(index, -1);
⁺        /* ココマデ */

⁺        /* Redmineが4.xの場合の処理 : 3.xの場合は以下コメントアウトすること */
⁺        /* 作業工数一覧に飛ぶために、概要URL取得 */
⁺        //const url = document.getElementsByClassName('overview').getAttribute('href');
⁺        //url = url + "/time_entries";
⁺        /* ココマデ */

 ⁺       /* ページ遷移 */
 ⁺       window.location.href = url;
    }).catch(function(e) {
        let error;
        if (e.responseText) {
            error = JSON.parse(e.responseText);
        }
        alert(
            "関連ユーザーの工数登録に失敗しました。\n" +
            error.errors.join("\n")
        );
    });
});
```

#2. コード全体
```ruby:qiita.javascript
PathPattern: /time_entries/
Type: JavaScript
挿入位置: 全てのページのヘッダ
コメント: /*複数人の工数を同時に入力する*/ 

const makeUserList = function() {
    /* 作成済み要素を削除 */
    $('#time_entry_related_user').remove();
    /* チケット番号取得 */
    const issueId = $('#time_entry_issue_id').val();
 
    /* 対象チケットが属するプロジェクトのメンバー情報を取得 */
    getIssue(issueId).then(function(issueInfo) {
        console.log(issueInfo);
        if (!issueInfo) {
            return Promise.reject();
        }
        return getProjectMember(issueInfo.issue.project.id);
    }).then(function(resp) {

        /* [table class=list members]の[td class=group members]だったらはじきたい
           ---> idの前と、データ3つ目にgroupがある */
        const userPromise = [];

        /* 配列の中身に「group」があるかどうか確認したいのでLoopをfor文にする */
        //resp.memberships.forEach(function(member) {

        /* 配列分Loop */
        for(var i=0; i<resp.memberships.length; i++){
            const member = resp.memberships[i];

            /* 配列の項目名取り出し */
            const ggg = Object.keys(member);
            /* groupのデータだったら処理しない */
            if(-1 == ggg.indexOf('group')){
              userPromise.push(getUser(member.user.id));
            }
        }
        return Promise.all(userPromise);
    }).then(function(resp) {

        /* プロジェクトのメンバー情報を全て取得後、select要素を生成する
           ---> ログインユーザーの取得（DOM要素から無理やり取ってくるよ）*/
        const loginUser = $('#loggedas .user.active').text();
        let users = "";
        resp.forEach(function(userInfo) {

            /* 上でLoop処理変更したので、ここでログインユーザをはじく処理を削除しておく */
            //if (loginUser === userInfo.user.login) {
            //    return;
            //}
            users += '<option value="' + userInfo.user.login + '">'
                    +   userInfo.user.lastname + userInfo.user.firstname
                    + '</option>';
        });

        /* 関連ユーザセレクトボックス作成 */
        const source = '<p>'
        + '<label for="time_entry_user">関連ユーザー</label>'
        + '<select name="time_entry[user]" id="multiSelect" multiple="multiple">'
        +   '<option value="">--- 選んでください ---</option>'
        +       users
        +   '</select>'
        +  '<input id="button-time-entry-user" type="button" name="button" value="工数登録" style="vertical-align:top; margin: 0px 4px">'
        +  '</p>';

        $(".box.tabular").append(source);

        /* optionの数に合わせてリストのサイズを可変する */
        const l = document.getElementById("multiSelect");
        l.setAttribute("size", l.length + 1);
    }).catch(function(e) {
        console.log(e);
    });
}
 
/*
 * Redmine API 実行用関数
 * @param _param 
*/
const executeApi = function(_param) {

   /* API_KEY取得：viewcustomize1.2以下の場合 */
   let apikey = "";
   $.get('/my/api_key').done(function(data){
      apikey = $('#content > div.box > pre', $(data)).first().text();
   });


   /* API_KEY取得：viewcustomize1.2以上の場合はこっちをアクティブにする */
   // const API_KEY = ViewCustomize.context.user.apiKey;

    // API_KEYは共通なので、実行用関数で設定
    if (_param.headers) {
        _param.headers['X-Redmine-API-Key'] = apikey    //API_KEY;
    } else {
        _param['headers'] = {
            'X-Redmine-API-Key': apikey   //API_KEY
        }
    }
    // パラメータ設定
    const param = {
        type: _param.type,
        url: _param.url,
        headers: _param.headers,
        dataType: 'text',
        contentType: 'application/json',
    }
    // dataが設定されている場合、パラメータに追加
    if (_param.data) {
        param['data'] = JSON.stringify(_param.data)
    }
    return new Promise(function(resolve, reject) {
        $.ajax(param).done(function(resp) {
            resolve(JSON.parse(resp));
        }).fail(function(jqXHR, textStatus, errorThrown){
            console.log(jqXHR);
            console.log(textStatus);
            console.log(errorThrown);
            reject(jqXHR);
        });
    });
};


/**
 * 工数入力用関数
 * @param userId 
 */
const postTimeEntry = function(userId) {
    const param = {
        type: 'POST',
        url: '/time_entries.json',
        headers: {
            'X-Redmine-Switch-User': userId,
        },
        data: {
            "time_entry": {
                "issue_id": $('#time_entry_issue_id').val(),
                "spent_on": $('#time_entry_spent_on').val(),
                "hours":$('#time_entry_hours').val(),
                "comments":$('#time_entry_comments').val(),
                "activity_id":$('#time_entry_activity_id').val()
            }
        },
    }
    return executeApi(param);
}


/*
 * プロジェクトメンバー取得用関数
*/
const getProjectMember = function(projectId) {
    const param = {
        type: 'GET',
        url: '/projects/' + projectId + '/memberships.json',
    }
    return executeApi(param);
}

/*
 * ユーザー情報取得用関数
 * @param userId 
*/
const getUser = function(userId) {
    const param = {
        type: 'GET',
        url: '/users/' + userId + '.json',
    }
    return executeApi(param);
}
 
const getIssue = function(issueId) {
    const param = {
        type: 'GET',
        url: '/issues/' + issueId + '.json'
    }
    return executeApi(param);
     
}


/*
 * 工数登録ボタンクリックイベント
*/
$(document).on('click', '#button-time-entry-user', function(e) {
    const self = this;
    console.log(e);
    const selected = $('#multiSelect').val();
    const promises = [];

    /* 未選択の場合、スキップ */
    if (!selected) {
        return;
    }
    selected.forEach(function(selectedUser) {
        /* 未選択の場合、スキップ */
        if (!selectedUser) {
            return;
        }
        promises.push(postTimeEntry(selectedUser));
    });

    /* 工数登録処理実行対象外の場合、終了 */
    if (promises.length === 0) {
        return;
    }
    Promise.all(promises).then(function(responses) {
        alert("関連ユーザーの工数を登録しました。")
        console.log(responses);
        $(self).prop("disabled", true);

        /* 成功したら作業時間－詳細（一覧）画面に飛ぶ処理を追加
           ---> url取得＆加工 */

        /* Redmineが3.xの場合の処理 : 4.xの場合は以下コメントアウトすること */
        /* 現在のページ（作業工数入力画面）取得 */
        var url = location.href;
        /* url最後の'/'以降削除設定 */
        var index = url.lastIndexOf("/");
        /* 遷移先url取得 */
        url = url.substring(index, -1);
        /* ココマデ */

        /* Redmineが4.xの場合の処理 : 3.xの場合は以下コメントアウトすること */
        /* 作業工数一覧に飛ぶために、概要URL取得 */
        //const url = document.getElementsByClassName('overview').getAttribute('href');
        //url = url + "/time_entries";
        /* ココマデ */

        /* ページ遷移 */
        window.location.href = url;
    }).catch(function(e) {
        let error;
        if (e.responseText) {
            error = JSON.parse(e.responseText);
        }
        alert(
            "関連ユーザーの工数登録に失敗しました。\n" +
            error.errors.join("\n")
        );
    });
});

 
/*
 * チケットIDの変更イベント
*/
$(document).on('change', '#time_entry_issue_id', function(e) {
    makeUserList();    
});
 
$(function() {
    makeUserList();
});
```

#3. おしまいに
以上、昨年に引き続き「素人でもなんとかなる！」を表明する記事でした。
不便と便利を追求すると素人でもなんとかなります。
この部分こう変更したらイケる？と思って試してみられるのがview customize pluginの素晴らしいところです。感謝。


[^1]: いつもお世話になっております。
[^2]: 本当は投票機能について記事書こうと思ってたんですが、砂場ごっそり消されたorz バックアップは取ってあるけど、生き残っている環境でテストしてないのでまた今度。
[^3]: 先人の知恵にはいつも助けられています。