Merge lp:~wallyworld/launchpad/new-team-picker-load-form into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15492
Proposed branch: lp:~wallyworld/launchpad/new-team-picker-load-form
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/new-team-picker
Diff against target: 1007 lines (+326/-204)
11 files modified
lib/lp/app/javascript/formoverlay/formoverlay.js (+4/-4)
lib/lp/app/javascript/picker/person_picker.js (+124/-86)
lib/lp/app/javascript/picker/picker.js (+62/-37)
lib/lp/app/javascript/picker/picker_patcher.js (+5/-6)
lib/lp/app/javascript/picker/tests/test_personpicker.html (+22/-5)
lib/lp/app/javascript/picker/tests/test_personpicker.js (+62/-6)
lib/lp/app/javascript/picker/tests/test_picker_patcher.js (+34/-23)
lib/lp/app/javascript/subscribers/subscribers_list.js (+6/-6)
lib/lp/bugs/javascript/filebug_dupefinder.js (+2/-2)
lib/lp/registry/javascript/sharing/shareepicker.js (+0/-24)
lib/lp/registry/javascript/sharing/tests/test_shareepicker.js (+5/-5)
To merge this branch: bzr merge lp:~wallyworld/launchpad/new-team-picker-load-form
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+111543@code.launchpad.net

Commit message

Load the team form from the model and make it available for the New Team functionality in the pickers.

Description of the change

== Implementation ==

The branch continues the work to add team creation capability to the person picker. The previous branch hard coded a sample team form, this branch provides the infrastructure to fetch the actual team form html from the model using an XHR ++form++ call. There are also some small fixes noted in the review comments from the previous branch.

There may be several pickers which need to have the create team form html provided to them. We don't want to make a separate XHR call for each picker, so what happens is that the first picker to be processed makes the call and the others are queued up and provided with the result when it arrives. A YUI namespace is used to coordinate the workflow since the picker javascript can live inside separate yui instances and events don't propagate across instances.

The XHR call to fetch the create team form happens from the picker initialisation. By the time the user clicks on the New Team link, the form should already have been loaded and rendered, but if not, the user sees a big spinner which is replaced by the form automatically when it arrives.

This branch sees a form containing every team attribute rendered. The next branch will implement a form with only a subset of the attributes eg name, displayname, subscription policy etc.

== Tests ==

Existing yui tests were modified to account for the XHR call and stub that out. A test was added to ensure only one XHR call is made for multiple pickers.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/picker/person_picker.js
  lib/lp/app/javascript/picker/picker.js
  lib/lp/app/javascript/picker/picker_patcher.js
  lib/lp/app/javascript/picker/tests/test_personpicker.html
  lib/lp/app/javascript/picker/tests/test_personpicker.js
  lib/lp/app/javascript/picker/tests/test_picker_patcher.js
  lib/lp/registry/javascript/sharing/shareepicker.js
  lib/lp/registry/javascript/sharing/tests/test_shareepicker.js

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Ian, just giving a quick look and was curious if you'd thought about doing the load_new_team_form purely with events? The pickers would listen to an event: Y.on('new_team_form:load') and then the ajax code could just fire that event with the html it fetched to help decouple the whole thing and make it easier to test each endpoint?

Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Rick

It was buried in my long covering letter - yes I did consider it, but this approach will suffer the same sad fate as when it was tried previously - there are separate yui instances involved due to js embedded in the widget TAL so event propagation won't work across instances and global events are verboten. So I have to use the pattern introduced previously ie coordinate things using a namespace.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for this branch. I have a few comments that need fixing before you land this branch.

=== modified file 'lib/lp/app/javascript/picker/person_picker.js'
--- lib/lp/app/javascript/picker/person_picker.js 2012-06-21 07:43:17 +0000
+++ lib/lp/app/javascript/picker/person_picker.js 2012-06-25 04:17:20 +0000
...

We prefer function expressions because they clearly show that the
function is a value of a variable. function statements are always hoisted
to the top of where scope is defined to ensure you can call them before they
are defined. Browsers and developers handle this badly.

