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
=== modified file 'README'
--- README 2011-09-09 00:10:12 +0000
+++ README 2020-01-18 03:04:47 +0000
@@ -11,10 +11,9 @@
11to do javascript-ish things on top of certain web pages. To install11to do javascript-ish things on top of certain web pages. To install
12GreaseMonkey, follow these steps:12GreaseMonkey, follow these steps:
1313
14 * Go to Firefox->Tools->AddOns14 * From Firefox's "hamburger menu" go to AddOns
15 * Click Get Extensions (or Get Ubuntu AddOns if on Ubuntu)15 * In the search field type "Greasemonkey"
16 * Select GreaseMonkey, and install16 * Select GreaseMonkey, and "+ Add to Firefox"
17 * Restart Firefox
1817
19Then to install these scripts, run:18Then to install these scripts, run:
2019
2120
=== modified file 'lp_stockreplies.user.js'
--- lp_stockreplies.user.js 2013-05-13 15:46:00 +0000
+++ lp_stockreplies.user.js 2020-01-18 03:04:47 +0000
@@ -1,31 +1,28 @@
1// ==UserScript==1// ==UserScript==
2// @name LP_StockReplies2// @name LP_Stock_Replies_NG
3// @namespace http://outflux.net/greasemonkey/3// @author Bryce Harrington <bryce@canonical.com>
4// @description (Launchpad) Stock replies4// @namespace http://www.bryceharrington.org/greasemonkey/
5// @include https://launchpad.net/*5// @description Pre-defined replies for Launchpad bug reports
6// @include https://*.launchpad.net/*6// @include https://bugs.launchpad.net/*
7// @include https://*.edge.launchpad.net/*7// @include https://bugs.edge.launchpad.net/*
8// @version 1.58// @version 1.6
9// @date 2009-12-229// @grant none
10// @creator Kees Cook <kees@ubuntu.com>10// @require http://www.bryceharrington.org/greasemonkey/responses.js?
11// @contributor Brian Murray <brian@ubuntu.com>
12// @contributor Bryce Harrington <bryce@ubuntu.com>
13// ==/UserScript==11// ==/UserScript==
14// Based on code originally written by:12
15// Tollef Fog Heen <tfheen@err.no>13// Note: To force reloading the @require data above, append or delete "?" from the URL,
16// Brian Murray <brian@ubuntu.com>14// and then reload the page.
1715
18(function () {16// Based on code originally created by:
19 var SCRIPT = {17// Tollef Fog Heen <tfheen@err.no>
20 name: "LP_StockReplies",18// Kees Cook <kees@ubuntu.com>
21 namespace: "http://outflux.net/greasemonkey/",19// Bryce Harrington <bryce@ubuntu.com>
22 description: '(Launchpad) Stock replies',20// Brian Murray <brian@ubuntu.com>
23 source: "http://codebrowse.launchpad.net/~ubuntu-dev/ubuntu-gm-scripts/ubuntu/files",21
24 identifier: "http://codebrowse.launchpad.net/~ubuntu-dev/ubuntu-gm-scripts/ubuntu/file/lp_stockreplies.user.js",22
25 version: "1.6",23////////////////////////
26 date: (new Date(2013, 05, 13))// update date24//// Document Model ////
27 .valueOf()25////////////////////////
28 };
2926
30function xpath(query, context) {27function xpath(query, context) {
31 context = context ? context : document;28 context = context ? context : document;
@@ -34,611 +31,257 @@
34}31}
3532
36String.prototype.ucFirst = function () {33String.prototype.ucFirst = function () {
37 return this.substr(0,1).toUpperCase() + this.substr(1,this.length);34 return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
38};35};
3936
40var debug = 0;37//////////////////////////
4138//// Form Data Access ////
42var prefsData = new Object;39//////////////////////////
43var prefsFields = new Array(40
44 "name", // required -- the clickable name41// Textarea
45 "comment", // required -- the stock reply!42function get_textarea(form_name, widget_name) {
46 "status", // "" == leave unchanged43 var id = widget_name;
47 "tip", // tooltip hint (optional)44 return xpath("//textarea[contains(@name, '"+id+"')]").snapshotItem(0).value;
48 "assign", // "" == leave unchanged45}
49 // "-me" == assign to self46
50 // "-nobody" == assign to nobody47// String (text input)
51 "importance", // "" == leave unchanged48function get_text_input(form_name, widget_name) {
52 "package", // "" == leave unchanged49 var id = form_name + '.' + widget_name;
53 "standard" // "yes" == is from standard XML?50 return xpath('//input[@id="'+id+'"]').snapshotItem(0).value;
54 );51}
5552
5653// Checkbox
57// This routine is called once per stock reply item at time of page loading54function get_checkbox_input(form_name, widget_name) {
58function injectStockreply(formname, idx) {55 var id = form_name + '.' + widget_name;
5956 return xpath('//input[@value="'+ id + '"]').snapshotItem(0).checked;
57}
58
59// Radio Button
60function get_radio_input(form_name, widget_name) {
61 var id = form_name + '.' + widget_name;
62 // TODO
63}
64
65// Drop Down
66function get_select(form_name, widget_name) {
67 var id = form_name + '.' + widget_name;
68 var widget = xpath('//select[@id="'+id+'"]/option[@selected="selected"]').snapshotItem(0);
69 return widget.value;
70}
71
72
73///////////////////////////
74//// Form Modification ////
75///////////////////////////
76
77// Textarea
78function set_textarea(form_name, widget_name, value) {
79 var id = widget_name;
80 xpath("//textarea[contains(@name, '"+id+"')]").snapshotItem(0).value = value;
81 console.log('Set '+id+' to '+value);
82}
83
84// String (text input)
85function set_text_input(form_name, widget_name, value) {
86 var id = form_name + '.' + widget_name;
87 xpath('//input[@id="'+id+'"]').snapshotItem(0).value = value;
88 console.log('Set '+id+' to ' + value);
89}
90
91// Checkbox
92function set_checkbox_input(form_name, widget_name, value) {
93 var id = form_name + '.' + widget_name;
94 xpath('//input[@value="'+ id + '"]').snapshotItem(0).checked = value;
95 console.log('Set '+id+' to '+value);
96}
97
98// Radio Button
99function set_radio_input(form_name, widget_name, value) {
100 var id = form_name + '.' + widget_name;
101 // TODO
102 console.log('Set '+widget_name+' to '+value);
103}
104
105// Drop Down
106function set_select(form_name, widget_name, value) {
107 var id = form_name + '.' + widget_name;
108 var widget = xpath('//select[@id="'+id+'"]/option[.="'+value+'"]').snapshotItem(0);
109 widget.selected = true;
110 console.log('Set '+id+' to ' + value);
111}
112
113
114
115///////////////////////////////
116//// Stock Reply Functions ////
117///////////////////////////////
118
119// Hyperlink [Action]
120function insert_clickable(node, element) {
121 var span = document.createElement("span");
122
123 // Fill span
124 span.appendChild(document.createTextNode("["));
125 span.appendChild(element);
126 span.appendChild(document.createTextNode("]"));
127 span.appendChild(document.createTextNode("\n"));
128
129 // Insert span
130 node.insertBefore(span, span.nextSibling);
131}
132
133// Fills in text template using key:value pairs from the data hashmap
134function substitute_values(template, data) {
135 var text = template
136 for (var key in data) {
137 var value = data[key];
138 if (value) {
139 // If there is no value to substitute, leave the key in place for user to manually fill in
140 text = text.replace(key, value);
141 }
142 }
143 // TODO: Assure there are no un-substituted values remaining
144 return text;
145}
146
147function get_bug_details() {
148 var path_name = window.location.pathname;
149 var details = {
150 'PROJECTNAME': path_name.split('/')[1].ucFirst(),
151 'PKGNAME': null,
152 'BUGNUMBER': path_name.split('/').pop(),
153 'REPORTER': xpath("//*[@class='registering']/*[contains(@class,'sprite person')]").snapshotItem(0).firstChild.nodeValue,
154 'UPSTREAMBUG': xpath("//*[@class='link-external']").snapshotItem(0)
155 }
156 if (path_name.split('/')[2] == "source") {
157 details['PKGNAME'] = path_name.split('/')[3];
158 }
159 return details;
160}
161
162function get_original_values(textarea_input) {
163 var textarea_name = textarea_input.name;
164 var form_name = textarea_name.substr(0, textarea_name.lastIndexOf("."));
165 var original_values = {
166 'assignee': null,
167 'package': get_text_input(form_name, 'target.package'),
168 'comment': get_textarea(form_name, textarea_name),
169 'importance': get_select(form_name, 'importance'),
170 'status': get_select(form_name, 'status')
171 }
172
173 if (get_checkbox_input(form_name, 'assignee.assign_to_me')) {
174 original_values['assignee'] = '-me';
175 } else if (get_checkbox_input(form_name, 'assignee.assign_to_nobody')) {
176 original_values['assignee'] = '-nobody';
177 } else {
178 original_values['assignee'] = get_text_input(form_name, 'assignee');
179 }
180
181 return original_values;
182}
183
184// Adds the html elements to the form for a reply button with a callback function
185function create_stock_reply_element(textarea_input, response, original_values) {
60 var element = document.createElement('a');186 var element = document.createElement('a');
61 element.href = document.location + "#";187 element.href = document.location + "#";
62 var innerTextElement = document.createTextNode(prefsData['name'][idx]);188 var inner_text_element = document.createTextNode(response['name']);
63189 var textarea_name = textarea_input.name;
64 // Default to using comment as tooltip, when tooltip is missing190 var form_name = textarea_name.substr(0, textarea_name.lastIndexOf("."));
65 tip = prefsData['tip'][idx];191 console.log("form_name = " + form_name);
66 if (tip == "") {192
67 tip = prefsData['comment'][idx];193 element.title = response['tip'];
68 }194 element.appendChild(inner_text_element);
69 element.title = tip;
70
71 element.appendChild(innerTextElement);
72 element.addEventListener('click', function(e) {195 element.addEventListener('click', function(e) {
73 e.preventDefault();196 e.preventDefault();
74197
75 // Retrieve bug details198 // Retrieve bug details that we can substitute
76 var pathname = window.location.pathname;199 var bug_details = get_bug_details();
77 var bug_project = pathname.split('/')[1].ucFirst();200
78 // if the bug has no package this ends up being the bug number201 // Set form elements
79 var bug_package = pathname.split('/')[3];202 var comment_text = substitute_values(response['comment'], bug_details);
80 var bug_number = pathname.split('/').pop();203 if (comment_text) {
81 var bug_reporter = xpath("//*[@class='registering']/*[contains(@class,'sprite person')]").snapshotItem(0).firstChild.nodeValue;204 set_textarea(form_name, textarea_name, comment_text);
82 var bug_upstream = xpath("//*[@class='link-external']").snapshotItem(0);205 } else {
83206 set_textarea(form_name, textarea_name, original_values['comment']);
84 // Set comment207 }
85 var comment_text = prefsData['comment'][idx];208
86 comment_text = comment_text.replace("PROJECTNAME", bug_project);209 if (response['status']) {
87 comment_text = comment_text.replace("BUGNUMBER", bug_number);210 set_select(form_name, 'status', response['status']);
88 // only replace the package name for bugs with a package i.e. don't do it for no package bugs211 } else {
89 if (bug_number != bug_package) {212 set_select(form_name, 'status', original_values['status']);
90 comment_text = comment_text.replace("PKGNAME", bug_package);213 }
91 }214
92 comment_text = comment_text.replace("REPORTER", bug_reporter);215 if (response['importance']) {
93 if (bug_upstream != null) {216 set_select(form_name, 'importance', response['importance']);
94 comment_text = comment_text.replace("UPSTREAMBUG", bug_upstream.href);217 } else {
95 }218 set_select(form_name, 'importance', original_values['importance']);
96 xpath('//textarea[@id="'+ formname + '.comment_on_change"]').snapshotItem(0).value = comment_text;219 }
97220
98 // Set status221 if (response['package']) {
99 if (prefsData['status'][idx] != "") {222 set_text_input(form_name, 'target.package', response['package']);
100 xpath('//select[@id="'+ formname + '.status"]/option[.="'+prefsData['status'][idx].replace(/"/,'\\"')+'"]').snapshotItem(0).selected = true;223 } else {
101 }224 set_text_input(form_name, 'target.package', original_values['package']);
102225 }
103 // Set assignee226
104 if (prefsData['assign'][idx] != "") {227 if (response['assignee']) {
105 if (prefsData['assign'][idx] == "-me") {228 console.log("assignee == " + response['assignee']);
106 xpath('//input[@value="'+ formname + '.assignee.assign_to_me"]').snapshotItem(0).checked = true;229 if (response['assignee'] == '-me') {
107 }230 set_checkbox_input(form_name, 'assignee.assign_to_me', true);
108 else if (prefsData['assign'][idx] == "-nobody") {231 } else if (response['assignee'] == '-nobody') {
109 xpath('//input[@value="'+ formname + '.assignee.assign_to_nobody"]').snapshotItem(0).checked = true;232 set_checkbox_input(form_name, 'assignee.assign_to_nobody', true);
110 }233 } else if (response['assignee'] != "") {
111 else {234 console.log("Setting assignee");
112 xpath('//input[@value="'+ formname + '.assignee.assign_to"]').snapshotItem(0).checked = true;235 set_checkbox_input(form_name, 'assignee.assign_to', true);
113 xpath('//input[@id="'+ formname + '.assignee"]').snapshotItem(0).value = prefsData['assign'][idx];236 set_text_input(form_name, 'assignee', response['assignee']);
114 }237 }
115 }238 } else {
116239 if (original_values['assignee'] == '-me') {
117 // Set package240 set_checkbox_input(form_name, 'assignee.assign_to_me', true);
118 if (prefsData['package'][idx] != "") {241 set_text_input(form_name, 'assignee', "");
119 xpath('//input[@name="'+ formname + '.target.package"]').snapshotItem(0).value = prefsData['package'][idx];242 } else if (original_values['assignee'] == '-nobody') {
120 }243 set_checkbox_input(form_name, 'assignee.assign_to_nobody', true);
121244 set_text_input(form_name, 'assignee', "");
122 // Set importance245 } else if (original_values['assignee'] != "") {
123 if (prefsData['importance'][idx] != "") {246 console.log("Restoring assignee");
124 xpath('//select[@id="'+ formname + '.importance"]/option[.="'+prefsData['importance'][idx].replace(/"/,'\\"')+'"]').snapshotItem(0).selected = true;247 set_checkbox_input(form_name, 'assignee.assign_to', true);
125 }248 set_text_input(form_name, 'assignee', original_values['assignee']);
126249 }
127 // Subscribe triager by default250 }
128// var sub = xpath('//input[@id="subscribe"]').snapshotItem(0);
129// if (sub) sub.checked = true;
130251
131 return false;252 return false;
132 }, false);253 }, false);
133 return element;254
134}255 return element;
135256}
136var reply_class = 'lp_sr';257
137function insert_clickable(node, newElement, tagged, left, right)258
138{259// Main
139 if (!left) { left='['; }260(function() {
140 if (!right) { right=']'; }261 console.log("LP_Stock_Replies_NG: start");
141 var span = document.createElement("span");262
142 var leftBrace = document.createTextNode(left);263 var all_forms = xpath("//form");
143 var rightBrace = document.createTextNode(right+' ');264 for (var form_id=0; form_id<all_forms.snapshotLength; form_id++) {
144265 var this_form = all_forms.snapshotItem(form_id);
145 /* mark up for future removal? */266
146 if (tagged) {267 // Get all nodes within this form that appear to be relating to bug details
147 span.setAttribute('class',reply_class)268 var form_inputs = xpath(".//textarea[contains(@name, '.comment_on_change')]", this_form);
148 }269 for (var i=0; i<form_inputs.snapshotLength; i++) {
149270 var textarea_input = form_inputs.snapshotItem(i);
150 /* fill span */271 console.log("textarea for comment " + textarea_input.name);
151 span.appendChild(leftBrace);272
152 span.appendChild(newElement);273 // Record original settings for bug details
153 span.appendChild(rightBrace);274 var original_values = get_original_values(textarea_input);
154 // make the source readable275
155 span.appendChild(document.createTextNode("\n"));276 // Append stock replies
156277 for (var r=0; r<response_data.length; r++) {
157 /* insert span */278 console.log(response_data[r]['name']);
158 node.insertBefore(span, span.nextSibling);279 var elem = create_stock_reply_element(textarea_input, response_data[r], original_values);
159}280 insert_clickable(textarea_input.parentNode, elem);
160281 }
161function deleteReply(idx)282 }
162{
163 var count = parseInt(GM_getValue('count',0))
164 if (count == 0) return;
165 if (idx >= count) return;
166 if (idx < 0) 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,""));
173 }
174 }
175 }
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() */
179}
180
181function clearStandardReplies()
182{
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);
188 }
189 }
190 loadPreferences();
191}
192
193function loadPreferences()
194{
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;
200
201 for (var idx = 0; idx < prefsData.count; idx ++) {
202 prefsData[fieldname][idx] = GM_getValue(fieldname+idx,"");
203 }
204 }
205 for (var idx = 0; idx < prefsData.count; idx ++) {
206 if (prefsData['standard'][idx] == "yes") {
207 prefsData.standardSeen = true;
208 }
209 }
210 prefsData.reloadAt = parseInt(GM_getValue('reload-at', 0));
211}
212
213function loadStandardReplies() {
214 GM_xmlhttpRequest
215 (
216 {
217 method: 'GET',
218 url: 'http://people.canonical.com/~brian/greasemonkey/bugsquad-replies.xml',
219 headers: {
220 'Accept': 'application/atom+xml,application/xml,text/xml',
221 },
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
227 hidePreferences();
228 /* if we actually have some replies, clear the old ones */
229 if (debug) {
230 alert("Dropping old standard stock replies");
231 }
232 if (replies.length>0) {
233 clearStandardReplies();
234 }
235 if (debug) {
236 alert("Parsing new standard stock replies");
237 }
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];
243 var text = "";
244 if (fieldname == "standard") {
245 text = "yes";
246 }
247 else {
248 // alert("Want " + fieldname + " (offset " + i + ")");
249 element = replies[i].getElementsByTagName(fieldname);
250 if (element.length) {
251 text = element[0].textContent;
252 }
253 }
254 prefsData[fieldname][base+i] = text;
255 // alert(fieldname + " as [" + text + "] at " + (base+i));
256 if (debug) {
257 if (fieldname == "name") {
258 alert("Parsed [" + text + "]");
259 }
260 }
261 }
262 }
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);
267 if (debug) {
268 alert("Saving stock replies");
269 }
270 savePreferences();
271 if (debug) {
272 alert("Finished reloading standard stock replies");
273 }
274 }
275 }
276 )
277}
278
279function addColumnPreference(idx,fieldname)
280{
281 var td = document.createElement('td');
282 // why doesn't this alignment work?
283 //td.setAttribute('valign','top');
284
285 var id = reply_class + '.' + idx + '.' + fieldname;
286
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);
292
293 var input;
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');
299 }
300 else {
301 input = document.createElement('input');
302 input.setAttribute('type','text');
303 input.setAttribute('size','15');
304 }
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) {
310 e.preventDefault();
311
312 var obj = e.target;
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] = "";
318 }
319 prefsData[fieldname][idx] = obj.value;
320
321 return false;
322 }, false);
323 td.appendChild(input);
324
325 // make the source readable
326 td.appendChild(document.createTextNode("\n"));
327
328 return td;
329}
330
331function addRowPreferences(table,idx)
332{
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;
339
340 // set up empty default
341 if (!prefsData[fieldname][idx]) {
342 prefsData[fieldname][idx]="";
343 }
344
345 td = addColumnPreference(idx,fieldname);
346 tr.appendChild(td);
347 }
348 table.appendChild(tr);
349
350 // make the source readable
351 table.appendChild(document.createTextNode("\n"));
352
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);
359
360 // make the source readable
361 table.appendChild(document.createTextNode("\n"));
362
363 // spacer
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 );
369
370 // did we bump the count higher?
371 if (prefsData.count == idx) {
372 prefsData.count++;
373 }
374}
375
376function showPreferences(prefsDiv)
377{
378 var tr;
379 var table = document.createElement('table');
380 prefsDiv.appendChild(table);
381
382 // get the count and initialize arrays
383 var count = prefsData.count;
384
385/*
386 // table headers
387 tr = document.createElement('tr');
388 table.appendChild(tr);
389
390 for (var field in prefsFields) {
391 var fieldname = prefsFields[field];
392 if (fieldname == 'standard') continue;
393 if (fieldname == 'comment') continue;
394
395 var th = document.createElement('th');
396 // why doesn't this alignment work?
397 //th.setAttribute('align','left');
398 th.appendChild(document.createTextNode(fieldname));
399 tr.appendChild(th);
400 }
401*/
402
403 // load the preferences
404 var reload_time_seen = false;
405 for (var idx = 0; idx < count; idx ++) {
406
407 if (prefsData['standard'][idx] == 'yes' && !reload_time_seen) {
408 var time = new Date();
409 time.setUTCMilliseconds( prefsData.reloadAt );
410
411 var sep_tr;
412 var sep_td;
413
414 // spacer
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 );
420
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 );
431
432 // spacer
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 );
438
439 reload_time_seen = true;
440 }
441
442 addRowPreferences(table, idx);
443 }
444
445 // Show pref-control buttons
446 tr = document.createElement('tr');
447 table.appendChild(tr);
448
449 // Expand list
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) {
456 e.preventDefault();
457
458 addRowPreferences(table, prefsData.count);
459
460 return false;
461 }, false);
462 insert_clickable(td, click, false);
463 tr.appendChild(td);
464
465 // Save preferences
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) {
472 e.preventDefault();
473
474 savePreferences();
475
476 alert('Replies Saved');
477
478 return false;
479 }, false);
480 insert_clickable(td, click, false);
481 tr.appendChild(td);
482
483}
484
485function savePreferences()
486{
487 // save the count
488 GM_setValue('count', ''+prefsData.count);
489 // save standard-reply-reload date
490 GM_setValue('reload-at', ''+prefsData.reloadAt);
491
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]);
497 }
498 }
499
500 // redisplay the prefs!
501 remove_replies();
502 show_replies();
503}
504
505/*
506function 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) {
513 e.preventDefault();
514
515 remove_replies();
516 show_replies();
517
518 return false;
519 }, false);
520 return element;
521}
522*/
523function 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) {
530 e.preventDefault();
531
532 loadStandardReplies();
533 alert('Refreshing Standard Replies');
534
535 return false;
536 }, false);
537 return element;
538}
539
540var prefsDiv = null;
541var prefsId = 'lp_sr_prefs';
542function hidePreferences() {
543 var prefs = document.getElementById(prefsId);
544 if (prefs) {
545 prefs.parentNode.removeChild(prefs);
546 prefsDiv = null;
547 }
548}
549
550function 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) {
557 e.preventDefault();
558
559 // create the dialog if it doesn't exist yet
560 if (prefsDiv === null) {
561 prefsDiv = document.createElement('div');
562 prefsDiv.setAttribute('id',prefsId);
563
564 showPreferences(prefsDiv);
565 }
566
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.
573 if (prefs) {
574 prefs.parentNode.removeChild(prefs);
575 }
576 */
577 element.parentNode.insertBefore(prefsDiv, prefsDiv.nextSibling);
578 }
579 else {
580 prefs.parentNode.removeChild(prefs);
581 }
582
583 return false;
584 }, false);
585 return element;
586}
587
588function 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);
593 }
594}
595
596function 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) {
603 continue;
604 }
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);
608
609 // append all stock replies
610 for (var idx = 0; idx < prefsData.count; idx++) {
611 var left='{';
612 var right='}';
613 if (prefsData['standard'][idx] == "yes") {
614 left='[';
615 right=']';
616 }
617 insert_clickable(thisSubmit.parentNode,
618 injectStockreply(formname, idx), true, left, right);
619 }
620
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);
625
626 }283 }
627}284 console.log("LP_Stock_Replies_NG: end");
628
629window.addEventListener("load", function(e) {
630
631 loadPreferences();
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();
638 }
639
640 show_replies();
641
642}, false);
643
644})();285})();
286
287

Subscribers

People subscribed via source and target branches