4
2

More than 3 years have passed since last update.

view customize pluginでもっと遊ぶ

Posted at

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

4
2
0

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
  3. You can use dark theme
What you can do with signing up
4
2