+ function on_success(id, response, picker) {
        var on_success = function(id, response, picker) {

...
+ function on_failure(id, response, picker) {
        var on_failure = function(id, response, picker) {

=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
--- lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-21 03:48:05 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-25 04:17:20 +0000
...
I don't think the   belongs in this test, or in code to make this
visible to webkit...this link is missing action-icon. lazr-btn was intended
for <button>. I think a lot of code uses it as a marker, but we do not
have css rules to make it present weill with links.
+ <a id="edit-anotherpickertest-btn"
+ class="sprite edit lazr-btn yui3-activator-act"
+ href="/fnord/+edit-people"
+ >&nbsp;Edit</a>

=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.js'
--- lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-22 14:12:33 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-25 04:17:20 +0000
...

@@ -325,6 +329,32 @@
This markup is invalid. The YUI test will not play in some browsers, notably
IE which knows that <tr> elements may only contain <td>. Some browsers will
will add the <td> for you in the DOM...but that means scripts may break
because they assume there is no <td>.
+ _simple_team_form: function() {
+ return '<table><tr>' +
+ '<input id="field.name">' +
+ '<input id="field.displayname"></tr></table>';
+ },

review: Approve (code)
Revision history for this message
Ian Booth (wallyworld) wrote :

Thanks for the review.

>
> === modified file 'lib/lp/app/javascript/picker/person_picker.js'
> --- lib/lp/app/javascript/picker/person_picker.js 2012-06-21 07:43:17 +0000
> +++ lib/lp/app/javascript/picker/person_picker.js 2012-06-25 04:17:20 +0000
> ...
>
> We prefer function expressions because they clearly show that the
> function is a value of a variable. function statements are always hoisted
> to the top of where scope is defined to ensure you can call them before they
> are defined. Browsers and developers handle this badly.
>

Fair point. I would never do this myself if I were writing the code from
scratch. This code pattern was lifted from formoverlay, and is also used
in subscribers_list and filebug_dupfinder. I'll file a bug to ensure
these other places are fixed.

>
> === modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
> --- lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-21 03:48:05 +0000
> +++ lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-25 04:17:20 +0000
> ...
> I don't think the &nbsp; belongs in this test, or in code to make this
> visible to webkit...this link is missing action-icon. lazr-btn was intended
> for <button>. I think a lot of code uses it as a marker, but we do not
> have css rules to make it present weill with links.
> + <a id="edit-anotherpickertest-btn"
> + class="sprite edit lazr-btn yui3-activator-act"
> + href="/fnord/+edit-people"
> + >&nbsp;Edit</a>
>
>

Ah, the &nbsp; is old markup originally added to the test harness by
copying old rendered lp html before the recent css changes and this html
in the test was copied to create anotherpicker anchor for this branch.
The tests don't seem to care either way. I'll certainly amend the markup
to confirm with the recent changes.

> === modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.js'
> --- lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-22 14:12:33 +0000
> +++ lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-25 04:17:20 +0000
> ...
>
> @@ -325,6 +329,32 @@
> This markup is invalid. The YUI test will not play in some browsers, notably
> IE which knows that <tr> elements may only contain <td>. Some browsers will
> will add the <td> for you in the DOM...but that means scripts may break
> because they assume there is no <td>.
> + _simple_team_form: function() {
> + return '<table><tr>' +
> + '<input id="field.name">' +
> + '<input id="field.displayname"></tr></table>';
> + },
>

Yes, you are correct. Clearly my attempt at some trivial html for the
test failed. Will fix.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/formoverlay/formoverlay.js'
2--- lib/lp/app/javascript/formoverlay/formoverlay.js 2011-08-31 02:58:52 +0000
3+++ lib/lp/app/javascript/formoverlay/formoverlay.js 2012-06-26 00:18:26 +0000
4@@ -523,17 +523,17 @@
5 * @param url {String} The URL from where to load the form content.
6 */
7 loadFormContentAndRender: function (url) {
8- function on_success(id, response, overlay) {
9+ var on_success = function(id, response, overlay) {
10 overlay.set('form_content', response.responseText);
11 overlay.renderUI();
12 overlay.bindUI();
13- }
14- function on_failure(id, response, overlay) {
15+ };
16+ var on_failure = function(id, response, overlay) {
17 overlay.set(
18 'form_content',
19 "Sorry, an error occurred while loading the form.");
20 overlay.renderUI();
21- }
22+ };
23 var cfg = {
24 on: {success: on_success, failure: on_failure},
25 "arguments": this
26
27=== modified file 'lib/lp/app/javascript/picker/person_picker.js'
28--- lib/lp/app/javascript/picker/person_picker.js 2012-06-21 07:43:17 +0000
29+++ lib/lp/app/javascript/picker/person_picker.js 2012-06-26 00:18:26 +0000
30@@ -18,62 +18,50 @@
31 if (!Y.Lang.isValue(LP.links.me)) {
32 this.set('show_assign_me_button', false);
33 }
34- this.set('new_team_template', this._new_team_template());
35- this.set('new_team_form', this._new_team_form());
36+ if (this.get('show_create_team')) {
37+ // We need to provide the 'New team' link.
38+ // There could be several pickers and we only want to make the XHR
39+ // call to get the form once. So first one gets to do the call and
40+ // subsequent ones register the to be notified of the result.
41+ this.team_form_node = Y.Node.create(this._new_team_template());
42+ this.form_namespace = Y.namespace('lp.app.picker.teamform');
43+ var form_callbacks = this.form_namespace.form_callbacks;
44+ var perform_load = false;
45+ if (!Y.Lang.isArray(form_callbacks)) {
46+ perform_load = true;
47+ form_callbacks = [];
48+ this.form_namespace.form_callbacks = form_callbacks;
49+ }
50+ form_callbacks.push({
51+ picker: this,
52+ callback: this._render_new_team_form});
53+ this._load_new_team_form(perform_load);
54+ }
55 },
56
57 _new_team_template: function() {
58 return [
59 '<div class="new-team-node">',
60- '<div class="step-on" style="width: 100%;"></div>',
61- '<div class="transparent important-notice-popup">',
62- '{{> new_team_form}}',
63- '<div class="extra-form-buttons">',
64- '<button class="yes_button" type="button"></button>',
65- '<button class="no_button" type="button"></button>',
66+ '<div id=new-team-form-placeholder ',
67+ 'class="yui3-overlay-indicator-content">',
68+ '<img src="/@@/spinner-big/">',
69+ '</div>',
70+ '<div class="extra-form-buttons hidden">',
71+ '<button class="yes_button" type="button">',
72+ 'Create Team</button>',
73+ '<button class="no_button" type="button">Cancel</button>',
74 '</div>',
75 '</div>',
76 '</div>'].join('');
77 },
78
79- _new_team_form: function() {
80- // TODO - get the form using ++form++
81- return [
82- "<table id='launchpad-form-widgets' class='form'>",
83- "<tbody><tr><td colspan='2'><div>",
84- "<label for='field.name'>Name:</label><div>",
85- "<input type='text' value='' size='20'",
86- " name='field.name' id='field.name'",
87- " class='lowerCaseText textType'></div>",
88- "<p class='formHelp'>",
89- " A short unique name, beginning with a lower-case letter",
90- " or number, and containing only letters, numbers, dots,",
91- " hyphens, or plus signs.</p>",
92- "</div></td></tr><tr><td colspan='2'><div>",
93- "<label for='field.displayname'>Display Name:</label><div>",
94- "<input type='text' value='' size='20'",
95- " name='field.displayname' id='field.displayname'",
96- " class='textType'></div>",
97- "<p class='formHelp'>",
98- " This team's name as you would like it displayed",
99- " throughout Launchpad.</p>",
100- "</div></td></tr><tr><td colspan='2'><div>",
101- "<label for='field.visibility'>Visibility:</label>",
102- "<div><div><div class='value'>",
103- "<select size='1'",
104- " name='field.visibility' id='field.visibility'>",
105- "<option value='PUBLIC' selected='selected'>Public</option>",
106- "<option value='PRIVATE'>Private</option></select></div>",
107- "</div></div><p class='formHelp'>",
108- " Anyone can see a public team's data. Only team members",
109- " and Launchpad admins can see private team data.",
110- " Private teams cannot become public.</p>",
111- "</div></td></tr></tbody></table>"
112- ].join('');
113- },
114-
115 hide: function() {
116 this.get('boundingBox').setStyle('display', 'none');
117+ // We want to cancel the new team form is there is one rendered.
118+ var node = this.get('contentBox').one('.new-team-node');
119+ if (Y.Lang.isValue(node) && !node.hasClass('hidden')) {
120+ this.hide_extra_content(node, false);
121+ }
122 Y.lazr.picker.Picker.prototype.hide.call(this);
123 },
124
125@@ -128,72 +116,113 @@
126 });
127 },
128
129- _cancel_new_team: function(picker) {
130- var node = picker.get('contentBox').one('.new-team-node');
131- picker.hide_extra_content(node);
132+ _cancel_new_team: function() {
133+ var node = this.get('contentBox').one('.new-team-node');
134+ this.hide_extra_content(node);
135 },
136
137- _save_new_team: function(picker) {
138- var node = picker.get('contentBox').one('.new-team-node');
139- var team_name = Y.Node.getDOMNode(node.one('[id=field.name]')).value;
140- var team_display_name =
141- Y.Node.getDOMNode(node.one('[id=field.displayname]')).value;
142- picker.hide_extra_content(node);
143+ _save_new_team: function() {
144+ var node = this.get('contentBox').one('.new-team-node');
145+ var team_name = node.one('[id=field.name]').get('value');
146+ var team_display_name = node.one('[id=field.displayname]')
147+ .get('value');
148+ this.hide_extra_content(node, false);
149 // TODO - make back end call to save team
150 var value = {
151 "api_uri": "/~" + team_name,
152 "title": team_display_name,
153 "value": team_name,
154 "metadata": "team"};
155- picker.fire('validate', value);
156- },
157-
158- show_new_team_form: function () {
159- var partials = {new_team_form: this.get('new_team_form')};
160- var html = Y.lp.mustache.to_html(
161- this.get('new_team_template'), {}, partials);
162- var self = this;
163+ this.fire('validate', value);
164+ },
165+
166+ _load_new_team_form: function (perform_load) {
167+ // Load the new team form from the model using an XHR call.
168+ // If perform_load is true, this is the first invocation of this method
169+ // across all pickers so we do the XHR call and send the result to all
170+ // registered pickers.
171+ // If perform_load is false, another picker is making the XNR call and
172+ // all we want to do is receive and render the preloaded_team_form.
173+ // We first check though that the result hasn't arrived already.
174+ var preloaded_team_form = this.form_namespace.team_form;
175+ if (Y.Lang.isValue(preloaded_team_form)) {
176+ this._render_new_team_form(preloaded_team_form, true);
177+ return;
178+ }
179+ if (!perform_load) {
180+ return;
181+ }
182+
183+ var on_success = function(id, response, picker) {
184+ Y.Array.each(picker.form_namespace.form_callbacks,
185+ function(callback_info) {
186+ Y.bind(
187+ callback_info.callback, callback_info.picker,
188+ response.responseText, true)();
189+ });
190+ picker.form_namespace.team_form = response.responseText;
191+ };
192+ var on_failure = function(id, response, picker) {
193+ Y.Array.each(picker.form_namespace.form_callbacks,
194+ function(callback_info) {
195+ Y.bind(
196+ callback_info.callback, callback_info.picker,
197+ 'Sorry, an error occurred while loading the form.',
198+ false)();
199+ });
200+ };
201+ var cfg = {
202+ on: {success: on_success, failure: on_failure},
203+ "arguments": this
204+ };
205+ var uri = Y.lp.client.get_absolute_uri('people/+newteam/++form++');
206+ uri = uri.replace('api/devel', '');
207+ this.get("io_provider").io(uri, cfg);
208+ },
209+
210+ _render_new_team_form: function(form_html, show_submit) {
211+ // Poke the actual team form into the DOM and wire up the save and
212+ // cancel buttons.
213+ this.team_form_node.one('#new-team-form-placeholder')
214+ .replace(form_html);
215 var button_callback = function(e, callback_fn) {
216 e.halt();
217 if (Y.Lang.isFunction(callback_fn) ) {
218- callback_fn(self);
219+ Y.bind(callback_fn, this)();
220 }
221 };
222- var team_form_node = Y.Node.create(html);
223- team_form_node.one(".yes_button")
224- .set('text', 'Create Team')
225- .on('click', function(e) {
226- button_callback(e, self._save_new_team);
227- });
228+ var submit_button = this.team_form_node.one(".yes_button");
229+ if (show_submit) {
230+ submit_button.on(
231+ 'click', button_callback, this, this._save_new_team);
232+ } else {
233+ submit_button.addClass('hidden');
234+ }
235+ this.team_form_node.one(".no_button")
236+ .on('click', button_callback, this, this._cancel_new_team);
237+ this.team_form_node.one('.extra-form-buttons')
238+ .removeClass('hidden');
239+ },
240
241- team_form_node.one(".no_button")
242- .set('text', 'Cancel')
243- .on('click', function(e) {
244- button_callback(e, self._cancel_new_team);
245- });
246- this.get('contentBox').one('.yui3-widget-bd')
247- .insert(team_form_node, 'before');
248+ show_new_team_form: function() {
249 this.show_extra_content(
250- team_form_node.one(".important-notice-popup"),
251- "Enter new team details");
252+ this.team_form_node, "Enter new team details");
253 },
254
255 _assign_me_button_html: function() {
256 return [
257- '<a class="yui-picker-assign-me-button bg-image ',
258+ '<a class="yui-picker-assign-me-button sprite person ',
259 'js-action" href="javascript:void(0)" ',
260- 'style="background-image: url(/@@/person); ',
261- 'padding-right: 1em">',
262+ 'style="padding-right: 1em">',
263 this.get('assign_me_text'),
264 '</a>'].join('');
265 },
266
267 _remove_button_html: function() {
268 return [
269- '<a class="yui-picker-remove-button bg-image js-action" ',
270- 'href="javascript:void(0)" ',
271- 'style="background-image: url(/@@/remove); ',
272- 'padding-right: 1em">',
273+ '<a class="yui-picker-remove-button sprite remove ',
274+ 'js-action" href="javascript:void(0)" ',
275+ 'style="padding-right: 1em">',
276 this.get('remove_person_text'),
277 '</a>'].join('');
278 },
279@@ -250,7 +279,16 @@
280 min_search_chars: {value: 2},
281 show_create_team: {value: false},
282 new_team_template: {value: null},
283- new_team_form: {value: null}
284+ new_team_form: {value: null},
285+ /**
286+ * The object that provides the io function for doing XHR requests.
287+ *
288+ * @attribute io_provider
289+ * @type object
290+ * @default Y
291+ */
292+ io_provider: {value: Y}
293 }
294 });
295-}, "0.1", {"requires": ["base", "node", "lazr.picker", "lp.mustache"]});
296+}, "0.1", {"requires": [
297+ "base", "node", "lazr.picker"]});
298
299=== modified file 'lib/lp/app/javascript/picker/picker.js'
300--- lib/lp/app/javascript/picker/picker.js 2012-06-22 14:12:33 +0000
301+++ lib/lp/app/javascript/picker/picker.js 2012-06-26 00:18:26 +0000
302@@ -825,56 +825,81 @@
303 /*
304 * Insert the extra content into the form and animate its appearance.
305 */
306- show_extra_content: function(extra_content, header) {
307+ show_extra_content: function(extra_content, header, steptitle, progress) {
308 if (Y.Lang.isValue(header)) {
309- this.set('picker_header', this.get('headerContent'));
310+ if (!Y.Lang.isValue(this.get('saved_header'))) {
311+ this.set('saved_header', this.get('headerContent'));
312+ }
313 this.set(
314 'headerContent',
315 Y.Node.create("<h2></h2>").set('text', header));
316 }
317- this.get('contentBox').one('.yui3-widget-bd').hide();
318- this.get('contentBox').all('.steps').hide();
319- var duration = 0;
320- if (this.get('use_animation')) {
321- duration = 0.9;
322- }
323- var fade_in = new Y.Anim({
324- node: extra_content,
325- to: {opacity: 1},
326- duration: duration
327- });
328- fade_in.run();
329+ if (Y.Lang.isValue(steptitle)) {
330+ if (!Y.Lang.isValue(this.get('saved_steptitle'))) {
331+ this.set('saved_steptitle', this.get('steptitle'));
332+ }
333+ this.set('steptitle', steptitle);
334+ }
335+ if (Y.Lang.isValue(progress)) {
336+ if (!Y.Lang.isValue(this.get('saved_progress'))) {
337+ this.set('saved_progress', this.get('progress'));
338+ }
339+ this.set('progress', progress);
340+ }
341+ var contentBox = this.get('contentBox');
342+ var original_content = contentBox.one('.yui3-widget-bd');
343+ var extra_content_id = extra_content.get('id');
344+ if (!Y.Lang.isValue(contentBox.one('#'+extra_content_id))) {
345+ extra_content.addClass('important-notice-popup');
346+ original_content.insert(extra_content, 'before');
347+ }
348+ this._fade_in(extra_content, original_content);
349 },
350
351- hide_extra_content: function(extra_content_node) {
352- var saved_header = this.get('picker_header');
353+ hide_extra_content: function(extra_content_node, use_animation) {
354+ var saved_header = this.get('saved_header');
355 if (Y.Lang.isValue(saved_header)) {
356 this.set('headerContent', saved_header);
357- this.set('picker_header', null);
358- }
359- this.get('contentBox').all('.steps').show();
360+ this.set('saved_header', null);
361+ }
362+ var saved_steptitle = this.get('saved_steptitle');
363+ if (Y.Lang.isValue(saved_steptitle)) {
364+ this.set('steptitle', saved_steptitle);
365+ this.set('saved_steptitle', null);
366+ }
367+ var saved_progress = this.get('saved_progress');
368+ if (Y.Lang.isValue(saved_progress)) {
369+ this.set('progress', saved_progress);
370+ this.set('saved_progress', null);
371+ }
372 var content_node = this.get('contentBox').one('.yui3-widget-bd');
373- if (extra_content_node !== null) {
374- extra_content_node.get('parentNode')
375- .removeChild(extra_content_node);
376- content_node.addClass('transparent');
377- content_node.setStyle('opacity', 0);
378- content_node.show();
379- var duration = 0;
380- if (this.get('use_animation')) {
381- duration = 0.6;
382- }
383- var content_fade_in = new Y.Anim({
384- node: content_node,
385- to: {opacity: 1},
386- duration: duration
387- });
388- content_fade_in.run();
389- } else {
390+ this._fade_in(content_node, extra_content_node, use_animation);
391+ },
392+
393+ _fade_in: function(content_node, old_content, use_animation) {
394+ content_node.removeClass('hidden');
395+ if (old_content === null) {
396 content_node.removeClass('transparent');
397 content_node.setStyle('opacity', 1);
398 content_node.show();
399- }
400+ return;
401+ }
402+ old_content.addClass('hidden');
403+ if (!Y.Lang.isValue(use_animation)) {
404+ use_animation = this.get('use_animation');
405+ }
406+ if (!use_animation) {
407+ old_content.setStyle('opacity', 1);
408+ return;
409+ }
410+ content_node.addClass('transparent');
411+ content_node.setStyle('opacity', 0);
412+ var fade_in = new Y.Anim({
413+ node: content_node,
414+ to: {opacity: 1},
415+ duration: 0.8
416+ });
417+ fade_in.run();
418 },
419
420 /*
421
422=== modified file 'lib/lp/app/javascript/picker/picker_patcher.js'
423--- lib/lp/app/javascript/picker/picker_patcher.js 2012-06-21 03:48:05 +0000
424+++ lib/lp/app/javascript/picker/picker_patcher.js 2012-06-26 00:18:26 +0000
425@@ -247,14 +247,11 @@
426
427 var node = Y.Node.create(
428 ['<div class="validation-node">',
429- '<div class="step-on" style="width: 100%;"></div>',
430- '<div class="transparent important-notice-popup">',
431 '<div class="validation-content-placeholder"></div>',
432 '<div class="extra-form-buttons">',
433 '<button class="yes_button" type="button"></button>',
434 '<button class="no_button" type="button"></button>',
435 '</div>',
436- '</div>',
437 '</div>'].join(''));
438
439 var button_callback = function(e, callback_fn) {
440@@ -272,8 +269,7 @@
441 .on('click', function(e) { button_callback(e, no_fn); });
442
443 node.one(".validation-content-placeholder").replace(content);
444- picker.get('contentBox').one('.yui3-widget-bd').insert(node, 'before');
445- picker.show_extra_content(node.one(".important-notice-popup"));
446+ picker.show_extra_content(node);
447 };
448
449 /*
450@@ -315,6 +311,9 @@
451 function reset_form(picker) {
452 var validation_node = picker.get('contentBox').one('.validation-node');
453 picker.hide_extra_content(validation_node);
454+ if (Y.Lang.isValue(validation_node)) {
455+ validation_node.remove(true);
456+ }
457 }
458
459
460@@ -348,7 +347,6 @@
461 validators, picker, picker_result, do_save, do_cancel) {
462 if (validators.length === 0) {
463 do_save();
464- reset_form(picker);
465 return;
466 }
467 var validator_callback = validators.pop();
468@@ -495,6 +493,7 @@
469 Y.log('Got save event.');
470 var picker_result = e.details[Y.lazr.picker.Picker.SAVE_RESULT];
471 user_has_searched = false;
472+ reset_form(picker);
473 picker.hide();
474 if (Y.Lang.isFunction(config.save)) {
475 config.save(picker_result);
476
477=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
478--- lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-21 03:48:05 +0000
479+++ lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-26 00:18:26 +0000
480@@ -36,8 +36,6 @@
481 <script type="text/javascript"
482 src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
483 <script type="text/javascript"
484- src="../../../../../../build/js/lp/app/mustache.js"></script>
485- <script type="text/javascript"
486 src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
487 <script type="text/javascript"
488 src="../../../../../../build/js/lp/app/effects/effects.js"></script>
489@@ -49,6 +47,8 @@
490 src="../../../../../../build/js/lp/app/picker/picker.js"></script>
491 <script type="text/javascript"
492 src="../../../../../../build/js/lp/app/picker/picker_patcher.js"></script>
493+ <script type="text/javascript"
494+ src="../../../../../../build/js/lp/app/testing/mockio.js"></script>
495
496
497 <!-- The module under test. -->
498@@ -69,9 +69,26 @@
499 <span>
500 A picker widget test
501 <a id="edit-pickertest-btn"
502- class="sprite edit lazr-btn yui3-activator-act"
503- href="/fnord/+edit-people"
504- >&nbsp;Edit</a>
505+ class="sprite edit action-icon yui3-activator-act"
506+ href="/fnord/+edit-people"
507+ >Edit</a>
508+ </span>
509+ <span class="yui3-activator-data-box">
510+ </span>
511+ <span class="yui3-activator-message-box yui3-activator-hidden"></span>
512+ </span>
513+ </span>
514+ </div>
515+
516+ <div class="yui3-widget yui3-activator yui3-activator-focused">
517+ <span id="anotherpicker_id" class="yui3-activator-content yui3-activator-success">
518+ <span id="anotherpickertest">
519+ <span>
520+ Another picker widget test
521+ <a id="edit-anotherpickertest-btn"
522+ class="sprite edit action-icon yui3-activator-act"
523+ href="/fnord/+edit-people"
524+ >Edit</a>
525 </span>
526 <span class="yui3-activator-data-box">
527 </span>
528
529=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.js'
530--- lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-22 14:12:33 +0000
531+++ lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-26 00:18:26 +0000
532@@ -3,7 +3,7 @@
533
534 YUI().use('test', 'console', 'plugin',
535 'lazr.picker', 'lazr.person-picker', 'lp.app.picker',
536- 'lp.app.mustache', 'node-event-simulate', function(Y) {
537+ 'lp.testing.mockio', 'node-event-simulate', function(Y) {
538
539 var Assert = Y.Assert;
540
541@@ -88,6 +88,10 @@
542
543 tearDown: function() {
544 cleanup_widget(this.picker);
545+ var form_namespace = Y.namespace('lp.app.picker.teamform');
546+ form_namespace.form_callbacks = null;
547+ form_namespace.team_form = null;
548+ delete this.mockio;
549 delete window.LP;
550 },
551
552@@ -325,6 +329,32 @@
553 Assert.isNull(Y.one('.yui-picker-new-team-button'));
554 },
555
556+ _simple_team_form: function() {
557+ return '<table><tr><td></td>' +
558+ '<input id="field.name">' +
559+ '<input id="field.displayname"></td></tr></table>';
560+ },
561+
562+ test_picker_new_team_xhr_calls: function() {
563+ // Only one XHR call is made to fetch the team form even if more
564+ // than one picker is used.
565+ this.create_picker(this._picker_params(false, false, true));
566+ var config = {
567+ "io_provider": this.mockio,
568+ "use_animation": false,
569+ "picker_type": "person",
570+ "show_create_team": true
571+ };
572+ var another = Y.lp.app.picker.addPickerPatcher(
573+ this.vocabulary,
574+ "foo/bar",
575+ "test_link",
576+ "anotherpicker_id",
577+ config);
578+ Y.Assert.areEqual(1, this.mockio.requests.length);
579+ another.destroy();
580+ },
581+
582 test_picker_new_team_button_click_shows_form: function() {
583 // Clicking the new team button displays the new team form.
584 this.create_picker(this._picker_params(true, true, true));
585@@ -336,7 +366,8 @@
586 'Enter new team details',
587 this.picker.get('headerContent').get('text'));
588 Y.Assert.isNotNull(
589- this.picker.get('contentBox').one('[id=field.name]'));
590+ this.picker.get('contentBox')
591+ .one('input[id=field.name]'));
592 Y.Assert.areEqual('none',
593 this.picker.get('contentBox').one('.yui3-widget-bd')
594 .getStyle('display'));
595@@ -360,8 +391,9 @@
596 Y.Assert.areEqual(
597 'Pick Someone',
598 this.picker.get('headerContent').get('text'));
599- Y.Assert.isNull(
600- this.picker.get('contentBox').one('[id=field.name]'));
601+ Y.Assert.isNotNull(
602+ this.picker.get('contentBox').one('input[id=field.name]')
603+ .ancestor('div.hidden'));
604 Y.Assert.isNotNull(
605 this.picker.get('contentBox').one('.yui3-picker-search'));
606 },
607@@ -385,8 +417,8 @@
608 var new_team =
609 picker_content.one('.yui-picker-new-team-button');
610 new_team.simulate('click');
611- var team_name = picker_content.one('[id=field.name]');
612- Y.Node.getDOMNode(team_name).value = 'fred';
613+ var team_name = picker_content.one('input[id=field.name]');
614+ team_name.set('value', 'fred');
615 var form_buttons = picker_content.one('.extra-form-buttons');
616 simulate(
617 form_buttons, 'button:nth-child(1)', 'click');
618@@ -409,7 +441,9 @@
619 data_box.one('a').set('href', field_value);
620 }
621
622+ this.mockio = new Y.lp.testing.mockio.MockIo();
623 var config = {
624+ "io_provider": this.mockio,
625 "use_animation": false,
626 "picker_type": "person",
627 "step_title": "Choose someone",
628@@ -431,6 +465,14 @@
629 "test_link",
630 "picker_id",
631 config);
632+ if (params.show_create_team) {
633+ Y.Assert.areEqual(
634+ 'file:////people/+newteam/++form++',
635+ this.mockio.last_request.url);
636+ this.mockio.success({
637+ responseText: this._simple_team_form(),
638+ responseHeaders: {'Content-Type': 'text/html'}});
639+ }
640 }
641 };
642
643@@ -444,6 +486,9 @@
644
645 tearDown: function() {
646 cleanup_widget(this.picker);
647+ var form_namespace = Y.namespace('lp.app.picker.teamform');
648+ form_namespace.form_callbacks = null;
649+ form_namespace.team_form = null;
650 this.search_input.remove();
651 delete window.LP;
652 },
653@@ -458,7 +503,10 @@
654 if (field_value !== undefined) {
655 text_field.set('text', field_value);
656 }
657+ this.mockio = new Y.lp.testing.mockio.MockIo();
658 var config = {
659+ "io_provider": this.mockio,
660+ "use_animation": false,
661 "picker_type": "person",
662 "header": "Pick Someone",
663 "associated_field_id": associated_field_id,
664@@ -473,6 +521,14 @@
665 };
666 this.picker = Y.lp.app.picker.create(
667 this.vocabulary, config, associated_field_id);
668+ if (params.show_create_team) {
669+ Y.Assert.areEqual(
670+ 'file:////people/+newteam/++form++',
671+ this.mockio.last_request.url);
672+ this.mockio.success({
673+ responseText: this._simple_team_form(),
674+ responseHeaders: {'Content-Type': 'text/html'}});
675+ }
676 }
677 };
678
679
680=== modified file 'lib/lp/app/javascript/picker/tests/test_picker_patcher.js'
681--- lib/lp/app/javascript/picker/tests/test_picker_patcher.js 2012-04-06 17:28:25 +0000
682+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.js 2012-06-26 00:18:26 +0000
683@@ -71,6 +71,7 @@
684
685 create_picker: function(validate_callback, extra_config) {
686 var config = {
687+ "use_animation": false,
688 "step_title": "Choose someone",
689 "header": "Pick Someone",
690 "null_display_value": "No one",
691@@ -96,7 +97,7 @@
692 create_picker_direct: function(associated_field) {
693 this.picker = Y.lp.app.picker.create(
694 this.vocabulary,
695- undefined,
696+ {use_animation: false},
697 associated_field);
698 var self = this;
699 this.picker.subscribe('save', function(e) {
700@@ -118,7 +119,8 @@
701 yesno_validate_callback: function(expected_value) {
702 return function(picker, value, save_fn, cancel_fn) {
703 Assert.areEqual(
704- expected_value, value.api_uri, "unexpected picker value");
705+ expected_value,
706+ value.api_uri, "unexpected picker value");
707 if (value === null) {
708 return true;
709 }
710@@ -189,9 +191,10 @@
711 Assert.areEqual('message', footer_slot.get('text'));
712 },
713
714- test_footer_node_preserved_without_extra_no_results_message: function () {
715- // If "extra_no_results_message" is not defined, the footer slot node
716- // should be preserved.
717+ test_footer_node_preserved_without_extra_no_results_message:
718+ function () {
719+ // If "extra_no_results_message" is not defined, the footer slot
720+ // node should be preserved.
721 this.create_picker();
722 var footer_node = Y.Node.create("<span>foobar</span>");
723 this.picker.set('footer_slot', footer_node);
724@@ -211,7 +214,8 @@
725 simulate(
726 this.picker.get('boundingBox').one('.yui3-picker-results'),
727 'li:nth-child(2)', 'click');
728- var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
729+ var yesno = this.picker.get('contentBox')
730+ .one('.extra-form-buttons');
731
732 simulate(
733 yesno, 'button:nth-child(1)', 'click');
734@@ -232,7 +236,8 @@
735 simulate(
736 this.picker.get('boundingBox').one('.yui3-picker-results'),
737 'li:nth-child(2)', 'click');
738- var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
739+ var yesno = this.picker.get('contentBox')
740+ .one('.extra-form-buttons');
741
742 simulate(
743 yesno, 'button:nth-child(1)', 'click');
744@@ -249,7 +254,8 @@
745 simulate(
746 this.picker.get('boundingBox').one('.yui3-picker-results'),
747 'li:nth-child(2)', 'click');
748- var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
749+ var yesno = this.picker.get('contentBox')
750+ .one('.extra-form-buttons');
751 simulate(
752 yesno, 'button:nth-child(2)', 'click');
753 Assert.isNull(this.saved_picker_value);
754@@ -260,7 +266,8 @@
755 simulate(
756 this.picker.get('boundingBox').one('.yui3-picker-results'),
757 'li:nth-child(2)', 'click');
758- var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
759+ var yesno = this.picker.get('contentBox')
760+ .one('.extra-form-buttons');
761 // Click the Yes button.
762 simulate(yesno, 'button:nth-child(1)', 'click');
763 yesno = this.picker.get('contentBox').one('.extra-form-buttons');
764@@ -329,7 +336,8 @@
765 simulate(
766 this.picker.get('boundingBox').one('.yui3-picker-results'),
767 'li:nth-child(2)', 'click');
768- var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
769+ var yesno = this.picker.get('contentBox')
770+ .one('.extra-form-buttons');
771 // Click the Yes button.
772 simulate(yesno, 'button:nth-child(1)', 'click');
773 yesno = this.picker.get('contentBox').one('.extra-form-buttons');
774@@ -339,8 +347,8 @@
775 },
776
777 test_connect_select_menu: function() {
778- // connect_select_menu() connects the select menu's onchange event to
779- // copy the selected value to the text input field.
780+ // connect_select_menu() connects the select menu's onchange event
781+ // to copy the selected value to the text input field.
782 this.text_input = Y.Node.create(
783 '<input id="field.testfield" value="foo" />');
784 var node = Y.one(document.body).appendChild(this.text_input);
785@@ -390,7 +398,8 @@
786 'li:nth-child(2)', 'click');
787 // expected_text is a little weird since breaks between p tags and
788 // buttons are lost.
789- var expected_text = 'This action will reveal this team\'s name to ' +
790+ var expected_text =
791+ 'This action will reveal this team\'s name to ' +
792 'the public.ContinueChoose Again';
793 var text = Y.one(".validation-node").get('text');
794 Assert.areEqual(expected_text, text);
795@@ -473,8 +482,9 @@
796 },
797
798 test_picker_displays_warning_by_default: function() {
799- // If show_search_box is not supplied in config, it defaults to true.
800- // Thus the picker will refuse to display more than 120 values.
801+ // If show_search_box is not supplied in config, it defaults to
802+ // true. Thus the picker will refuse to display more than 120
803+ // values.
804 this.create_picker();
805 this.picker.set('min_search_chars', 0);
806 this.picker.fire('search', '');
807@@ -536,9 +546,9 @@
808 this.mock_io.failure(
809 {responseHeaders: this.get_oops_headers('OOPS')});
810 Assert.areEqual(
811- "Sorry, something went wrong with your search. We've recorded " +
812- "what happened, and we'll fix it as soon as possible. " +
813- "(Error ID: OOPS)",
814+ "Sorry, something went wrong with your search. " +
815+ "We've recorded what happened, and we'll fix it as soon " +
816+ "as possible. (Error ID: OOPS)",
817 this.picker.get('error'));
818 },
819
820@@ -615,9 +625,10 @@
821 },
822
823 test_automated_search_error_ignored_if_user_has_searched: function() {
824- // If an automated search (like loading branch suggestions) returns an
825- // error and the user has submitted a search, then the error from the
826- // automated search is ignored so as not to confuse the user.
827+ // If an automated search (like loading branch suggestions) returns
828+ // an error and the user has submitted a search, then the error
829+ // from the automated search is ignored so as not to confuse the
830+ // user.
831 var mock_io = new Y.lp.testing.mockio.MockIo();
832 var picker = this.create_picker(mock_io);
833 picker.fire('search', 'test');
834@@ -629,7 +640,7 @@
835
836 }));
837
838-}, '0.1', {'requires': ['test', 'console', 'lp.pickert', 'node', 'lp', 'lp.client',
839+}, '0.1', {'requires': ['test', 'console', 'lp.app.picker', 'node',
840 'event-focus', 'event-simulate', 'lazr.picker', 'lazr.person-picker',
841 'lp.app.picker', 'node-event-simulate', 'escape', 'event',
842- 'lp.testing.mockio',]});
843+ 'lp.testing.mockio', 'lp', 'lp.client']});
844
845=== modified file 'lib/lp/app/javascript/subscribers/subscribers_list.js'
846--- lib/lp/app/javascript/subscribers/subscribers_list.js 2012-06-07 19:43:37 +0000
847+++ lib/lp/app/javascript/subscribers/subscribers_list.js 2012-06-26 00:18:26 +0000
848@@ -255,7 +255,7 @@
849 var loader = this;
850
851 // Fetch the person and add a subscription.
852- function on_success(subscribers) {
853+ var on_success = function(subscribers) {
854 loader._loadSubscribersFromList(subscribers);
855 // We may need to set up the subscribe me link.
856 // This has to be done after subscribers have been loaded so that we
857@@ -264,7 +264,7 @@
858 loader._setupSubscribeMe();
859 }
860 loader.subscribers_list.stopActivity();
861- }
862+ };
863
864 var config = { on: {
865 success: on_success,
866@@ -484,7 +484,7 @@
867 }
868 var loader = this;
869
870- function on_success() {
871+ var on_success = function() {
872 var unsubscribe_callback = loader._getUnsubscribeCallback();
873 if (update_subscribers_list) {
874 loader.subscribers_list.stopSubscriberActivity(subscriber, true);
875@@ -502,8 +502,8 @@
876 };
877 loader._updateSubscribeMeLink(true);
878 }
879- }
880- function on_failure(t_id, response) {
881+ };
882+ var on_failure = function(t_id, response) {
883 if (update_subscribers_list) {
884 loader.subscribers_list.stopSubscriberActivity(
885 subscriber, false, function() {
886@@ -524,7 +524,7 @@
887 false,
888 error_msg
889 );
890- }
891+ };
892 var config = {
893 on: { success: on_success,
894 failure: on_failure },
895
896=== modified file 'lib/lp/bugs/javascript/filebug_dupefinder.js'
897--- lib/lp/bugs/javascript/filebug_dupefinder.js 2012-05-25 14:38:42 +0000
898+++ lib/lp/bugs/javascript/filebug_dupefinder.js 2012-06-26 00:18:26 +0000
899@@ -177,7 +177,7 @@
900 search_button.removeClass(UNSEEN);
901 }
902
903- function on_success(transaction_id, response, args) {
904+ var on_success = function(transaction_id, response, args) {
905 // Hide the spinner and show the duplicates.
906 Y.one('#spinner').addClass(UNSEEN);
907
908@@ -216,7 +216,7 @@
909 // Finally, change the label on the search button and show it again.
910 search_button.set('value', 'Check again');
911 search_button.removeClass(UNSEEN);
912- }
913+ };
914
915 var search_term = encodeURI(search_field.get('value'));
916 var search_url = search_url_base + '?title=' + search_term;
917
918=== modified file 'lib/lp/registry/javascript/sharing/shareepicker.js'
919--- lib/lp/registry/javascript/sharing/shareepicker.js 2012-04-26 21:01:19 +0000
920+++ lib/lp/registry/javascript/sharing/shareepicker.js 2012-06-26 00:18:26 +0000
921@@ -29,10 +29,6 @@
922 },
923 sharing_permissions: {
924 value: []
925- },
926- // Override for testing
927- anim_duration: {
928- value: 1
929 }
930 };
931
932@@ -77,26 +73,6 @@
933 });
934 },
935
936- _fade_in: function(content_node, old_content) {
937- content_node.removeClass('unseen');
938- if (old_content === null) {
939- return;
940- }
941- old_content.addClass('unseen');
942- var anim_duration = this.get('anim_duration');
943- if (anim_duration === 0) {
944- return;
945- }
946- content_node.addClass('transparent');
947- content_node.setStyle('opacity', 0);
948- var fade_in = new Y.Anim({
949- node: content_node,
950- to: {opacity: 1},
951- duration: anim_duration
952- });
953- fade_in.run();
954- },
955-
956 _display_step_one: function() {
957 this.set('headerContent', this.step_one_header);
958 this.set(
959
960=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareepicker.js'
961--- lib/lp/registry/javascript/sharing/tests/test_shareepicker.js 2012-04-26 21:01:19 +0000
962+++ lib/lp/registry/javascript/sharing/tests/test_shareepicker.js 2012-06-26 00:18:26 +0000
963@@ -54,7 +54,7 @@
964
965 _create_picker: function(overrides) {
966 var config = {
967- anim_duration: 0,
968+ use_animation: false,
969 progressbar: true,
970 progress: 50,
971 headerContent: "<h2>Share with a user or team</h2>",
972@@ -111,7 +111,7 @@
973 // The progress should be 75%
974 Y.Assert.areEqual(75, this.picker.get('progress'));
975 // The first step ui should be hidden.
976- Y.Assert.isTrue(cb.one('.yui3-widget-bd').hasClass('unseen'));
977+ Y.Assert.isTrue(cb.one('.yui3-widget-bd').hasClass('hidden'));
978 // The step title should be updated according to the selected
979 // person. The title should remain unchanged.
980 Y.Assert.areEqual(
981@@ -122,7 +122,7 @@
982 'Select sharing policies for Fred', steptitle);
983 // The second step ui should be visible.
984 var step_two_content = cb.one('.picker-content-two');
985- Y.Assert.isFalse(step_two_content.hasClass('unseen'));
986+ Y.Assert.isFalse(step_two_content.hasClass('hidden'));
987 // The second step ui should contain input buttons for each access
988 // policy type for each sharing permission.
989 Y.Array.each(this.information_types, function(info_type) {
990@@ -281,7 +281,7 @@
991 // The progress should be 50%
992 Y.Assert.areEqual(50, this.picker.get('progress'));
993 // The first step ui should be visible.
994- Y.Assert.isFalse(cb.one('.yui3-widget-bd').hasClass('unseen'));
995+ Y.Assert.isFalse(cb.one('.yui3-widget-bd').hasClass('hidden'));
996 // The title and step title should be updated.
997 Y.Assert.areEqual(
998 'Share with a user or team',
999@@ -291,7 +291,7 @@
1000 'Search for user or exclusive team with whom to share',
1001 steptitle);
1002 // The second step ui should be hidden.
1003- Y.Assert.isTrue(step_two_content.hasClass('unseen'));
1004+ Y.Assert.isTrue(step_two_content.hasClass('hidden'));
1005 },
1006
1007 // Test that a selection made in step two is correctly passed to the