LoginSignup
2

More than 1 year has passed since last update.

posted at

view customize pluginでもっと遊ぶ

Redmine Advent Calendar 2020 12日目参加です。
今年も、楽しすぎるview customize plugin 1で遊びつつ、Redmineをカスタマイズしています。
今回機能は、【Redmineで複数人の工数を同時に入力する】 Naoki(@nkwtnb)さんの記事を参考にさせていただきました。
ありがとうございます。

目次

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


1.複数人の作業工数を一気に登録 2

数人のリーダーさんに、メンバーの作業工数って自分がまとめて入れられないの?
と聞かれたので調べてみました。3
ほとんど理想の機能がすぐに見つかったので、少々自分ち用にメンテナンス。
ありがたい。

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

1-1. ユーザ一覧作成時にグループをはじきたい+ユーザ数に合わせてプルダウンの高さを変更したい

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
WS000002.JPG

1-2. ViewCustomizeのバージョンが古いチームがいっぱいあるので新旧どちらもいけるようにしたい

つーか、はよバージョン上げてください。

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使いましょう。

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

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

⁺        /* Redmine3.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. コード全体

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. 先人の知恵にはいつも助けられています。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2