2
// @name LP_StockReplies
3
// @namespace http://outflux.net/greasemonkey/
4
// @description (Launchpad) Stock replies
5
// @include https://launchpad.net/*
6
// @include https://*.launchpad.net/*
7
// @include https://*.edge.launchpad.net/*
10
// @creator Kees Cook <kees@ubuntu.com>
11
// @contributor Brian Murray <brian@ubuntu.com>
12
// @contributor Bryce Harrington <bryce@ubuntu.com>
14
// Based on code originally written by:
15
// Tollef Fog Heen <tfheen@err.no>
16
// Brian Murray <brian@ubuntu.com>
20
name: "LP_StockReplies",
21
namespace: "http://outflux.net/greasemonkey/",
22
description: '(Launchpad) Stock replies',
23
source: "http://codebrowse.launchpad.net/~ubuntu-dev/ubuntu-gm-scripts/ubuntu/files",
24
identifier: "http://codebrowse.launchpad.net/~ubuntu-dev/ubuntu-gm-scripts/ubuntu/file/lp_stockreplies.user.js",
26
date: (new Date(2009, 12 - 1, 22))// update date
30
function xpath(query, context) {
31
context = context ? context : document;
32
return document.evaluate(query, context, null,
33
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
36
String.prototype.ucFirst = function () {
37
return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
42
var prefsData = new Object;
43
var prefsFields = new Array(
44
"name", // required -- the clickable name
45
"comment", // required -- the stock reply!
46
"status", // "" == leave unchanged
47
"tip", // tooltip hint (optional)
48
"assign", // "" == leave unchanged
49
// "-me" == assign to self
50
// "-nobody" == assign to nobody
51
"importance", // "" == leave unchanged
52
"package", // "" == leave unchanged
53
"standard" // "yes" == is from standard XML?
57
// This routine is called once per stock reply item at time of page loading
58
function injectStockreply(formname, idx) {
60
var element = document.createElement('a');
61
element.href = document.location + "#";
62
var innerTextElement = document.createTextNode(prefsData['name'][idx]);
64
// Default to using comment as tooltip, when tooltip is missing
65
tip = prefsData['tip'][idx];
67
tip = prefsData['comment'][idx];
71
element.appendChild(innerTextElement);
72
element.addEventListener('click', function(e) {
75
// Retrieve bug details
76
var pathname = window.location.pathname;
77
var bug_project = pathname.split('/')[1].ucFirst();
78
// if the bug has no package this ends up being the bug number
79
var bug_package = pathname.split('/')[3];
80
var bug_number = pathname.split('/').pop();
81
var bug_reporter = xpath("//*[@class='registering']/*[contains(@class,'sprite person')]").snapshotItem(0).firstChild.nodeValue;
82
var bug_upstream = xpath("//*[@class='link-external']").snapshotItem(0);
85
var comment_text = prefsData['comment'][idx];
86
comment_text = comment_text.replace("PROJECTNAME", bug_project);
87
comment_text = comment_text.replace("BUGNUMBER", bug_number);
88
// only replace the package name for bugs with a package i.e. don't do it for no package bugs
89
if (bug_number != bug_package) {
90
comment_text = comment_text.replace("PKGNAME", bug_package);
92
comment_text = comment_text.replace("REPORTER", bug_reporter);
93
if (bug_upstream != null) {
94
comment_text = comment_text.replace("UPSTREAMBUG", bug_upstream.href);
96
xpath('//textarea[@id="'+ formname + '.comment_on_change"]').snapshotItem(0).value = comment_text;
99
if (prefsData['status'][idx] != "") {
100
xpath('//select[@id="'+ formname + '.status"]/option[.="'+prefsData['status'][idx].replace(/"/,'\\"')+'"]').snapshotItem(0).selected = true;
104
if (prefsData['assign'][idx] != "") {
105
if (prefsData['assign'][idx] == "-me") {
106
xpath('//input[@value="'+ formname + '.assignee.assign_to_me"]').snapshotItem(0).checked = true;
108
else if (prefsData['assign'][idx] == "-nobody") {
109
xpath('//input[@value="'+ formname + '.assignee.assign_to_nobody"]').snapshotItem(0).checked = true;
112
xpath('//input[@value="'+ formname + '.assignee.assign_to"]').snapshotItem(0).checked = true;
113
xpath('//input[@id="'+ formname + '.assignee"]').snapshotItem(0).value = prefsData['assign'][idx];
118
if (prefsData['package'][idx] != "") {
119
xpath('//input[@name="'+ formname + '.target.package"]').snapshotItem(0).value = prefsData['package'][idx];
123
if (prefsData['importance'][idx] != "") {
124
xpath('//select[@id="'+ formname + '.importance"]/option[.="'+prefsData['importance'][idx].replace(/"/,'\\"')+'"]').snapshotItem(0).selected = true;
127
// Subscribe triager by default
128
// var sub = xpath('//input[@id="subscribe"]').snapshotItem(0);
129
// if (sub) sub.checked = true;
136
var reply_class = 'lp_sr';
137
function insert_clickable(node, newElement, tagged, left, right)
139
if (!left) { left='['; }
140
if (!right) { right=']'; }
141
var span = document.createElement("span");
142
var leftBrace = document.createTextNode(left);
143
var rightBrace = document.createTextNode(right+' ');
145
/* mark up for future removal? */
147
span.setAttribute('class',reply_class)
151
span.appendChild(leftBrace);
152
span.appendChild(newElement);
153
span.appendChild(rightBrace);
154
// make the source readable
155
span.appendChild(document.createTextNode("\n"));
158
node.insertBefore(span, span.nextSibling);
161
function deleteReply(idx)
163
var count = parseInt(GM_getValue('count',0))
164
if (count == 0) return;
165
if (idx >= count) return;
167
/* move all the prefs up one to wipe out the deleted one */
168
for (var move = idx + 1; move < count; move ++) {
169
for (var field in prefsFields) {
170
for (var field in prefsFields) {
171
var fieldname = prefsFields[field];
172
GM_setValue(fieldname+(move-1),GM_getValue(fieldname+move,""));
176
GM_setValue('count',''+(count-1))
177
/* since we've deleted a reply, caller needs to reload this script's
178
view of the GM prefs via loadPreferences() */
181
function clearStandardReplies()
183
var count = parseInt(GM_getValue('count', 0));
184
for (var idx = count - 1; idx >= 0; idx --) {
185
standard = GM_getValue('standard'+idx,"");
186
if (standard == "yes") {
187
deleteReply(idx, false);
193
function loadPreferences()
195
prefsData.standardSeen = false;
196
prefsData.count = parseInt(GM_getValue('count', 0));
197
for (var field in prefsFields) {
198
var fieldname = prefsFields[field];
199
prefsData[fieldname] = new Array;
201
for (var idx = 0; idx < prefsData.count; idx ++) {
202
prefsData[fieldname][idx] = GM_getValue(fieldname+idx,"");
205
for (var idx = 0; idx < prefsData.count; idx ++) {
206
if (prefsData['standard'][idx] == "yes") {
207
prefsData.standardSeen = true;
210
prefsData.reloadAt = parseInt(GM_getValue('reload-at', 0));
213
function loadStandardReplies() {
218
url: 'http://people.ubuntu.com/~brian/greasemonkey/bugsquad-replies.xml',
220
'Accept': 'application/atom+xml,application/xml,text/xml',
222
onload: function(results) {
223
var parser = new DOMParser();
224
var dom = parser.parseFromString(results.responseText,"application/xml");
225
var replies = dom.getElementsByTagName('reply');
226
// destroy preferences for possible reload
228
/* if we actually have some replies, clear the old ones */
230
alert("Dropping old standard stock replies");
232
if (replies.length>0) {
233
clearStandardReplies();
236
alert("Parsing new standard stock replies");
238
var base = prefsData.count;
239
for (var i=0; i < replies.length; i++) {
240
var standardReply = new Array;
241
for (var field in prefsFields) {
242
var fieldname = prefsFields[field];
244
if (fieldname == "standard") {
248
// alert("Want " + fieldname + " (offset " + i + ")");
249
element = replies[i].getElementsByTagName(fieldname);
250
if (element.length) {
251
text = element[0].textContent;
254
prefsData[fieldname][base+i] = text;
255
// alert(fieldname + " as [" + text + "] at " + (base+i));
257
if (fieldname == "name") {
258
alert("Parsed [" + text + "]");
263
prefsData.count += replies.length;
264
// reload again in 1.5 days
265
var time = new Date();
266
prefsData.reloadAt = time.getUTCMilliseconds() + (1000 * 60 * 60 * 36);
268
alert("Saving stock replies");
272
alert("Finished reloading standard stock replies");
279
function addColumnPreference(idx,fieldname)
281
var td = document.createElement('td');
282
// why doesn't this alignment work?
283
//td.setAttribute('valign','top');
285
var id = reply_class + '.' + idx + '.' + fieldname;
287
var label = document.createElement('label');
288
label.setAttribute('style','font-weight: bold;');
289
label.setAttribute('for',id);
290
label.appendChild(document.createTextNode(fieldname));
291
td.appendChild(label);
294
if (fieldname == 'comment') {
295
input = document.createElement('textarea');
296
// match current LP comment field size
297
input.setAttribute('cols','62');
298
input.setAttribute('rows','4');
301
input = document.createElement('input');
302
input.setAttribute('type','text');
303
input.setAttribute('size','15');
305
input.value = prefsData[fieldname][idx];
306
input.setAttribute('name',fieldname);
307
input.setAttribute('id',id);
308
//alert('added ('+fieldname+','+idx+'): '+input.value);
309
input.addEventListener('change', function(e) {
313
var fieldname = obj.getAttribute('name');
314
//alert('changed ('+fieldname+','+idx+'): '+obj.value);
315
if (prefsData[fieldname][idx] != obj.value) {
316
/* mark as non-standard if it was changed */
317
prefsData['standard'][idx] = "";
319
prefsData[fieldname][idx] = obj.value;
323
td.appendChild(input);
325
// make the source readable
326
td.appendChild(document.createTextNode("\n"));
331
function addRowPreferences(table,idx)
333
/* TODO: mark this row in some way if it is a standard reply */
334
var tr = document.createElement('tr');
335
for (var field in prefsFields) {
336
var fieldname = prefsFields[field];
337
if (fieldname == 'standard') continue;
338
if (fieldname == 'comment') continue;
340
// set up empty default
341
if (!prefsData[fieldname][idx]) {
342
prefsData[fieldname][idx]="";
345
td = addColumnPreference(idx,fieldname);
348
table.appendChild(tr);
350
// make the source readable
351
table.appendChild(document.createTextNode("\n"));
353
// add "comment" input separately since it is a textarea
354
var comment_tr = document.createElement('tr');
355
var comment_td = addColumnPreference(idx,'comment');
356
comment_td.setAttribute('colspan', prefsFields.length - 2);
357
comment_tr.appendChild( comment_td );
358
table.appendChild(comment_tr);
360
// make the source readable
361
table.appendChild(document.createTextNode("\n"));
364
var sep_td = document.createElement('td');
365
var sep_tr = document.createElement('tr');
366
sep_td.appendChild(document.createTextNode("\u00A0")); // nbsp
367
sep_tr.appendChild( sep_td );
368
table.appendChild( sep_tr );
370
// did we bump the count higher?
371
if (prefsData.count == idx) {
376
function showPreferences(prefsDiv)
379
var table = document.createElement('table');
380
prefsDiv.appendChild(table);
382
// get the count and initialize arrays
383
var count = prefsData.count;
387
tr = document.createElement('tr');
388
table.appendChild(tr);
390
for (var field in prefsFields) {
391
var fieldname = prefsFields[field];
392
if (fieldname == 'standard') continue;
393
if (fieldname == 'comment') continue;
395
var th = document.createElement('th');
396
// why doesn't this alignment work?
397
//th.setAttribute('align','left');
398
th.appendChild(document.createTextNode(fieldname));
403
// load the preferences
404
var reload_time_seen = false;
405
for (var idx = 0; idx < count; idx ++) {
407
if (prefsData['standard'][idx] == 'yes' && !reload_time_seen) {
408
var time = new Date();
409
time.setUTCMilliseconds( prefsData.reloadAt );
415
sep_td = document.createElement('td');
416
sep_tr = document.createElement('tr');
417
sep_td.appendChild(document.createTextNode("\u00A0")); // nbsp
418
sep_tr.appendChild( sep_td );
419
table.appendChild( sep_tr );
421
// report auto-reload time
422
sep_tr = document.createElement('tr');
423
sep_td = document.createElement('td');
424
var sep_span = document.createElement('span');
425
sep_td.setAttribute('colspan', prefsFields.length - 2);
426
sep_span.appendChild(document.createTextNode("Standard Replies (next auto-reload at: "+ time.toString() +")"));
427
sep_span.setAttribute('style','font-weight: bold;');
428
sep_td.appendChild( sep_span );
429
sep_tr.appendChild( sep_td );
430
table.appendChild( sep_tr );
433
sep_td = document.createElement('td');
434
sep_tr = document.createElement('tr');
435
sep_td.appendChild(document.createTextNode("\u00A0")); // nbsp
436
sep_tr.appendChild( sep_td );
437
table.appendChild( sep_tr );
439
reload_time_seen = true;
442
addRowPreferences(table, idx);
445
// Show pref-control buttons
446
tr = document.createElement('tr');
447
table.appendChild(tr);
450
var td = document.createElement('td');
451
var click = document.createElement('a');
452
click.href = document.location + "#";
453
click.title = "Expand form with a new blank entry for stock replies (remember to click save!)";
454
click.appendChild(document.createTextNode("Add New Stock Reply"));
455
click.addEventListener('click', function(e) {
458
addRowPreferences(table, prefsData.count);
462
insert_clickable(td, click, false);
466
var td = document.createElement('td');
467
var click = document.createElement('a');
468
click.title = "Save the stock replies to disk (Important Note: You will need to restart firefox for the replies to save permanently)";
469
click.href = document.location + "#";
470
click.appendChild(document.createTextNode("Save Stock Replies"));
471
click.addEventListener('click', function(e) {
476
alert('Replies Saved');
480
insert_clickable(td, click, false);
485
function savePreferences()
488
GM_setValue('count', ''+prefsData.count);
489
// save standard-reply-reload date
490
GM_setValue('reload-at', ''+prefsData.reloadAt);
492
// save the preferences
493
for (var idx = 0; idx < prefsData.count; idx ++) {
494
for (var field in prefsFields) {
495
//alert("Saving "+prefsFields[field]+idx);
496
GM_setValue(prefsFields[field]+idx, prefsData[prefsFields[field]][idx]);
500
// redisplay the prefs!
506
function reloadReplies(title) {
507
var element = document.createElement('a');
508
element.href = document.location + "#";
509
var innerTextElement = document.createTextNode(title);
510
element.title = "Reload the replies from preferences";
511
element.appendChild(innerTextElement);
512
element.addEventListener('click', function(e) {
523
function reloadStandardReplies(title) {
524
var element = document.createElement('a');
525
element.href = document.location + "#";
526
var innerTextElement = document.createTextNode(title);
527
element.appendChild(innerTextElement);
528
element.title = "Reload the standard replies from remote website";
529
element.addEventListener('click', function(e) {
532
loadStandardReplies();
533
alert('Refreshing Standard Replies');
541
var prefsId = 'lp_sr_prefs';
542
function hidePreferences() {
543
var prefs = document.getElementById(prefsId);
545
prefs.parentNode.removeChild(prefs);
550
function popPreferences(title) {
551
var element = document.createElement('a');
552
element.href = document.location + "#";
553
var innerTextElement = document.createTextNode(title);
554
element.title = "Display the stock replies preferences form";
555
element.appendChild(innerTextElement);
556
element.addEventListener('click', function(e) {
559
// create the dialog if it doesn't exist yet
560
if (prefsDiv === null) {
561
prefsDiv = document.createElement('div');
562
prefsDiv.setAttribute('id',prefsId);
564
showPreferences(prefsDiv);
567
// locate the prior dialog location
568
var prefs = document.getElementById(prefsId);
569
if (!prefs || (prefs.parentNode != element.parentNode)) {
570
// if prefs already exists in the DOM, drop it from prior
571
// location, so we can attach it to the current element.
572
/* oh, this seems to happen automatically. Thanks, DOM.
574
prefs.parentNode.removeChild(prefs);
577
element.parentNode.insertBefore(prefsDiv, prefsDiv.nextSibling);
580
prefs.parentNode.removeChild(prefs);
588
function remove_replies() {
589
var allReplies = xpath("//*[@class='"+reply_class+"']");
590
for (var i = 0; i < allReplies.snapshotLength; i++) {
591
var thisReply = allReplies.snapshotItem(i);
592
thisReply.parentNode.removeChild(thisReply);
596
function show_replies() {
597
var allForms = xpath("//form");
598
for (var i = 0; i < allForms.snapshotLength; i++) {
599
var thisForm = allForms.snapshotItem(i);
600
//var thisInput = xpath(".//input[contains(@name, '.sourcepackagename') or contains(@name, '.product')]", thisForm);
601
var thisInput = xpath(".//input[contains(@name, '.target')]", thisForm);
602
if (thisInput.snapshotLength == 0) {
605
var formname = thisInput.snapshotItem(0).name;
606
formname = formname.substr(0, formname.lastIndexOf("."));
607
var thisSubmit = xpath(".//label[contains(@for, '.comment_on_change')]", thisForm).snapshotItem(0);
609
// append all stock replies
610
for (var idx = 0; idx < prefsData.count; idx++) {
613
if (prefsData['standard'][idx] == "yes") {
617
insert_clickable(thisSubmit.parentNode,
618
injectStockreply(formname, idx), true, left, right);
621
// Add preferences "button"
622
insert_clickable(thisSubmit.parentNode, popPreferences("+edit+"), true);
623
//insert_clickable(thisSubmit.parentNode, reloadReplies("*"), true);
624
insert_clickable(thisSubmit.parentNode, reloadStandardReplies("+reload+"), true);
629
window.addEventListener("load", function(e) {
632
// load standard replies if none are already in the preferences, or
633
// if the "reloadAt" preference has expired
634
var time = new Date();
635
if (!prefsData.standardSeen ||
636
time.getUTCMilliseconds() > prefsData.reloadAt) {
637
loadStandardReplies();