Merge lp:~bryce/launchpad-gm-scripts/stockreplies-revamp into lp:launchpad-gm-scripts

Proposed by Bryce Harrington
Status: Merged
Merged at revision: 188
Proposed branch: lp:~bryce/launchpad-gm-scripts/stockreplies-revamp
Merge into: lp:launchpad-gm-scripts
Diff against target: 930 lines (+268/-626)
2 files modified
README (+3/-4)
lp_stockreplies.user.js (+265/-622)
To merge this branch: bzr merge lp:~bryce/launchpad-gm-scripts/stockreplies-revamp
Reviewer Review Type Date Requested Status
Brian Murray Approve
Review via email: mp+377470@code.launchpad.net

Description of the change

This is a rewrite of the stockreplies script.

* The core functionality all works - stock reply data is loaded from external files, and presented to the user as clickable links. On activation, the links fill in the bug report text area, and sets other form widgets like assignee, importance, package, etc.

* Modern Greasemonky drops support for dynamically loading external XML files, but has a reliable mechanism for loading JSON files. So, with this change, stockreply data is stored as JSON rather than XML.

* The previous script implementation had a live-edit functionality. In addition to no longer functioning, this involved a large amount of UI code making it difficult to debug. To simplify script maintenance, this feature has been dropped.

* Due to the above two changes there is no longer a 'reload' functionality. I'd like to have this, but haven't figured out a way to implement it.

I've taken the opportunity to cleanup and restructure the code for the script, which I hope makes it a bit easier to maintain going forward.

To post a comment you must log in.
Revision history for this message
Brian Murray (brian-murray) wrote :

Thanks for working on this, I'm excited to have it functioning again. One thing I seem to recall (but I could be wrong) about the previous one was that you could have different sets of replies for different packages. This ended up reducing the number of replies you had to browse through when trying to find the right one. Does that sound familiar to you and if so does your script do that?

When testing this with a bug report without a package the description ended up containing the following oddity:

"report this bug, LP #1857789 against 1857789"

It looks like PKGNAME had the same value as BUGNUMBER.

I also noticed that importance was not being unset if there was no importance defined in the json file e.g. clicking "good" then "missing". Rather than setting it to "Undecided" if importance is null it should probably set it to whatever the importance originally was to prevent mistakenly overwriting data. That same thing also appears to be true when the package is not defined in the json file.

Otherwise this look great thanks again for working on it.

review: Needs Fixing
Revision history for this message
Bryce Harrington (bryce) wrote :

Per-package replies would be nice - I filed #845203 way back when to ask for something like that. I don't think I've seen this functionality, could have been done in a fork?

In any case, I did give some thought to this while doing this reimplementation. An approach might be to have multiple .json files each with their own sets of responses for different packages, or even different triaging contexts. Then have the script load several of these .json files and toggle them on or off either by user selection or detection by patch name or other condition. But I decided to just stay focused on getting the basics back in service, and leave that idea for future follow-on work, if enough other people end up using this.

The issue with things not being set back to the original value is an existing bug in the old script, LP: #334040. I was also going to leave this as follow on work, but I can take a look.

And yeah, the script's logic doesn't get the pkgname when there isn't a source package. Probably similar corner cases for undefined users and so on. The logic I'm using for pkgname is lifted directly from the old script so it probably had the same issue. I can also take a look at this.

Revision history for this message
Brian Murray (brian-murray) wrote :

Looking again its lp_buttontags.user.js which selectively displays tags for different packages and projects. Sorry for the confusion!

186. By Bryce Harrington

Only set PKGNAME for bug tasks identifying a source package

187. By Bryce Harrington

If there is no value to substitute, leave key for user to manually fill in

This isn't great, but it's better than displaying 'null' in the comment.
At least the all-caps keyword should be noticeable.

188. By Bryce Harrington

Restore original settings when selecting a response with undefined values

If a given stock reply has an empty or missing value for 'package' the
script would leave the package widget untouched. If a previously
selected stock reply had inserted a value for package, this would cause
the new stock reply to show that changed value - requiring the user to
have to notice and fix it up manually. Instead, keep track of what the
original package was, and restore to that value when it's undefined in
the stock reply.

Follow this same behavior pattern for importance, status, assignee, and
comment.

Fixes: https://bugs.launchpad.net/launchpad-gm-scripts/+bug/334040

Revision history for this message
Bryce Harrington (bryce) wrote :

Brian, I think you'll find this fixes the package assignment a lot cleaner now, please have another look when you get a chance.

Try it also on packages with no source package, it should work a bit more sensibly now.

Revision history for this message
Brian Murray (brian-murray) wrote :

I've tested the latest version of the script and it seems to address all the concerns I had. Thanks!

review: Approve
Revision history for this message
Bryce Harrington (bryce) wrote :

Thanks Brian, the changes are landed in master, and I've written a discourse post about it:

https://discourse.ubuntu.com/t/stock-replies-script/14103

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2011-09-09 00:10:12 +0000
3+++ README 2020-01-18 03:04:47 +0000
4@@ -11,10 +11,9 @@
5 to do javascript-ish things on top of certain web pages. To install
6 GreaseMonkey, follow these steps:
7
8- * Go to Firefox->Tools->AddOns
9- * Click Get Extensions (or Get Ubuntu AddOns if on Ubuntu)
10- * Select GreaseMonkey, and install
11- * Restart Firefox
12+ * From Firefox's "hamburger menu" go to AddOns
13+ * In the search field type "Greasemonkey"
14+ * Select GreaseMonkey, and "+ Add to Firefox"
15
16 Then to install these scripts, run:
17
18
19=== modified file 'lp_stockreplies.user.js'
20--- lp_stockreplies.user.js 2013-05-13 15:46:00 +0000
21+++ lp_stockreplies.user.js 2020-01-18 03:04:47 +0000
22@@ -1,31 +1,28 @@
23 // ==UserScript==
24-// @name LP_StockReplies
25-// @namespace http://outflux.net/greasemonkey/
26-// @description (Launchpad) Stock replies
27-// @include https://launchpad.net/*
28-// @include https://*.launchpad.net/*
29-// @include https://*.edge.launchpad.net/*
30-// @version 1.5
31-// @date 2009-12-22
32-// @creator Kees Cook <kees@ubuntu.com>
33-// @contributor Brian Murray <brian@ubuntu.com>
34-// @contributor Bryce Harrington <bryce@ubuntu.com>
35+// @name LP_Stock_Replies_NG
36+// @author Bryce Harrington <bryce@canonical.com>
37+// @namespace http://www.bryceharrington.org/greasemonkey/
38+// @description Pre-defined replies for Launchpad bug reports
39+// @include https://bugs.launchpad.net/*
40+// @include https://bugs.edge.launchpad.net/*
41+// @version 1.6
42+// @grant none
43+// @require http://www.bryceharrington.org/greasemonkey/responses.js?
44 // ==/UserScript==
45-// Based on code originally written by:
46-// Tollef Fog Heen <tfheen@err.no>
47-// Brian Murray <brian@ubuntu.com>
48-
49-(function () {
50- var SCRIPT = {
51- name: "LP_StockReplies",
52- namespace: "http://outflux.net/greasemonkey/",
53- description: '(Launchpad) Stock replies',
54- source: "http://codebrowse.launchpad.net/~ubuntu-dev/ubuntu-gm-scripts/ubuntu/files",
55- identifier: "http://codebrowse.launchpad.net/~ubuntu-dev/ubuntu-gm-scripts/ubuntu/file/lp_stockreplies.user.js",
56- version: "1.6",
57- date: (new Date(2013, 05, 13))// update date
58- .valueOf()
59- };
60+
61+// Note: To force reloading the @require data above, append or delete "?" from the URL,
62+// and then reload the page.
63+
64+// Based on code originally created by:
65+// Tollef Fog Heen <tfheen@err.no>
66+// Kees Cook <kees@ubuntu.com>
67+// Bryce Harrington <bryce@ubuntu.com>
68+// Brian Murray <brian@ubuntu.com>
69+
70+
71+////////////////////////
72+//// Document Model ////
73+////////////////////////
74
75 function xpath(query, context) {
76 context = context ? context : document;
77@@ -34,611 +31,257 @@
78 }
79
80 String.prototype.ucFirst = function () {
81- return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
82+ return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
83 };
84
85-var debug = 0;
86-
87-var prefsData = new Object;
88-var prefsFields = new Array(
89- "name", // required -- the clickable name
90- "comment", // required -- the stock reply!
91- "status", // "" == leave unchanged
92- "tip", // tooltip hint (optional)
93- "assign", // "" == leave unchanged
94- // "-me" == assign to self
95- // "-nobody" == assign to nobody
96- "importance", // "" == leave unchanged
97- "package", // "" == leave unchanged
98- "standard" // "yes" == is from standard XML?
99- );
100-
101-
102-// This routine is called once per stock reply item at time of page loading
103-function injectStockreply(formname, idx) {
104-
105+//////////////////////////
106+//// Form Data Access ////
107+//////////////////////////
108+
109+// Textarea
110+function get_textarea(form_name, widget_name) {
111+ var id = widget_name;
112+ return xpath("//textarea[contains(@name, '"+id+"')]").snapshotItem(0).value;
113+}
114+
115+// String (text input)
116+function get_text_input(form_name, widget_name) {
117+ var id = form_name + '.' + widget_name;
118+ return xpath('//input[@id="'+id+'"]').snapshotItem(0).value;
119+}
120+
121+// Checkbox
122+function get_checkbox_input(form_name, widget_name) {
123+ var id = form_name + '.' + widget_name;
124+ return xpath('//input[@value="'+ id + '"]').snapshotItem(0).checked;
125+}
126+
127+// Radio Button
128+function get_radio_input(form_name, widget_name) {
129+ var id = form_name + '.' + widget_name;
130+ // TODO
131+}
132+
133+// Drop Down
134+function get_select(form_name, widget_name) {
135+ var id = form_name + '.' + widget_name;
136+ var widget = xpath('//select[@id="'+id+'"]/option[@selected="selected"]').snapshotItem(0);
137+ return widget.value;
138+}
139+
140+
141+///////////////////////////
142+//// Form Modification ////
143+///////////////////////////
144+
145+// Textarea
146+function set_textarea(form_name, widget_name, value) {
147+ var id = widget_name;
148+ xpath("//textarea[contains(@name, '"+id+"')]").snapshotItem(0).value = value;
149+ console.log('Set '+id+' to '+value);
150+}
151+
152+// String (text input)
153+function set_text_input(form_name, widget_name, value) {
154+ var id = form_name + '.' + widget_name;
155+ xpath('//input[@id="'+id+'"]').snapshotItem(0).value = value;
156+ console.log('Set '+id+' to ' + value);
157+}
158+
159+// Checkbox
160+function set_checkbox_input(form_name, widget_name, value) {
161+ var id = form_name + '.' + widget_name;
162+ xpath('//input[@value="'+ id + '"]').snapshotItem(0).checked = value;
163+ console.log('Set '+id+' to '+value);
164+}
165+
166+// Radio Button
167+function set_radio_input(form_name, widget_name, value) {
168+ var id = form_name + '.' + widget_name;
169+ // TODO
170+ console.log('Set '+widget_name+' to '+value);
171+}
172+
173+// Drop Down
174+function set_select(form_name, widget_name, value) {
175+ var id = form_name + '.' + widget_name;
176+ var widget = xpath('//select[@id="'+id+'"]/option[.="'+value+'"]').snapshotItem(0);
177+ widget.selected = true;
178+ console.log('Set '+id+' to ' + value);
179+}
180+
181+
182+
183+///////////////////////////////
184+//// Stock Reply Functions ////
185+///////////////////////////////
186+
187+// Hyperlink [Action]
188+function insert_clickable(node, element) {
189+ var span = document.createElement("span");
190+
191+ // Fill span
192+ span.appendChild(document.createTextNode("["));
193+ span.appendChild(element);
194+ span.appendChild(document.createTextNode("]"));
195+ span.appendChild(document.createTextNode("\n"));
196+
197+ // Insert span
198+ node.insertBefore(span, span.nextSibling);
199+}
200+
201+// Fills in text template using key:value pairs from the data hashmap
202+function substitute_values(template, data) {
203+ var text = template
204+ for (var key in data) {
205+ var value = data[key];
206+ if (value) {
207+ // If there is no value to substitute, leave the key in place for user to manually fill in
208+ text = text.replace(key, value);
209+ }
210+ }
211+ // TODO: Assure there are no un-substituted values remaining
212+ return text;
213+}
214+
215+function get_bug_details() {
216+ var path_name = window.location.pathname;
217+ var details = {
218+ 'PROJECTNAME': path_name.split('/')[1].ucFirst(),
219+ 'PKGNAME': null,
220+ 'BUGNUMBER': path_name.split('/').pop(),
221+ 'REPORTER': xpath("//*[@class='registering']/*[contains(@class,'sprite person')]").snapshotItem(0).firstChild.nodeValue,
222+ 'UPSTREAMBUG': xpath("//*[@class='link-external']").snapshotItem(0)
223+ }
224+ if (path_name.split('/')[2] == "source") {
225+ details['PKGNAME'] = path_name.split('/')[3];
226+ }
227+ return details;
228+}
229+
230+function get_original_values(textarea_input) {
231+ var textarea_name = textarea_input.name;
232+ var form_name = textarea_name.substr(0, textarea_name.lastIndexOf("."));
233+ var original_values = {
234+ 'assignee': null,
235+ 'package': get_text_input(form_name, 'target.package'),
236+ 'comment': get_textarea(form_name, textarea_name),
237+ 'importance': get_select(form_name, 'importance'),
238+ 'status': get_select(form_name, 'status')
239+ }
240+
241+ if (get_checkbox_input(form_name, 'assignee.assign_to_me')) {
242+ original_values['assignee'] = '-me';
243+ } else if (get_checkbox_input(form_name, 'assignee.assign_to_nobody')) {
244+ original_values['assignee'] = '-nobody';
245+ } else {
246+ original_values['assignee'] = get_text_input(form_name, 'assignee');
247+ }
248+
249+ return original_values;
250+}
251+
252+// Adds the html elements to the form for a reply button with a callback function
253+function create_stock_reply_element(textarea_input, response, original_values) {
254 var element = document.createElement('a');
255 element.href = document.location + "#";
256- var innerTextElement = document.createTextNode(prefsData['name'][idx]);
257-
258- // Default to using comment as tooltip, when tooltip is missing
259- tip = prefsData['tip'][idx];
260- if (tip == "") {
261- tip = prefsData['comment'][idx];
262- }
263- element.title = tip;
264-
265- element.appendChild(innerTextElement);
266+ var inner_text_element = document.createTextNode(response['name']);
267+ var textarea_name = textarea_input.name;
268+ var form_name = textarea_name.substr(0, textarea_name.lastIndexOf("."));
269+ console.log("form_name = " + form_name);
270+
271+ element.title = response['tip'];
272+ element.appendChild(inner_text_element);
273 element.addEventListener('click', function(e) {
274 e.preventDefault();
275
276- // Retrieve bug details
277- var pathname = window.location.pathname;
278- var bug_project = pathname.split('/')[1].ucFirst();
279- // if the bug has no package this ends up being the bug number
280- var bug_package = pathname.split('/')[3];
281- var bug_number = pathname.split('/').pop();
282- var bug_reporter = xpath("//*[@class='registering']/*[contains(@class,'sprite person')]").snapshotItem(0).firstChild.nodeValue;
283- var bug_upstream = xpath("//*[@class='link-external']").snapshotItem(0);
284-
285- // Set comment
286- var comment_text = prefsData['comment'][idx];
287- comment_text = comment_text.replace("PROJECTNAME", bug_project);
288- comment_text = comment_text.replace("BUGNUMBER", bug_number);
289- // only replace the package name for bugs with a package i.e. don't do it for no package bugs
290- if (bug_number != bug_package) {
291- comment_text = comment_text.replace("PKGNAME", bug_package);
292- }
293- comment_text = comment_text.replace("REPORTER", bug_reporter);
294- if (bug_upstream != null) {
295- comment_text = comment_text.replace("UPSTREAMBUG", bug_upstream.href);
296- }
297- xpath('//textarea[@id="'+ formname + '.comment_on_change"]').snapshotItem(0).value = comment_text;
298-
299- // Set status
300- if (prefsData['status'][idx] != "") {
301- xpath('//select[@id="'+ formname + '.status"]/option[.="'+prefsData['status'][idx].replace(/"/,'\\"')+'"]').snapshotItem(0).selected = true;
302- }
303-
304- // Set assignee
305- if (prefsData['assign'][idx] != "") {
306- if (prefsData['assign'][idx] == "-me") {
307- xpath('//input[@value="'+ formname + '.assignee.assign_to_me"]').snapshotItem(0).checked = true;
308- }
309- else if (prefsData['assign'][idx] == "-nobody") {
310- xpath('//input[@value="'+ formname + '.assignee.assign_to_nobody"]').snapshotItem(0).checked = true;
311- }
312- else {
313- xpath('//input[@value="'+ formname + '.assignee.assign_to"]').snapshotItem(0).checked = true;
314- xpath('//input[@id="'+ formname + '.assignee"]').snapshotItem(0).value = prefsData['assign'][idx];
315- }
316- }
317-
318- // Set package
319- if (prefsData['package'][idx] != "") {
320- xpath('//input[@name="'+ formname + '.target.package"]').snapshotItem(0).value = prefsData['package'][idx];
321- }
322-
323- // Set importance
324- if (prefsData['importance'][idx] != "") {
325- xpath('//select[@id="'+ formname + '.importance"]/option[.="'+prefsData['importance'][idx].replace(/"/,'\\"')+'"]').snapshotItem(0).selected = true;
326- }
327-
328- // Subscribe triager by default
329-// var sub = xpath('//input[@id="subscribe"]').snapshotItem(0);
330-// if (sub) sub.checked = true;
331+ // Retrieve bug details that we can substitute
332+ var bug_details = get_bug_details();
333+
334+ // Set form elements
335+ var comment_text = substitute_values(response['comment'], bug_details);
336+ if (comment_text) {
337+ set_textarea(form_name, textarea_name, comment_text);
338+ } else {
339+ set_textarea(form_name, textarea_name, original_values['comment']);
340+ }
341+
342+ if (response['status']) {
343+ set_select(form_name, 'status', response['status']);
344+ } else {
345+ set_select(form_name, 'status', original_values['status']);
346+ }
347+
348+ if (response['importance']) {
349+ set_select(form_name, 'importance', response['importance']);
350+ } else {
351+ set_select(form_name, 'importance', original_values['importance']);
352+ }
353+
354+ if (response['package']) {
355+ set_text_input(form_name, 'target.package', response['package']);
356+ } else {
357+ set_text_input(form_name, 'target.package', original_values['package']);
358+ }
359+
360+ if (response['assignee']) {
361+ console.log("assignee == " + response['assignee']);
362+ if (response['assignee'] == '-me') {
363+ set_checkbox_input(form_name, 'assignee.assign_to_me', true);
364+ } else if (response['assignee'] == '-nobody') {
365+ set_checkbox_input(form_name, 'assignee.assign_to_nobody', true);
366+ } else if (response['assignee'] != "") {
367+ console.log("Setting assignee");
368+ set_checkbox_input(form_name, 'assignee.assign_to', true);
369+ set_text_input(form_name, 'assignee', response['assignee']);
370+ }
371+ } else {
372+ if (original_values['assignee'] == '-me') {
373+ set_checkbox_input(form_name, 'assignee.assign_to_me', true);
374+ set_text_input(form_name, 'assignee', "");
375+ } else if (original_values['assignee'] == '-nobody') {
376+ set_checkbox_input(form_name, 'assignee.assign_to_nobody', true);
377+ set_text_input(form_name, 'assignee', "");
378+ } else if (original_values['assignee'] != "") {
379+ console.log("Restoring assignee");
380+ set_checkbox_input(form_name, 'assignee.assign_to', true);
381+ set_text_input(form_name, 'assignee', original_values['assignee']);
382+ }
383+ }
384
385 return false;
386 }, false);
387- return element;
388-}
389-
390-var reply_class = 'lp_sr';
391-function insert_clickable(node, newElement, tagged, left, right)
392-{
393- if (!left) { left='['; }
394- if (!right) { right=']'; }
395- var span = document.createElement("span");
396- var leftBrace = document.createTextNode(left);
397- var rightBrace = document.createTextNode(right+' ');
398-
399- /* mark up for future removal? */
400- if (tagged) {
401- span.setAttribute('class',reply_class)
402- }
403-
404- /* fill span */
405- span.appendChild(leftBrace);
406- span.appendChild(newElement);
407- span.appendChild(rightBrace);
408- // make the source readable
409- span.appendChild(document.createTextNode("\n"));
410-
411- /* insert span */
412- node.insertBefore(span, span.nextSibling);
413-}
414-
415-function deleteReply(idx)
416-{
417- var count = parseInt(GM_getValue('count',0))
418- if (count == 0) return;
419- if (idx >= count) return;
420- if (idx < 0) return;
421- /* move all the prefs up one to wipe out the deleted one */
422- for (var move = idx + 1; move < count; move ++) {
423- for (var field in prefsFields) {
424- for (var field in prefsFields) {
425- var fieldname = prefsFields[field];
426- GM_setValue(fieldname+(move-1),GM_getValue(fieldname+move,""));
427- }
428- }
429- }
430- GM_setValue('count',''+(count-1))
431- /* since we've deleted a reply, caller needs to reload this script's
432- view of the GM prefs via loadPreferences() */
433-}
434-
435-function clearStandardReplies()
436-{
437- var count = parseInt(GM_getValue('count', 0));
438- for (var idx = count - 1; idx >= 0; idx --) {
439- standard = GM_getValue('standard'+idx,"");
440- if (standard == "yes") {
441- deleteReply(idx, false);
442- }
443- }
444- loadPreferences();
445-}
446-
447-function loadPreferences()
448-{
449- prefsData.standardSeen = false;
450- prefsData.count = parseInt(GM_getValue('count', 0));
451- for (var field in prefsFields) {
452- var fieldname = prefsFields[field];
453- prefsData[fieldname] = new Array;
454-
455- for (var idx = 0; idx < prefsData.count; idx ++) {
456- prefsData[fieldname][idx] = GM_getValue(fieldname+idx,"");
457- }
458- }
459- for (var idx = 0; idx < prefsData.count; idx ++) {
460- if (prefsData['standard'][idx] == "yes") {
461- prefsData.standardSeen = true;
462- }
463- }
464- prefsData.reloadAt = parseInt(GM_getValue('reload-at', 0));
465-}
466-
467-function loadStandardReplies() {
468- GM_xmlhttpRequest
469- (
470- {
471- method: 'GET',
472- url: 'http://people.canonical.com/~brian/greasemonkey/bugsquad-replies.xml',
473- headers: {
474- 'Accept': 'application/atom+xml,application/xml,text/xml',
475- },
476- onload: function(results) {
477- var parser = new DOMParser();
478- var dom = parser.parseFromString(results.responseText,"application/xml");
479- var replies = dom.getElementsByTagName('reply');
480- // destroy preferences for possible reload
481- hidePreferences();
482- /* if we actually have some replies, clear the old ones */
483- if (debug) {
484- alert("Dropping old standard stock replies");
485- }
486- if (replies.length>0) {
487- clearStandardReplies();
488- }
489- if (debug) {
490- alert("Parsing new standard stock replies");
491- }
492- var base = prefsData.count;
493- for (var i=0; i < replies.length; i++) {
494- var standardReply = new Array;
495- for (var field in prefsFields) {
496- var fieldname = prefsFields[field];
497- var text = "";
498- if (fieldname == "standard") {
499- text = "yes";
500- }
501- else {
502- // alert("Want " + fieldname + " (offset " + i + ")");
503- element = replies[i].getElementsByTagName(fieldname);
504- if (element.length) {
505- text = element[0].textContent;
506- }
507- }
508- prefsData[fieldname][base+i] = text;
509- // alert(fieldname + " as [" + text + "] at " + (base+i));
510- if (debug) {
511- if (fieldname == "name") {
512- alert("Parsed [" + text + "]");
513- }
514- }
515- }
516- }
517- prefsData.count += replies.length;
518- // reload again in 1.5 days
519- var time = new Date();
520- prefsData.reloadAt = time.getUTCMilliseconds() + (1000 * 60 * 60 * 36);
521- if (debug) {
522- alert("Saving stock replies");
523- }
524- savePreferences();
525- if (debug) {
526- alert("Finished reloading standard stock replies");
527- }
528- }
529- }
530- )
531-}
532-
533-function addColumnPreference(idx,fieldname)
534-{
535- var td = document.createElement('td');
536- // why doesn't this alignment work?
537- //td.setAttribute('valign','top');
538-
539- var id = reply_class + '.' + idx + '.' + fieldname;
540-
541- var label = document.createElement('label');
542- label.setAttribute('style','font-weight: bold;');
543- label.setAttribute('for',id);
544- label.appendChild(document.createTextNode(fieldname));
545- td.appendChild(label);
546-
547- var input;
548- if (fieldname == 'comment') {
549- input = document.createElement('textarea');
550- // match current LP comment field size
551- input.setAttribute('cols','62');
552- input.setAttribute('rows','4');
553- }
554- else {
555- input = document.createElement('input');
556- input.setAttribute('type','text');
557- input.setAttribute('size','15');
558- }
559- input.value = prefsData[fieldname][idx];
560- input.setAttribute('name',fieldname);
561- input.setAttribute('id',id);
562- //alert('added ('+fieldname+','+idx+'): '+input.value);
563- input.addEventListener('change', function(e) {
564- e.preventDefault();
565-
566- var obj = e.target;
567- var fieldname = obj.getAttribute('name');
568- //alert('changed ('+fieldname+','+idx+'): '+obj.value);
569- if (prefsData[fieldname][idx] != obj.value) {
570- /* mark as non-standard if it was changed */
571- prefsData['standard'][idx] = "";
572- }
573- prefsData[fieldname][idx] = obj.value;
574-
575- return false;
576- }, false);
577- td.appendChild(input);
578-
579- // make the source readable
580- td.appendChild(document.createTextNode("\n"));
581-
582- return td;
583-}
584-
585-function addRowPreferences(table,idx)
586-{
587- /* TODO: mark this row in some way if it is a standard reply */
588- var tr = document.createElement('tr');
589- for (var field in prefsFields) {
590- var fieldname = prefsFields[field];
591- if (fieldname == 'standard') continue;
592- if (fieldname == 'comment') continue;
593-
594- // set up empty default
595- if (!prefsData[fieldname][idx]) {
596- prefsData[fieldname][idx]="";
597- }
598-
599- td = addColumnPreference(idx,fieldname);
600- tr.appendChild(td);
601- }
602- table.appendChild(tr);
603-
604- // make the source readable
605- table.appendChild(document.createTextNode("\n"));
606-
607- // add "comment" input separately since it is a textarea
608- var comment_tr = document.createElement('tr');
609- var comment_td = addColumnPreference(idx,'comment');
610- comment_td.setAttribute('colspan', prefsFields.length - 2);
611- comment_tr.appendChild( comment_td );
612- table.appendChild(comment_tr);
613-
614- // make the source readable
615- table.appendChild(document.createTextNode("\n"));
616-
617- // spacer
618- var sep_td = document.createElement('td');
619- var sep_tr = document.createElement('tr');
620- sep_td.appendChild(document.createTextNode("\u00A0")); // nbsp
621- sep_tr.appendChild( sep_td );
622- table.appendChild( sep_tr );
623-
624- // did we bump the count higher?
625- if (prefsData.count == idx) {
626- prefsData.count++;
627- }
628-}
629-
630-function showPreferences(prefsDiv)
631-{
632- var tr;
633- var table = document.createElement('table');
634- prefsDiv.appendChild(table);
635-
636- // get the count and initialize arrays
637- var count = prefsData.count;
638-
639-/*
640- // table headers
641- tr = document.createElement('tr');
642- table.appendChild(tr);
643-
644- for (var field in prefsFields) {
645- var fieldname = prefsFields[field];
646- if (fieldname == 'standard') continue;
647- if (fieldname == 'comment') continue;
648-
649- var th = document.createElement('th');
650- // why doesn't this alignment work?
651- //th.setAttribute('align','left');
652- th.appendChild(document.createTextNode(fieldname));
653- tr.appendChild(th);
654- }
655-*/
656-
657- // load the preferences
658- var reload_time_seen = false;
659- for (var idx = 0; idx < count; idx ++) {
660-
661- if (prefsData['standard'][idx] == 'yes' && !reload_time_seen) {
662- var time = new Date();
663- time.setUTCMilliseconds( prefsData.reloadAt );
664-
665- var sep_tr;
666- var sep_td;
667-
668- // spacer
669- sep_td = document.createElement('td');
670- sep_tr = document.createElement('tr');
671- sep_td.appendChild(document.createTextNode("\u00A0")); // nbsp
672- sep_tr.appendChild( sep_td );
673- table.appendChild( sep_tr );
674-
675- // report auto-reload time
676- sep_tr = document.createElement('tr');
677- sep_td = document.createElement('td');
678- var sep_span = document.createElement('span');
679- sep_td.setAttribute('colspan', prefsFields.length - 2);
680- sep_span.appendChild(document.createTextNode("Standard Replies (next auto-reload at: "+ time.toString() +")"));
681- sep_span.setAttribute('style','font-weight: bold;');
682- sep_td.appendChild( sep_span );
683- sep_tr.appendChild( sep_td );
684- table.appendChild( sep_tr );
685-
686- // spacer
687- sep_td = document.createElement('td');
688- sep_tr = document.createElement('tr');
689- sep_td.appendChild(document.createTextNode("\u00A0")); // nbsp
690- sep_tr.appendChild( sep_td );
691- table.appendChild( sep_tr );
692-
693- reload_time_seen = true;
694- }
695-
696- addRowPreferences(table, idx);
697- }
698-
699- // Show pref-control buttons
700- tr = document.createElement('tr');
701- table.appendChild(tr);
702-
703- // Expand list
704- var td = document.createElement('td');
705- var click = document.createElement('a');
706- click.href = document.location + "#";
707- click.title = "Expand form with a new blank entry for stock replies (remember to click save!)";
708- click.appendChild(document.createTextNode("Add New Stock Reply"));
709- click.addEventListener('click', function(e) {
710- e.preventDefault();
711-
712- addRowPreferences(table, prefsData.count);
713-
714- return false;
715- }, false);
716- insert_clickable(td, click, false);
717- tr.appendChild(td);
718-
719- // Save preferences
720- var td = document.createElement('td');
721- var click = document.createElement('a');
722- click.title = "Save the stock replies to disk (Important Note: You will need to restart firefox for the replies to save permanently)";
723- click.href = document.location + "#";
724- click.appendChild(document.createTextNode("Save Stock Replies"));
725- click.addEventListener('click', function(e) {
726- e.preventDefault();
727-
728- savePreferences();
729-
730- alert('Replies Saved');
731-
732- return false;
733- }, false);
734- insert_clickable(td, click, false);
735- tr.appendChild(td);
736-
737-}
738-
739-function savePreferences()
740-{
741- // save the count
742- GM_setValue('count', ''+prefsData.count);
743- // save standard-reply-reload date
744- GM_setValue('reload-at', ''+prefsData.reloadAt);
745-
746- // save the preferences
747- for (var idx = 0; idx < prefsData.count; idx ++) {
748- for (var field in prefsFields) {
749- //alert("Saving "+prefsFields[field]+idx);
750- GM_setValue(prefsFields[field]+idx, prefsData[prefsFields[field]][idx]);
751- }
752- }
753-
754- // redisplay the prefs!
755- remove_replies();
756- show_replies();
757-}
758-
759-/*
760-function reloadReplies(title) {
761- var element = document.createElement('a');
762- element.href = document.location + "#";
763- var innerTextElement = document.createTextNode(title);
764- element.title = "Reload the replies from preferences";
765- element.appendChild(innerTextElement);
766- element.addEventListener('click', function(e) {
767- e.preventDefault();
768-
769- remove_replies();
770- show_replies();
771-
772- return false;
773- }, false);
774- return element;
775-}
776-*/
777-function reloadStandardReplies(title) {
778- var element = document.createElement('a');
779- element.href = document.location + "#";
780- var innerTextElement = document.createTextNode(title);
781- element.appendChild(innerTextElement);
782- element.title = "Reload the standard replies from remote website";
783- element.addEventListener('click', function(e) {
784- e.preventDefault();
785-
786- loadStandardReplies();
787- alert('Refreshing Standard Replies');
788-
789- return false;
790- }, false);
791- return element;
792-}
793-
794-var prefsDiv = null;
795-var prefsId = 'lp_sr_prefs';
796-function hidePreferences() {
797- var prefs = document.getElementById(prefsId);
798- if (prefs) {
799- prefs.parentNode.removeChild(prefs);
800- prefsDiv = null;
801- }
802-}
803-
804-function popPreferences(title) {
805- var element = document.createElement('a');
806- element.href = document.location + "#";
807- var innerTextElement = document.createTextNode(title);
808- element.title = "Display the stock replies preferences form";
809- element.appendChild(innerTextElement);
810- element.addEventListener('click', function(e) {
811- e.preventDefault();
812-
813- // create the dialog if it doesn't exist yet
814- if (prefsDiv === null) {
815- prefsDiv = document.createElement('div');
816- prefsDiv.setAttribute('id',prefsId);
817-
818- showPreferences(prefsDiv);
819- }
820-
821- // locate the prior dialog location
822- var prefs = document.getElementById(prefsId);
823- if (!prefs || (prefs.parentNode != element.parentNode)) {
824- // if prefs already exists in the DOM, drop it from prior
825- // location, so we can attach it to the current element.
826- /* oh, this seems to happen automatically. Thanks, DOM.
827- if (prefs) {
828- prefs.parentNode.removeChild(prefs);
829- }
830- */
831- element.parentNode.insertBefore(prefsDiv, prefsDiv.nextSibling);
832- }
833- else {
834- prefs.parentNode.removeChild(prefs);
835- }
836-
837- return false;
838- }, false);
839- return element;
840-}
841-
842-function remove_replies() {
843- var allReplies = xpath("//*[@class='"+reply_class+"']");
844- for (var i = 0; i < allReplies.snapshotLength; i++) {
845- var thisReply = allReplies.snapshotItem(i);
846- thisReply.parentNode.removeChild(thisReply);
847- }
848-}
849-
850-function show_replies() {
851- var allForms = xpath("//form");
852- for (var i = 0; i < allForms.snapshotLength; i++) {
853- var thisForm = allForms.snapshotItem(i);
854- //var thisInput = xpath(".//input[contains(@name, '.sourcepackagename') or contains(@name, '.product')]", thisForm);
855- var thisInput = xpath(".//input[contains(@name, '.target')]", thisForm);
856- if (thisInput.snapshotLength == 0) {
857- continue;
858- }
859- var formname = thisInput.snapshotItem(0).name;
860- formname = formname.substr(0, formname.lastIndexOf("."));
861- var thisSubmit = xpath(".//label[contains(@for, '.comment_on_change')]", thisForm).snapshotItem(0);
862-
863- // append all stock replies
864- for (var idx = 0; idx < prefsData.count; idx++) {
865- var left='{';
866- var right='}';
867- if (prefsData['standard'][idx] == "yes") {
868- left='[';
869- right=']';
870- }
871- insert_clickable(thisSubmit.parentNode,
872- injectStockreply(formname, idx), true, left, right);
873- }
874-
875- // Add preferences "button"
876- insert_clickable(thisSubmit.parentNode, popPreferences("+edit+"), true);
877- //insert_clickable(thisSubmit.parentNode, reloadReplies("*"), true);
878- insert_clickable(thisSubmit.parentNode, reloadStandardReplies("+reload+"), true);
879-
880+
881+ return element;
882+}
883+
884+
885+// Main
886+(function() {
887+ console.log("LP_Stock_Replies_NG: start");
888+
889+ var all_forms = xpath("//form");
890+ for (var form_id=0; form_id<all_forms.snapshotLength; form_id++) {
891+ var this_form = all_forms.snapshotItem(form_id);
892+
893+ // Get all nodes within this form that appear to be relating to bug details
894+ var form_inputs = xpath(".//textarea[contains(@name, '.comment_on_change')]", this_form);
895+ for (var i=0; i<form_inputs.snapshotLength; i++) {
896+ var textarea_input = form_inputs.snapshotItem(i);
897+ console.log("textarea for comment " + textarea_input.name);
898+
899+ // Record original settings for bug details
900+ var original_values = get_original_values(textarea_input);
901+
902+ // Append stock replies
903+ for (var r=0; r<response_data.length; r++) {
904+ console.log(response_data[r]['name']);
905+ var elem = create_stock_reply_element(textarea_input, response_data[r], original_values);
906+ insert_clickable(textarea_input.parentNode, elem);
907+ }
908+ }
909 }
910-}
911-
912-window.addEventListener("load", function(e) {
913-
914- loadPreferences();
915- // load standard replies if none are already in the preferences, or
916- // if the "reloadAt" preference has expired
917- var time = new Date();
918- if (!prefsData.standardSeen ||
919- time.getUTCMilliseconds() > prefsData.reloadAt) {
920- loadStandardReplies();
921- }
922-
923- show_replies();
924-
925-}, false);
926-
927+ console.log("LP_Stock_Replies_NG: end");
928 })();
929+
930+

Subscribers

People subscribed via source and target branches