Skip to content

Instantly share code, notes, and snippets.

@958
Last active February 26, 2024 07:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 958/6820041 to your computer and use it in GitHub Desktop.
Save 958/6820041 to your computer and use it in GitHub Desktop.
// Info
let PLUGIN_INFO =
<KeySnailPlugin>
<name>RedmineSnail</name>
<description>Redmine client</description>
<updateURL>https://gist.github.com/958/6820041/raw/redmine.ks.js</updateURL>
<author>958</author>
<version>0.0.1</version>
<license>MIT</license>
<include>main</include>
<detail lang="ja"><![CDATA[
=== 使い方 ===
]]></detail>
</KeySnailPlugin>;
// Option
let pOptions = plugins.setupOptions("redmine", {
"issues-keymap": {
preset: {
"C-z" : "prompt-toggle-edit-mode",
"SPC" : "prompt-next-page",
// redmine specific actions
"O" : "open-issue,c",
"E" : "edit-issue,c",
"P" : "select-project",
"S" : "change-status",
"p" : "change-priority",
"r" : "change-ratio",
"t" : "change-tracker",
"A" : "change-assign",
"C" : "new-issue",
"w" : "start-change-wizard",
},
description: M({
ja: "チケット一覧のキーマップ",
en: "Issues keymap"
})
},
}, PLUGIN_INFO);
// Setting
var settings = (function() {
const sitesKey = 'rmine_sites';
const infoKey = 'rmine_info';
return {
get info() {
return persist.restore(infoKey);
},
set info(val) {
persist.preserve(val, infoKey);
},
get sites() {
return persist.restore(sitesKey);
},
set sites(val) {
persist.preserve(val, sitesKey);
},
};
})();
// Site
var Site = (function() {
var _siteUrl, _apiKey;
var Site = function(siteUrl, apiKey) {
_siteUrl = siteUrl;
_apiKey = apiKey;
}
function createParams(params) {
for (var i in params) {
if (params[i])
params[i] = encodeURIComponent(params[i]);
else
delete params[i];
}
return params;
}
function requestPost(endpoint, params, onSuccess, onError) {
var url = _siteUrl + endpoint;
//params = createParams(params);
util.request('POST', url, {
header: {
'X-Redmine-API-Key': _apiKey,
'Content-Type': 'application/json',
},
params: JSON.stringify(params),
callback: function(xhr) {
if (xhr.status == 200) {
if (onSuccess) onSuccess(JSON.parse(xhr.responseText));
} else {
if (onError) onError(JSON.parse(xhr.responseText));
}
}
});
}
function requestPut(endpoint, params, onSuccess, onError) {
var url = _siteUrl + endpoint;
//params = createParams(params);
util.request('PUT', url, {
header: {
'X-Redmine-API-Key': _apiKey,
'Content-Type': 'application/json',
},
params: JSON.stringify(params),
callback: function(xhr) {
util.fbug(xhr.status);
util.fbug(xhr.responseText);
if (xhr.status == 200) {
//if (onSuccess) onSuccess(JSON.parse(xhr.responseText));
} else {
//if (onError) onError(JSON.parse(xhr.responseText));
}
}
});
}
function requestGet(endpoint, params, onSuccess, onError) {
var url = _siteUrl + endpoint;
params = createParams(params);
url += '?' + util.paramsToString(params);
util.request('GET', url, {
header: {
'X-Redmine-API-Key': _apiKey,
},
callback: function(xhr) {
if (xhr.status == 200) {
if (onSuccess) onSuccess(JSON.parse(xhr.responseText));
} else {
if (onError) onError(JSON.parse(xhr.responseText));
}
}
});
}
function requestDelete(endpoint, params, onSuccess, onError) {
var url = _siteUrl + endpoint;
params = createParams(params);
util.request('DELETE', url, {
header: {
'X-Redmine-API-Key': _apiKey,
},
callback: function(xhr) {
if (xhr.status == 200) {
if (onSuccess) onSuccess(JSON.parse(xhr.responseText));
} else {
if (onError) onError(JSON.parse(xhr.responseText));
}
}
});
}
function openUrl(url, where) {
openUILinkIn(_siteUrl + url, where || 'current');
}
Site.prototype = {
requestPost: requestPost,
requestPut: requestPut,
requestGet: requestGet,
requestDelete: requestDelete,
openUrl: openUrl,
};
return Site;
})();
// Redmine API
var Api = (function() {
var _site;
function Api(siteUrl, apiKey) {
_site = new Site(siteUrl, apiKey);
}
function getProjects(callback) {
_site.requestGet('projects.json', {},
function(res) { if (callback) callback(res); },
function(res) { display.echoStatusBar('Request failed.'); if (callback) callback(res); }
);
}
function openProject(project, where) {
_site.openUrl('projects/' + project.identifier, where);
}
function getAllIssues(filter, callback) {
_site.requestGet('issues.json',
_.extend({ limit: 100 }, filter || {}),
function(res) { if (callback) callback(res); },
function(res) { display.echoStatusBar('Request failed.'); if (callback) callback(res); }
);
}
function getProjectIssues(project, filter, callback) {
getAllIssues(_.extend({ project_id: project.id }, filter || {}), callback);
}
function updateIssue(issue, params, callback) {
_site.requestPut('issues/' + issue.id + '.json',
params,
function(res) { if (callback) callback(res); },
function(res) { display.echoStatusBar('Request failed.'); if (callback) callback(res); }
);
}
function createIssue(project, where) {
_site.openUrl('projects/' + project.identifier + '/issues/new', where);
}
function openIssue(issue, where, edit) {
_site.openUrl('issues/' + issue.id + (edit ? '/edit' : ''), where);
}
function getIssueStatuses(callback) {
_site.requestGet('issue_statuses.json', {},
function(res) { if (callback) callback(res); },
function(res) { display.echoStatusBar('Request failed.'); if (callback) callback(res); }
);
}
function getProjectMemberships(project, callback) {
_site.requestGet('projects/' + project.id + '/memberships.json', {},
function(res) { if (callback) callback(res); },
function(res) { display.echoStatusBar('Request failed.'); if (callback) callback(res); }
);
}
Api.prototype = {
getProjects: getProjects,
openProject: openProject,
getProjectIssues: getProjectIssues,
getAllIssues: getAllIssues,
updateIssue: updateIssue,
createIssue: createIssue,
openIssue: openIssue,
getIssueStatuses: getIssueStatuses,
getProjectMemberships: getProjectMemberships,
};
return Api;
})();
// Main
var rmine = (function() {
var _info = settings.info || {};
var _sites = settings.sites || {};
var _api;
function setCurrentSite(url) {
_api = new Api(url, _sites[url].apiKey);
_info.currentSite = url;
_info.currentProject = 0;
_project = null;
settings.info = _info;
}
function setCurrentProject(project) {
_info.currentProject = project.id;
settings.info = _info;
}
function addSite() {
var url, apiKey;
function readUrl() {
prompt.read('Please enter redmine URL :', function(str){
if (str != null) {
url = (str[str.length - 1] != '/') ? str + '/' : str;
readApiKey();
}
});
}
function readApiKey() {
prompt.read('Please enter your API key :', function(str){
if (str != null) {
apiKey = str;
_sites[url] = { apiKey: apiKey };
settings.sites = _sites;
setCurrentSite(url);
setTimeout(function() showProjects(), 0);
}
});
}
readUrl();
}
function selectSite() {
var collection = [];
for (var url in _sites) collection.push([ url ]);
prompt.selector({
message : 'Select site :',
collection : collection,
header : ['URL'],
actions : [
[
function(index) {
if (index < 0) return;
setCurrentSite(collection[index])
setTimeout(function() showProjects(!self.site.projects), 0);
},
'Select site', 'select-site'
],
]
});
}
function showProjects(isLoad) {
function showSelector(projects) {
prompt.selector({
message : 'Select project :',
collection : projects.map(function(p) [p.name, p.description, p]),
header : ['Name', 'Description'],
flags : [0, 0, HIDDEN|IGNORE],
actions : [
[
function(index) {
if (index < 0) return;
setCurrentProject(projects[index]);
setTimeout(function() showProjectIssues(projects[index]), 0);
},
'Select site', 'select-site'
],
]
});
}
if (isLoad || !_sites[_info.currentSite].projects)
_api.getProjects(function(res) {
_sites[_info.currentSite].projects = res.projects;
settings.sites = _sites;
showSelector(res.projects);
});
else
showSelector(_sites[_info.currentSite].projects);
}
function showIssuesSelector(project, issues) {
prompt.selector({
message : 'Select issue :',
collection : issues.map(function(i) {
return [
i.id,
(i.tracker) ? i.tracker.name : '' ,
(i.category) ? i.category.name : '',
i.status.name,
i.subject,
(i.fixed_version) ? i.fixed_version.name : '',
(i.assigned_to) ? i.assigned_to.name : '',
i.project.name,
i]
}),
header : ['ID', 'Tracker', 'Category', 'Status', 'Subject', 'Fixed version', 'Assigned', 'Project'],
width : [5, 5, 5, 5, 50, 10, 10, 10],
flags : [0, 0, 0, 0, 0, 0, 0, 0, HIDDEN|IGNORE],
keymap : pOptions['issues-keymap'],
actions : [
[
function(index) {
if (index < 0) return;
_api.openIssue(issues[index], 'tab');
},
'Open issue', 'open-issue'
],
[
function(index) {
if (index < 0) return;
_api.openIssue(issues[index], 'tab-shifted');
},
'Open issue for background', 'open-issue-background'
],
[
function(index) {
if (index < 0) return;
_api.openIssue(issues[index], 'tab', true);
},
'Edit issue', 'edit-issue'
],
[
function(index) {
setTimeout(showProjects, 0);
},
'Select project', 'select-project'
],
[
function(index) {
if (index < 0) return;
setTimeout(function() {
selectIssueStatuses(issues[index], function(status) {
selectAssignedTo(issues[index], function(assignedTo) {
inputComment(function(comment) {
_api.updateIssue(issues[index], {
issue: {
status_id: status.id,
assigned_to_id: assignedTo.id,
notes: comment
}
},
function() display.echoStatusBar('Done'));
});
});
});
}, 0);
},
'Change status', 'change-status'
],
[
function(index) {
if (project)
_api.createIssue(project, 'tab');
},
'New issue', 'new-issue'
],
]
});
}
function showProjectIssues(project, filter) {
project = project || self.project;
if (project)
_api.getProjectIssues(project, filter, function(res) {
showIssuesSelector(project, res.issues);
});
else
showProjects();
}
function showAllIssues(filter) {
_api.getAllIssues(filter, function(res) {
showIssuesSelector(null, res.issues);
});
}
function selectIssueStatuses(issue, callback) {
_api.getIssueStatuses(function(res) {
var statuses = res.issue_statuses.map(function(s) [s.name, s.id]);
var initialIndex = 0;
res.issue_statuses.some(function(s, i) {
if (s.id == issue.status.id) {
initialIndex = i;
return true;
}
});
prompt.selector({
message : 'Select status :',
collection : statuses,
flags : [0, HIDDEN|IGNORE],
initialIndex: initialIndex,
actions : [
[
function(index) {
if (index < 0 || !callback) return;
setTimeout(function() callback({ name: statuses[index][0], id: statuses[index][1] }), 0);
},
'Select status', 'select-status'
],
]
});
});
}
function selectAssignedTo(issue, callback) {
_api.getProjectMemberships(issue.project, function(res) {
var members = res.memberships.map(function(m) [m.user.name, m.user.id]);
var initialIndex = 0;
res.memberships.some(function(m, i) {
if (m.user.id == issue.assigned_to.id) {
initialIndex = i;
return true;
}
});
prompt.selector({
message : 'Select assigned member :',
collection : members,
initialIndex: initialIndex,
flags : [0, HIDDEN|IGNORE],
actions : [
[
function(index) {
if (index < 0 || !callback) return;
setTimeout(function() callback({ name: members[index][0], id: members[index][1] }), 0);
},
'Select member', 'select-member'
],
]
});
});
}
function inputComment(callback) {
prompt.reader({
message: 'Please enter comment:',
callback: function(str) {
if (str)
!callback || callback(str);
},
onFinish: function() prompt.multiLine = false,
});
prompt.multiLine = true;
setTimeout(function() document.getElementById("keysnail-prompt-textbox").focus(), 0);
}
var self = {
get site() {
return _sites[_info.currentSite];
},
get project() {
var prjs = self.site.projects.filter(function(p) p.id == _info.currentProject);
return (prjs.length > 0) ? prjs[0] : null;
},
addSite: addSite,
selectSite: selectSite,
showProjects: showProjects,
showProjectIssues: showProjectIssues,
showAllIssues: showAllIssues,
};
// Create pre selected site API
if (self.site)
_api = new Api(_info.currentSite, self.site.apiKey);
return self;
})();
plugins.rmine = rmine;
// Add ext
plugins.withProvides(function (provide) {
provide('rmine-add-site',
function (ev, arg) {
rmine.addSite();
},
M({en:'rmine - Add redmine site', ja:'rmine - サイトを追加'}));
provide('rmine-show-projects',
function (ev, arg) {
rmine.showProjects(arg);
},
M({en:'rmine - Show projects', ja:'rmine - プロジェクト一覧を表示'}));
provide('rmine-show-project-issues',
function (ev, arg) {
rmine.showProjectIssues();
},
M({en:'rmine - Show project issues', ja:'rmine - プロジェクトのチケット一覧を表示'}));
provide('rmine-show-all-issues',
function (ev, arg) {
rmine.showAllIssues();
},
M({en:'rmine - Show all issues', ja:'rmine - すべてのチケット一覧を表示'}));
}, PLUGIN_INFO);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment