Merge lp:~wallyworld/launchpad/private-dupe-bug-warning2-943497 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15719
Proposed branch: lp:~wallyworld/launchpad/private-dupe-bug-warning2-943497
Merge into: lp:launchpad
Diff against target: 809 lines (+434/-98)
7 files modified
lib/canonical/launchpad/icing/css/forms.css (+1/-2)
lib/canonical/launchpad/icing/css/modifiers.css (+8/-0)
lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css (+10/-0)
lib/lp/app/javascript/formoverlay/formoverlay.js (+14/-11)
lib/lp/bugs/javascript/duplicates.js (+278/-29)
lib/lp/bugs/javascript/tests/test_duplicates.html (+1/-0)
lib/lp/bugs/javascript/tests/test_duplicates.js (+122/-56)
To merge this branch: bzr merge lp:~wallyworld/launchpad/private-dupe-bug-warning2-943497
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+116793@code.launchpad.net

Commit message

Initial work to improve the bug duplicate form overlay to allow the selected bug to be previewed prior to submission.

Description of the change

== Implementation ==

This branch fixes a number of issues with the mark bug as duplicate form. See the linked bugs. The work is currently only for the bug selection widget used when marking bugs as duplicates. It would need to be generalised to be useful when selecting bugs to link to branches etc.

Not quite everything is implemented in this branch but it's big enough to be put up for review. The bit that is missing is marked with a TODO.

When a bug number is entered, a web service API call is made to get the json data for the bug. This data is presented to the user to allow then to confirm the bug is what they want. The TODO is because not all necessary data is returned and is currently hard coded. A new service API needs to be added to provided the required data.

Some css changes were necessary. Here some of the "non obvious" ones...

Needed to make spacing around form overlay form table correct due to css rule ordering :-

