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. ユーザ一覧作成時にグループをはじきたい+ユーザ数に合わせてプルダウンの高さを変更したい
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);
});
}
##1-2. ViewCustomizeのバージョンが古いチームがいっぱいあるので新旧どちらもいけるようにしたい
つーか、はよバージョン上げてください。
/*
* 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使いましょう。
/*
* 工数登録ボタンクリックイベント
*/
$(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. コード全体
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の素晴らしいところです。感謝。