13 table.form, table.extra-options {
...
16 - margin: 1em 0;
17 + margin: 1em 0 inherit inherit;

Needed to un-squash form buttons when they are not little lazr icons.

48 +.yui3-lazr-formoverlay-form div.yui3-lazr-formoverlay-actions button {
49 + margin-right: 0.7em;
50 + }
51

Expand out the error div on the form overlay when there is an error so it is not all squashed up when the form is narrow.

58 +.yui3-lazr-formoverlay-form .yui3-lazr-formoverlay-errors:not(:empty) {
59 + min-width: 250px;
60 + text-align: center;
61 + }
62 +

== Demo and QA ==

Here's an early video of it in operation.

http://people.canonical.com/~ianb/bug-dup.ogv

This video has a number of minor flaws (eg uncentred form, text alignment). These are addressed in this branch.

== Tests ==

Add a number of new yui tests and update existing ones.

== Lint ==

Linting changed files:
  lib/canonical/launchpad/icing/css/forms.css
  lib/canonical/launchpad/icing/css/modifiers.css
  lib/lp/app/javascript/formoverlay/formoverlay.js
  lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css
  lib/lp/bugs/javascript/duplicates.js
  lib/lp/bugs/javascript/tests/test_duplicates.html
  lib/lp/bugs/javascript/tests/test_duplicates.js

Clean except for some css noise.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (4.0 KiB)

I think this is a good start. It is must better than what users are seeing now. I have a few remarks.

> === modified file 'lib/lp/app/javascript/formoverlay/formoverlay.js'
> --- lib/lp/app/javascript/formoverlay/formoverlay.js 2012-06-26 00:15:11 +0000
> +++ lib/lp/app/javascript/formoverlay/formoverlay.js 2012-07-26 11:02:24 +0000
> @@ -492,18 +492,19 @@
> * @method showError
> */
> showError: function(error_msgs){
> + var error_content;
> if (Y.Lang.isString(error_msgs)) {
> - error_msgs = [error_msgs];
> + error_content = Y.Node.create('<p></p>').set('text', error_msgs);
> + } else {
> + error_content = Y.Node.create(
> + '<p>The following errors were encountered:</p><ul></ul>');
> + var error_list = error_content.one('ul');
> + Y.each(error_msgs, function(error_msg){
> + error_list.appendChild(
> + Y.Node.create('<li></li>').set('text', error_msg));
> + });

This iteration is expensive. repeated appends also call repeated refreshes
You can build the nodes individually, or pass a string to update the UI once.

            var li_nodes = new Y.NodeList([]);
            Y.each(error_msgs, function(error_msg){
                li_nodes.push(
                    Y.Node.create('<li></li>').set('text', error_msg));
            });
            error_list.append(li_nodes);

> === modified file 'lib/lp/bugs/javascript/duplicates.js'
> --- lib/lp/bugs/javascript/duplicates.js 2012-07-25 00:31:04 +0000
> +++ lib/lp/bugs/javascript/duplicates.js 2012-07-26 11:02:24 +0000
> @@ -13,10 +13,10 @@
> // Overlay related vars.
> var submit_button_html =
> '<button type="submit" name="field.actions.change" ' +
> - 'value="Change" class="lazr-pos lazr-btn" >OK</button>';
> + '>OK</button>';
> var cancel_button_html =
> '<button type="button" name="field.actions.cancel" ' +
> - 'class="lazr-neg lazr-btn" >Cancel</button>';
> + '>Cancel</button>';

Thank you for removing the ambiguous and tiny icons. "Ok" is a poor
action term that Launchpad and other UI guidelines avoid. "Save" or
"Change" are better generic terms. If we have a separate action/link to
remove/clear duplicate, then this action would be named "Save
Duplicate".

> @@ -82,12 +85,250 @@
> update_dupe_link.on('click', function(e) {
> // Only go ahead if we have received the form content by the
> // time the user clicks:
> - if (that.duplicate_form_overlay) {
> + if (that.duplicate_form) {
> e.halt();
> - that.duplicate_form_overlay.show();
> + that.duplicate_form.show();
> Y.DOM.byId('field.duplicateof').focus();
> }
> });
> + // We the due form overlay is hidden, we need to reset the form.

Maybe s/We/When/;

> + // Template for rendering the bug details.
> + _bug_details_template: function() {
> + return [
> + '<div id="client-listing" style="max-width: 75em;">',

This looks too large. 60 is the max for English and that is what we
use else-where in Lp. Wh...

Read more...

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/css/forms.css'
2--- lib/canonical/launchpad/icing/css/forms.css 2012-06-23 13:44:13 +0000
3+++ lib/canonical/launchpad/icing/css/forms.css 2012-07-27 01:21:21 +0000
4@@ -90,7 +90,6 @@
5 .extra-form-buttons {
6 text-align: center;
7 padding-top: 1em;
8- white-space: nowrap;
9 }
10 .extra-form-buttons button {
11 margin-right: 0.7em;
12@@ -140,7 +139,7 @@
13 table.form, table.extra-options {
14 /* Many forms are laid out using tables, with appropriate spacing: */
15 /* http://launchpad.dev/firefox/+edit */
16- margin: 1em 0;
17+ margin: 1em 0 inherit inherit;
18 width: 100%;
19 }
20 table.form th {
21
22=== modified file 'lib/canonical/launchpad/icing/css/modifiers.css'
23--- lib/canonical/launchpad/icing/css/modifiers.css 2012-07-23 03:40:33 +0000
24+++ lib/canonical/launchpad/icing/css/modifiers.css 2012-07-27 01:21:21 +0000
25@@ -112,6 +112,14 @@
26 height: 10px;
27 }
28
29+.ellipsis {
30+ white-space: nowrap;
31+ overflow: hidden;
32+ text-overflow: ellipsis;
33+ -o-text-overflow: ellipsis;
34+ -ms-text-overflow: ellipsis;
35+ }
36+
37 .exception {
38 color: #cc0000;
39 }
40
41=== modified file 'lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css'
42--- lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css 2012-05-12 01:56:24 +0000
43+++ lib/lp/app/javascript/formoverlay/assets/formoverlay-core.css 2012-07-27 01:21:21 +0000
44@@ -21,12 +21,22 @@
45 text-align: right;
46 }
47
48+.yui3-lazr-formoverlay-form div.yui3-lazr-formoverlay-actions button {
49+ margin-right: 0.7em;
50+ }
51+
52 .yui3-lazr-formoverlay-form .yui3-lazr-formoverlay-errors {
53 padding-top: 0;
54 padding-bottom: 0;
55 color: red;
56 }
57
58+.yui3-lazr-formoverlay-form .yui3-lazr-formoverlay-errors:not(:empty) {
59+ min-width: 250px;
60+ margin-left: auto;
61+ margin-right: auto;
62+ }
63+
64 .yui3-lazr-formoverlay-form table {
65 /* This gets rid of the 12px margin-bottom that yui specifies
66 * in its base.css.
67
68=== modified file 'lib/lp/app/javascript/formoverlay/formoverlay.js'
69--- lib/lp/app/javascript/formoverlay/formoverlay.js 2012-06-26 00:15:11 +0000
70+++ lib/lp/app/javascript/formoverlay/formoverlay.js 2012-07-27 01:21:21 +0000
71@@ -492,18 +492,21 @@
72 * @method showError
73 */
74 showError: function(error_msgs){
75+ var error_content;
76 if (Y.Lang.isString(error_msgs)) {
77- error_msgs = [error_msgs];
78+ error_content = Y.Node.create('<p></p>').set('text', error_msgs);
79+ } else {
80+ error_content = Y.Node.create(
81+ '<p>The following errors were encountered:</p><ul></ul>');
82+ var error_list = error_content.one('ul');
83+ var li_nodes = new Y.NodeList([]);
84+ Y.each(error_msgs, function(error_msg){
85+ li_nodes.push(
86+ Y.Node.create('<li></li>').set('text', error_msg));
87+ });
88+ error_list.append(li_nodes);
89 }
90- var error_html = "The following errors were encountered: <ul>";
91- Y.each(error_msgs, function(error_msg){
92- // XXX noodles 2009-02-13 bug=342212. We need to decide on
93- // or provide our own escapeHTML() helper.
94- error_html += "<li>" + error_msg.replace(/<([^>]+)>/g,'') +
95- "</li>";
96- });
97- error_html += "</ul>";
98- this.error_node.set('innerHTML', error_html);
99+ this.error_node.appendChild(error_content);
100 },
101
102 /**
103@@ -512,7 +515,7 @@
104 * @method clearError
105 */
106 clearError: function(){
107- this.error_node.set('innerHTML', '');
108+ this.error_node.empty(true);
109 },
110
111 /**
112
113=== modified file 'lib/lp/bugs/javascript/duplicates.js'
114--- lib/lp/bugs/javascript/duplicates.js 2012-07-25 00:31:04 +0000
115+++ lib/lp/bugs/javascript/duplicates.js 2012-07-27 01:21:21 +0000
116@@ -13,10 +13,10 @@
117 // Overlay related vars.
118 var submit_button_html =
119 '<button type="submit" name="field.actions.change" ' +
120- 'value="Change" class="lazr-pos lazr-btn" >OK</button>';
121+ '>Save Duplicate</button>';
122 var cancel_button_html =
123 '<button type="button" name="field.actions.cancel" ' +
124- 'class="lazr-neg lazr-btn" >Cancel</button>';
125+ '>Cancel</button>';
126
127 /**
128 * Manage the process of marking a bug as a duplicate.
129@@ -24,6 +24,8 @@
130 */
131 namespace.MarkBugDuplicate = Y.Base.create("markBugDupeWidget", Y.Widget, [], {
132 initializer: function(cfg) {
133+ this.lp_client = new Y.lp.client.Launchpad(cfg);
134+ this.io_provider = Y.lp.client.get_configured_io_provider(cfg);
135 var update_dupe_link = cfg.update_dupe_link;
136 if (update_dupe_link) {
137 // Add a class denoting the link as js-action link.
138@@ -55,19 +57,20 @@
139 '</p>';
140 }
141
142- this.duplicate_form_overlay = new Y.lazr.FormOverlay({
143+ this.duplicate_form = new Y.lazr.FormOverlay({
144 headerContent: '<h2>Mark bug report as duplicate</h2>',
145 form_header: form_header,
146 form_submit_button: Y.Node.create(submit_button_html),
147 form_cancel_button: Y.Node.create(cancel_button_html),
148 centered: true,
149+ // The form submit first searches for the bug.
150 form_submit_callback:
151- Y.bind(this._update_bug_duplicate, this),
152+ Y.bind(this._find_duplicate_bug, this),
153 visible: false,
154- io_provider: this.get('io_provider')
155+ io_provider: this.io_provider
156 });
157- this.duplicate_form_overlay.render('#duplicate-form-container');
158- this.duplicate_form_overlay.loadFormContentAndRender(
159+ this.duplicate_form.render('#duplicate-form-container');
160+ this.duplicate_form.loadFormContentAndRender(
161 mark_dupe_form_url);
162 },
163
164@@ -82,12 +85,251 @@
165 update_dupe_link.on('click', function(e) {
166 // Only go ahead if we have received the form content by the
167 // time the user clicks:
168- if (that.duplicate_form_overlay) {
169+ if (that.duplicate_form) {
170 e.halt();
171- that.duplicate_form_overlay.show();
172+ that.duplicate_form.show();
173 Y.DOM.byId('field.duplicateof').focus();
174 }
175 });
176+ // When the dupe form overlay is hidden, we need to reset the form.
177+ this.duplicate_form.after('visibleChange', function() {
178+ if (!this.get('visible')) {
179+ that._hide_confirm_node();
180+ }
181+ });
182+ },
183+
184+ /**
185+ * Show a spinner next to the specified node.
186+ *
187+ * @method _show_bug_spinner
188+ * @private
189+ */
190+ _show_bug_spinner: function(node) {
191+ if( node === null) {
192+ return;
193+ }
194+ var spinner_node = Y.Node.create(
195+ '<img class="spinner" src="/@@/spinner" alt="Searching..." />');
196+ node.insert(spinner_node, 'after');
197+ },
198+
199+ /**
200+ * Hide the spinner next to the specified node.
201+ *
202+ * @method _hide_bug_spinner
203+ * @private
204+ */
205+ _hide_bug_spinner: function(node) {
206+ if( node === null) {
207+ return;
208+ }
209+ var spinner = node.get('parentNode').one('.spinner');
210+ if (spinner !== null) {
211+ spinner.remove();
212+ }
213+ },
214+
215+ /**
216+ * Look up the selected bug and get the user to confirm that it is the one
217+ * they want.
218+ *
219+ * @param data
220+ * @private
221+ */
222+ _find_duplicate_bug: function(data) {
223+ var new_dup_id = Y.Lang.trim(data['field.duplicateof'][0]);
224+ // If there's no bug data entered then we are deleting the duplicate
225+ // link.
226+ if (new_dup_id === '') {
227+ this._update_bug_duplicate(new_dup_id);
228+ return;
229+ }
230+ var that = this;
231+ var qs_data
232+ = Y.lp.client.append_qs("", "ws.accept", "application.json");
233+
234+ var bug_field = this.duplicate_form.form_node
235+ .one('[id="field.duplicateof"]');
236+ var config = {
237+ on: {
238+ start: function() {
239+ that._show_bug_spinner(bug_field);
240+ that.duplicate_form.clearError();
241+ },
242+ end: function() {
243+ that._hide_bug_spinner(bug_field);
244+ },
245+ success: function(id, response) {
246+ if (response.responseText === '') {
247+ return;
248+ }
249+ var bug_data = Y.JSON.parse(response.responseText);
250+ that._confirm_selected_bug(bug_data);
251+ },
252+ failure: function(id, response) {
253+ var error_msg;
254+ if (response.status === 404) {
255+ error_msg = new_dup_id +
256+ ' is not a valid bug number.';
257+ } else {
258+ error_msg = response.responseText;
259+ }
260+ that.duplicate_form.showError(error_msg);
261+ }
262+ },
263+ data: qs_data
264+ };
265+ var uri
266+ = Y.lp.client.get_absolute_uri("/api/devel/bugs/" + new_dup_id);
267+ this.io_provider.io(uri, config);
268+ },
269+
270+ // Template for rendering the bug details.
271+ _bug_details_template: function() {
272+ return [
273+ '<div id="client-listing">',
274+ ' <div class="buglisting-col1">',
275+ ' <div class="importance {{importance_class}}">',
276+ ' {{importance}}',
277+ ' </div>',
278+ ' <div class="status {{status_class}}">',
279+ ' {{status}}',
280+ ' </div>',
281+ ' <div class="buginfo-extra">',
282+ ' <div class="information_type">',
283+ ' {{information_type}}',
284+ ' </div>',
285+ ' </div>',
286+ ' </div>',
287+ ' <div class="buglisting-col2" style="max-width: 60em;">',
288+ ' <span class="bugnumber">#{{id}}</span>',
289+ ' <a href="{{bug_url}}" class="bugtitle">{{bug_summary}}</a>',
290+ ' <div class="buginfo-extra">',
291+ ' <p class="ellipsis line-block" style="max-width: 70em;">',
292+ ' {{description}}</p>',
293+ ' </div>',
294+ ' </div>',
295+ '</div>'
296+ ].join(' ');
297+ },
298+
299+ // Template for the bug confirmation form.
300+ _bug_confirmation_form_template: function() {
301+ return [
302+ '<div class="confirmation-node yui3-lazr-formoverlay-form" ',
303+ 'style="margin-top: 6px;">',
304+ '{{> bug_details}}',
305+ '<div class="yui3-lazr-formoverlay-errors"></div>',
306+ '<div class="extra-form-buttons">',
307+ ' <button class="yes_button" type="button">',
308+ ' Save Duplicate</button>',
309+ ' <button class="no_button" type="button">Search Again</button>',
310+ ' <button class="cancel_button" type="button">Cancel</button>',
311+ '</div>',
312+ '</div>'].join('');
313+ },
314+
315+ /**
316+ * Ask the user to confirm the chosen bug is the one they want.
317+ * @param bug_data
318+ * @private
319+ */
320+ _confirm_selected_bug: function(bug_data) {
321+ // TODO - get real data from the server
322+ bug_data.importance = 'High';
323+ bug_data.importance_class = 'importanceHIGH';
324+ bug_data.status = 'Triaged';
325+ bug_data.status_class = 'statusTRIAGED';
326+ bug_data.bug_summary = bug_data.title;
327+ bug_data.bug_url = bug_data.web_link;
328+
329+ var bug_id = bug_data.id;
330+ var html = Y.lp.mustache.to_html(
331+ this._bug_confirmation_form_template(), bug_data,
332+ {bug_details: this._bug_details_template()});
333+ var confirm_node = Y.Node.create(html);
334+ this._show_confirm_node(confirm_node);
335+ var that = this;
336+ confirm_node.one(".yes_button")
337+ .on('click', function(e) {
338+ e.halt();
339+ that._update_bug_duplicate(bug_id);
340+ });
341+
342+ confirm_node.one(".no_button")
343+ .on('click', function(e) {
344+ e.halt();
345+ that._hide_confirm_node(confirm_node);
346+ });
347+ confirm_node.one(".cancel_button")
348+ .on('click', function(e) {
349+ e.halt();
350+ that.duplicate_form.hide();
351+ });
352+ },
353+
354+ // Centre the duplicate form along the x axis without changing y position.
355+ _xaxis_centre: function() {
356+ var viewport = Y.DOM.viewportRegion();
357+ var new_x = (viewport.right + viewport.left)/2 -
358+ this.duplicate_form.get('contentBox').get('offsetWidth')/2;
359+ this.duplicate_form.move([new_x, this.duplicate_form._getY()]);
360+
361+ },
362+
363+ /** Show the bug selection confirmation node.
364+ * @method _show_confirm_node
365+ * @private
366+ */
367+ _show_confirm_node: function(confirmation_node) {
368+ this.duplicate_form.form_header_node
369+ .insert(confirmation_node, 'after');
370+ this.confirmation_node = confirmation_node;
371+ this._xaxis_centre();
372+ this._fade_in(confirmation_node, this.duplicate_form.form_node);
373+ },
374+
375+ /** Hide the bug selection confirmation node.
376+ * @method _hide_confirm_node
377+ * @private
378+ */
379+ _hide_confirm_node: function() {
380+ this.duplicate_form.form_node.removeClass('hidden');
381+ if (Y.Lang.isValue(this.confirmation_node)) {
382+ this._fade_in(
383+ this.duplicate_form.form_node, this.confirmation_node);
384+ this._xaxis_centre();
385+ this.confirmation_node.remove();
386+ this.confirmation_node = null;
387+ }
388+ },
389+
390+ // Animate the display of content.
391+ _fade_in: function(content_node, old_content, use_animation) {
392+ content_node.removeClass('hidden');
393+ if (old_content === null) {
394+ content_node.removeClass('transparent');
395+ content_node.setStyle('opacity', 1);
396+ content_node.show();
397+ return;
398+ }
399+ old_content.addClass('hidden');
400+ if (!Y.Lang.isValue(use_animation)) {
401+ use_animation = this.get('use_animation');
402+ }
403+ if (!use_animation) {
404+ old_content.setStyle('opacity', 1);
405+ return;
406+ }
407+ content_node.addClass('transparent');
408+ content_node.setStyle('opacity', 0);
409+ var fade_in = new Y.Anim({
410+ node: content_node,
411+ to: {opacity: 1},
412+ duration: 0.8
413+ });
414+ fade_in.run();
415 },
416
417 /**
418@@ -101,6 +343,7 @@
419 */
420 _update_bug_duplicate_success: function(updated_entry, new_dup_url,
421 new_dup_id) {
422+ this.duplicate_form.hide();
423 updated_entry.set('duplicate_of_link', new_dup_url);
424 this.set('lp_bug_entry', updated_entry);
425
426@@ -129,16 +372,20 @@
427 dupe_span.one('a').set('href', update_dup_url);
428 this._hide_comment_on_duplicate_warning();
429 }
430+ var anim_duration = 1;
431+ if (!this.get('anim_duration')) {
432+ anim_duration = 0;
433+ }
434 Y.lp.anim.green_flash({
435 node: dupe_span,
436- duration: this.get('anim_duration')
437+ duration: anim_duration
438 }).run();
439 // ensure the new link is hooked up correctly:
440 var that = this;
441 dupe_span.one('a').on(
442 'click', function(e){
443 e.preventDefault();
444- that.duplicate_form_overlay.show();
445+ that.duplicate_form.show();
446 Y.DOM.byId('field.duplicateof').focus();
447 });
448 },
449@@ -156,23 +403,26 @@
450 // Reset the lp_bug_entry.duplicate_of_link as it wasn't
451 // updated.
452 this.get('lp_bug_entry').set('duplicate_of_link', old_dup_url);
453+ var error_msg = response.responseText;
454 if (response.status === 400) {
455- this.duplicate_form_overlay.showError(
456- new_dup_id + ' is not a valid bug number or nickname.');
457- } else {
458- this.duplicate_form_overlay.showError(response.responseText);
459+ var error_info = response.responseText.split('\n');
460+ error_msg = error_info.slice(1).join(' ');
461 }
462- this.duplicate_form_overlay.show();
463+ this.confirmation_node.one('.yui3-lazr-formoverlay-errors')
464+ .setContent(error_msg);
465+ this.confirmation_node.one(".yes_button").addClass('hidden');
466+ this._xaxis_centre();
467+
468 },
469
470 /**
471 * Update the bug duplicate via the LP API
472 *
473 * @method _update_bug_duplicate
474- * @param data
475+ * @param new_dup_id
476 * @private
477 */
478- _update_bug_duplicate: function(data) {
479+ _update_bug_duplicate: function(new_dup_id) {
480 // XXX noodles 2009-03-17 bug=336866 It seems the etag
481 // returned by lp_save() is incorrect. Remove it for now
482 // so that the second save does not result in a '412
483@@ -186,11 +436,7 @@
484 var lp_bug_entry = this.get('lp_bug_entry');
485 lp_bug_entry.removeAttr('http_etag');
486
487- // Hide the formoverlay:
488- this.duplicate_form_overlay.hide();
489-
490 var new_dup_url = null;
491- var new_dup_id = Y.Lang.trim(data['field.duplicateof'][0]);
492 if (new_dup_id !== '') {
493 var self_link = lp_bug_entry.get('self_link');
494 var last_slash_index = self_link.lastIndexOf('/');
495@@ -201,14 +447,20 @@
496
497 var dupe_span = this.get('dupe_span');
498 var that = this;
499+ var submit_btn = null;
500+ if (Y.Lang.isValue(this.confirmation_node)) {
501+ submit_btn = this.confirmation_node.one(".yes_button");
502+ }
503 var config = {
504 on: {
505 start: function() {
506 dupe_span.removeClass('sprite bug-dupe');
507 dupe_span.addClass('update-in-progress-message');
508+ that._show_bug_spinner(submit_btn);
509 },
510 end: function() {
511 dupe_span.removeClass('update-in-progress-message');
512+ that._hide_bug_spinner(submit_btn);
513 },
514 success: function(updated_entry) {
515 that._update_bug_duplicate_success(
516@@ -233,7 +485,7 @@
517 */
518 _show_comment_on_duplicate_warning: function() {
519 var duplicate_warning = Y.one('#warning-comment-on-duplicate');
520- if (duplicate_warning === null) {
521+ if (!Y.Lang.isValue(duplicate_warning)) {
522 var container = Y.one('#add-comment-form');
523 var first_node = container.get('firstChild');
524 duplicate_warning = Y.Node.create(
525@@ -286,16 +538,13 @@
526 }
527 },
528 // Override for testing.
529- io_provider: {
530- },
531- // Override for testing.
532- anim_duration: {
533- value: 1
534+ use_animation: {
535+ value: true
536 }
537 }
538 });
539
540 }, "0.1", {"requires": [
541 "base", "io", "oop", "node", "event", "json", "lazr.formoverlay",
542- "lazr.effects", "lp.app.widgets.expander",
543+ "lazr.effects", "lp.app.widgets.expander", "lp.mustache",
544 "lp.app.formwidgets.resizing_textarea", "plugin"]});
545
546=== modified file 'lib/lp/bugs/javascript/tests/test_duplicates.html'
547--- lib/lp/bugs/javascript/tests/test_duplicates.html 2012-07-25 00:31:04 +0000
548+++ lib/lp/bugs/javascript/tests/test_duplicates.html 2012-07-27 01:21:21 +0000
549@@ -27,6 +27,7 @@
550 <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
551
552 <!-- Dependencies -->
553+ <script type="text/javascript" src="../../../../../build/js/lp/app/mustache.js"></script>
554 <script type="text/javascript" src="../../../../../build/js/lp/app/anim/anim.js"></script>
555 <script type="text/javascript" src="../../../../../build/js/lp/app/client.js"></script>
556 <script type="text/javascript" src="../../../../../build/js/lp/app/errors.js"></script>
557
558=== modified file 'lib/lp/bugs/javascript/tests/test_duplicates.js'
559--- lib/lp/bugs/javascript/tests/test_duplicates.js 2012-07-25 12:13:38 +0000
560+++ lib/lp/bugs/javascript/tests/test_duplicates.js 2012-07-27 01:21:21 +0000
561@@ -51,7 +51,7 @@
562 var widget = new Y.lp.bugs.duplicates.MarkBugDuplicate({
563 srcNode: '#duplicate-actions',
564 lp_bug_entry: this.lp_bug_entry,
565- anim_duration: 0,
566+ use_animation: false,
567 io_provider: this.mockio
568 });
569 widget.render();
570@@ -97,8 +97,8 @@
571 Y.Assert.areEqual('http://foo/+duplicate', url);
572 },
573
574- // The duplicate entry form renders and submits the expected data.
575- _assert_duplicate_form_submission: function(bug_id) {
576+ // The search form renders and submits the expected data.
577+ _assert_search_form_submission: function(bug_id) {
578 var form = Y.one('#duplicate-form-container');
579 Y.Assert.isTrue(
580 form.one('div.pretty-overlay-window')
581@@ -108,15 +108,123 @@
582 Y.one('#duplicate-form-container div.pretty-overlay-window')
583 .hasClass('yui3-lazr-formoverlay-hidden'));
584 Y.DOM.byId('field.duplicateof').value = bug_id;
585- form.one('.lazr-pos').simulate('click');
586- Y.Assert.areEqual(
587- '/api/devel/bugs/1',
588- this.mockio.last_request.url);
589- var expected_link = '{}';
590+ form.one('[name="field.actions.change"]').simulate('click');
591+ var expected_url = '/api/devel/bugs/1';
592 if (bug_id !== '') {
593- expected_link =
594+ expected_url = 'file:///api/devel/bugs/' + bug_id;
595+ }
596+ Y.Assert.areEqual(expected_url, this.mockio.last_request.url);
597+ },
598+
599+ // The bug entry form is visible and the confirmation form is invisible
600+ // or visa versa.
601+ _assert_form_state: function(confirm_form_visible) {
602+ Y.Assert.areEqual(
603+ confirm_form_visible,
604+ Y.one('#duplicate-form-container form')
605+ .hasClass('hidden'));
606+ var bug_info = Y.one('#duplicate-form-container ' +
607+ '.confirmation-node #client-listing');
608+ if (confirm_form_visible) {
609+ Y.Assert.isNotNull(bug_info);
610+ } else {
611+ Y.Assert.isNull(bug_info);
612+ }
613+ },
614+
615+ // Invoke a successful search operation and check the form state.
616+ _assert_search_form_success: function(bug_id) {
617+ var expected_updated_entry = {
618+ id: bug_id,
619+ uri: 'api/devel/bugs/' + bug_id,
620+ duplicate_of_link: 'api/devel/bugs/' + bug_id,
621+ self_link: 'api/devel/bugs/' + bug_id};
622+ this.mockio.last_request.successJSON(expected_updated_entry);
623+ this._assert_form_state(true);
624+ },
625+
626+ // A successful search for a bug displays the confirmation form.
627+ test_initial_bug_search_success: function() {
628+ this.widget = this._createWidget(false);
629+ this._assert_search_form_submission(3);
630+ this._assert_search_form_success(3);
631+ },
632+
633+ // After a successful search, hitting the Search Again button takes us
634+ // back to the bug details entry form.
635+ test_initial_bug_search_try_again: function() {
636+ this.widget = this._createWidget(false);
637+ this._assert_search_form_submission(3);
638+ this._assert_search_form_success(3);
639+ Y.one('#duplicate-form-container .no_button')
640+ .simulate('click');
641+ this._assert_form_state(false);
642+ },
643+
644+ // After a successful search, hitting the Select bug button initiates
645+ // the mark as duplicate operation.
646+ test_bug_search_select_bug: function() {
647+ this.widget = this._createWidget(false);
648+ this._assert_search_form_submission(3);
649+ this._assert_search_form_success(3);
650+ var update_bug_duplicate_called = false;
651+ this.widget._update_bug_duplicate = function(bug_id) {
652+ update_bug_duplicate_called = true;
653+ Y.Assert.areEqual(3, bug_id);
654+ };
655+ Y.one('#duplicate-form-container .yes_button')
656+ .simulate('click');
657+ this._assert_form_state(true);
658+ Y.Assert.isTrue(update_bug_duplicate_called);
659+ },
660+
661+ // The specified error message is displayed.
662+ _assert_error_display: function(message) {
663+ var selector
664+ = '#duplicate-form-container div.pretty-overlay-window';
665+ Y.Assert.isFalse(
666+ Y.one(selector).hasClass('yui3-lazr-formoverlay-hidden'));
667+ var error_msg = Y.one('.yui3-lazr-formoverlay-errors p');
668+ Y.Assert.areEqual(message, error_msg.get('text'));
669+ },
670+
671+ // The error is displayed as expected when the initial bug search
672+ // fails with a generic error.
673+ test_initial_bug_search_generic_failure: function() {
674+ this.widget = this._createWidget(false);
675+ this._assert_search_form_submission(3);
676+ var response = {
677+ status: 500,
678+ responseText: 'An error occurred'
679+ };
680+ this.mockio.respond(response);
681+ this._assert_error_display('An error occurred');
682+ },
683+
684+ // The error is displayed as expected when the initial bug search
685+ // fails with a 404 (invalid/not found bug id).
686+ test_initial_bug_search_invalid_bug_failure: function() {
687+ this.widget = this._createWidget(false);
688+ this._assert_search_form_submission(3);
689+ var response = {
690+ status: 404,
691+ responseText: 'An error occurred'
692+ };
693+ this.mockio.respond(response);
694+ this._assert_error_display('3 is not a valid bug number.');
695+ },
696+
697+ // The duplicate entry form renders and submits the expected data.
698+ _assert_confirmation_form_submission: function(bug_id) {
699+ this._assert_search_form_submission(bug_id);
700+ this._assert_search_form_success(bug_id);
701+ Y.one('#duplicate-form-container .yes_button')
702+ .simulate('click');
703+ this._assert_form_state(true);
704+ Y.Assert.areEqual(
705+ '/api/devel/bugs/1', this.mockio.last_request.url);
706+ var expected_link =
707 '{"duplicate_of_link":"api/devel/bugs/' + bug_id + '"}';
708- }
709 Y.Assert.areEqual(
710 expected_link, this.mockio.last_request.config.data);
711 },
712@@ -124,6 +232,7 @@
713 // Submitting a bug dupe works as expected.
714 test_duplicate_form_submission_success: function() {
715 this.widget = this._createWidget(false);
716+ this._assert_confirmation_form_submission(3);
717 var success_called = false;
718 this.widget._update_bug_duplicate_success =
719 function(updated_entry, new_dup_url, new_dup_id) {
720@@ -134,7 +243,6 @@
721 Y.Assert.areEqual(3, new_dup_id);
722 success_called = true;
723 };
724- this._assert_duplicate_form_submission(3);
725 var expected_updated_entry = {
726 uri: 'api/devel/bugs/1',
727 duplicate_of_link: 'api/devel/bugs/3',
728@@ -146,6 +254,7 @@
729 // A submission failure is handled as expected.
730 test_duplicate_form_submission_failure: function() {
731 this.widget = this._createWidget(false);
732+ this._assert_confirmation_form_submission(3);
733 var failure_called = false;
734 this.widget._update_bug_duplicate_failure =
735 function(response, old_dup_url, new_dup_id) {
736@@ -155,7 +264,6 @@
737 Y.Assert.areEqual(3, new_dup_id);
738 failure_called = true;
739 };
740- this._assert_duplicate_form_submission(3);
741 this.mockio.respond({
742 status: 400,
743 responseText: 'There was an error',
744@@ -165,7 +273,8 @@
745
746 // Submitting a dupe removal request works as expected.
747 test_duplicate_form_submission_remove_dupe: function() {
748- this.widget = this._createWidget(true);
749+ this.widget = this._createWidget(false);
750+ this._assert_search_form_submission('');
751 var success_called = false;
752 this.widget._update_bug_duplicate_success =
753 function(updated_entry, new_dup_url, new_dup_id) {
754@@ -174,7 +283,6 @@
755 Y.Assert.areEqual('', new_dup_id);
756 success_called = true;
757 };
758- this._assert_duplicate_form_submission('');
759 var expected_updated_entry =
760 '{"duplicate_of_link":""}';
761 this.mockio.success({
762@@ -221,48 +329,6 @@
763 Y.one('#mark-duplicate-text .menu-link-mark-dupe'));
764 // Test the duplicate warning message is gone.
765 Y.Assert.isNull(Y.one('#warning-comment-on-duplicate'));
766- },
767-
768- // The remove bug duplicate error function works as expected for
769- // generic errors.
770- test_update_bug_duplicate_generic_failure: function() {
771- this.widget = this._createWidget(false);
772- var data = {
773- self_link: 'api/devel/bugs/1'};
774- var new_bug_entry = new Y.lp.client.Entry(
775- this.lp_client, data, data.self_link);
776- var response = {
777- status: 500,
778- responseText: 'An error occurred'
779- };
780- this.widget._update_bug_duplicate_failure(response, null, 3);
781- Y.Assert.isFalse(
782- Y.one('#duplicate-form-container div.pretty-overlay-window')
783- .hasClass('yui3-lazr-formoverlay-hidden'));
784- var error_msg = Y.one('.yui3-lazr-formoverlay-errors ul li');
785- Y.Assert.areEqual('An error occurred', error_msg.get('text'));
786- },
787-
788- // The remove bug duplicate error function works as expected for
789- // invalid bug errors.
790- test_update_bug_duplicate_invalid_bug_failure: function() {
791- this.widget = this._createWidget(false);
792- var data = {
793- self_link: 'api/devel/bugs/1'};
794- var new_bug_entry = new Y.lp.client.Entry(
795- this.lp_client, data, data.self_link);
796- var response = {
797- status: 400,
798- responseText: 'An error occurred'
799- };
800- this.widget._update_bug_duplicate_failure(response, null, 3);
801- Y.Assert.isFalse(
802- Y.one('#duplicate-form-container div.pretty-overlay-window')
803- .hasClass('yui3-lazr-formoverlay-hidden'));
804- var error_msg = Y.one('.yui3-lazr-formoverlay-errors ul li');
805- Y.Assert.areEqual(
806- '3 is not a valid bug number or nickname.',
807- error_msg.get('text'));
808 }
809 }));
810