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

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 15475
Proposed branch: lp:~wallyworld/launchpad/new-team-picker
Merge into: lp:launchpad
Prerequisite: lp:~jcsackett/launchpad/cleanup-pickers-with-base-create
Diff against target: 1171 lines (+491/-226)
11 files modified
lib/lp/app/browser/lazrjs.py (+10/-2)
lib/lp/app/browser/tests/test_inlineeditpickerwidget.py (+20/-2)
lib/lp/app/javascript/picker/person_picker.js (+148/-18)
lib/lp/app/javascript/picker/picker.js (+82/-16)
lib/lp/app/javascript/picker/picker_patcher.js (+17/-36)
lib/lp/app/javascript/picker/tests/test_personpicker.html (+2/-0)
lib/lp/app/javascript/picker/tests/test_personpicker.js (+158/-135)
lib/lp/app/widgets/popup.py (+23/-13)
lib/lp/app/widgets/tests/test_popup.py (+21/-0)
lib/lp/registry/browser/product.py (+4/-4)
lib/lp/services/features/flags.py (+6/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/new-team-picker
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Jelmer Vernooij Pending
Review via email: mp+111346@code.launchpad.net

Commit message

Initial infrastructure to provide a "New Team" link on person pickers.

Description of the change

== Implementation ==

The LOC is a little high, but much of this is a necessary cut and paste to remove duplicate code from existing person picker tests. Also, there is some hardwired html which will be replaced by a generated form.

This branch is the first in a series which will add the ability to create a new team directly from within the person picker. There is one view in Launchpad which offers the user a chance to create a new team alongside providing a person picker. This is the product +edit-people view, and what is rendered is a text input field for typing the field value (eg maintainer) and next to that two links: a "Choose..." link and a "or create new team" link. However, with inline pickers being used on the project overview page for setting maintainer and driver, only people without javascript actually see this view.

So what is done is that a "New Team" link is provided for the person picker. This link is rendered in the "extra buttons" div alongside the "Pick Me" and "Remove Person" links. The new team link is part of the picker and so is available regardless of how the picker is created, inline popup or Choose... link. So part of the work therefore is to have the picker javascript convert the "Choose... or Create new team" pair of links into a single, simple Choose... link.

As is currently the case, only person pickers configured to "show_create_team_link" have the create team functionality. This is used on product maintainer and driver fields.

The feature is controlled by a feature flag: "disclosure.add-team-person-picker.enabled". Without the flag, everything renders as before.

This is only an initial implementation to get the base infrastructure in place. Current unfinished things include:

- The create team form html is hardwired into the picker as a mustache template. It needs to be fetched from the backend using a ++form++ call.
- The form doesn't allow subscription policy to be set.
- The team is not actually created. A 'save' event is published by the picker as if it were created allowing the feature to be demoed.

Some code in the picker stuff was moved. There was code in picker_patcher to show and hide the validation forms used to confirm a user's selection. These methods were moved to Picker itself and made into generic functions to show/hide arbitrary forms (eg the team creation form).

== Demo ==

http://people.canonical.com/~ianb/picker-new-team-demo.ogv

== Tests ==

Add tests for the back end widgets: test_popup and test_inlineeditpickerwidget:
- test_show_create_team_link_with_feature_flag
- test_show_create_team_link
- test_create_team_link

Move duplicated tests in test_personpicker to a common base class.

Add new yui tests for the create team functionality:
- test_picker_no_team_button_unless_configured
- test_picker_new_team_button_click_shows_form
- test_picker_new_team_cancel
- test_picker_new_team_save

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/browser/lazrjs.py
  lib/lp/app/browser/tests/test_inlineeditpickerwidget.py
  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/widgets/popup.py
  lib/lp/app/widgets/tests/test_popup.py
  lib/lp/registry/browser/product.py
  lib/lp/services/features/flags.py

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

Thank you for this improvement. I have a few questions, but they wont block you because you have put this behind a feature flag (and this is an incremental branch.)

lines 121 and 122: <buttons> must have text for so users with screen readers can know what they do. Oh. I see the script sets the text later? Why? The buttons will only ever have one value, so you could inline the text in the string.

Why do you need a native DOM node. I see this pattern starting about line 190?
     Y.Node.getDOMNode(node.one('[id=field.name]')).value;
I expected
     node.one('[id=field.name]').get('value');

On 237 I see an image being loaded when the sprite is already loaded and the padding does not match the image:
    'style="background-image: url(/@@/person); ',
could be
    class="sprite person"

And in lines 247, maybe this should be
    class="sprite remove"

^ In both the preceding cases there is a class, but it probably duplicates sprite rules. Can we just use sprites? That is to say, this is no longer lazr.js. I see you did exactly as I expect for a new team.

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

>
> lines 121 and 122: <buttons> must have text for so users with screen readers can know what they do. Oh. I see the script sets the text later? Why? The buttons will only ever have one value, so you could inline the text in the string.
>
I can do that. The pattern I used mimicked the approach used in the
picker validation forms where the button did have different text and so
a generic form skeleton was created and the button text set later. But
here it is fixed, you are right.

> Why do you need a native DOM node. I see this pattern starting about line 190?
> Y.Node.getDOMNode(node.one('[id=field.name]')).value;
> I expected
> node.one('[id=field.name]').get('value');
>

I did indeed try the approach you suggest but get('value') kept
returning ''. I have no idea why. I'll try it again.

> On 237 I see an image being loaded when the sprite is already loaded and the padding does not match the image:
> 'style="background-image: url(/@@/person); ',
> could be
> class="sprite person"
>
> And in lines 247, maybe this should be
> class="sprite remove"
>
> ^ In both the preceding cases there is a class, but it probably duplicates sprite rules. Can we just use sprites? That is to say, this is no longer lazr.js. I see you did exactly as I expect for a new team.

That code was cut and pasted from elsewhere and put into a method for
convenience. Yes, I should have noticed the old pattern and replaced
with sprites. I'll fix.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/browser/lazrjs.py'
--- lib/lp/app/browser/lazrjs.py 2012-06-04 14:06:41 +0000
+++ lib/lp/app/browser/lazrjs.py 2012-06-22 14:19:22 +0000
@@ -346,7 +346,8 @@
346class InlinePersonEditPickerWidget(InlineEditPickerWidget):346class InlinePersonEditPickerWidget(InlineEditPickerWidget):
347 def __init__(self, context, exported_field, default_html,347 def __init__(self, context, exported_field, default_html,
348 content_box_id=None, header='Select an item',348 content_box_id=None, header='Select an item',
349 step_title='Search', assign_me_text='Pick me',349 step_title='Search', show_create_team=False,
350 assign_me_text='Pick me',
350 remove_person_text='Remove person',351 remove_person_text='Remove person',
351 remove_team_text='Remove team',352 remove_team_text='Remove team',
352 null_display_value='None',353 null_display_value='None',
@@ -372,13 +373,13 @@
372 in and when JS is off. Defaults to the edit_view on the context.373 in and when JS is off. Defaults to the edit_view on the context.
373 :param edit_title: Used to set the title attribute of the anchor.374 :param edit_title: Used to set the title attribute of the anchor.
374 :param help_link: Used to set a link for help for the widget.375 :param help_link: Used to set a link for help for the widget.
375 :param target_context: The target the person is being set for.
376 """376 """
377 super(InlinePersonEditPickerWidget, self).__init__(377 super(InlinePersonEditPickerWidget, self).__init__(
378 context, exported_field, default_html, content_box_id, header,378 context, exported_field, default_html, content_box_id, header,
379 step_title, null_display_value,379 step_title, null_display_value,
380 edit_view, edit_url, edit_title, help_link)380 edit_view, edit_url, edit_title, help_link)
381381
382 self._show_create_team = show_create_team
382 self.assign_me_text = assign_me_text383 self.assign_me_text = assign_me_text
383 self.remove_person_text = remove_person_text384 self.remove_person_text = remove_person_text
384 self.remove_team_text = remove_team_text385 self.remove_team_text = remove_team_text
@@ -399,11 +400,18 @@
399 user = getUtility(ILaunchBag).user400 user = getUtility(ILaunchBag).user
400 return user and user in vocabulary401 return user and user in vocabulary
401402
403 @property
404 def show_create_team(self):
405 return (self._show_create_team
406 and getFeatureFlag(
407 "disclosure.add-team-person-picker.enabled"))
408
402 def getConfig(self):409 def getConfig(self):
403 config = super(InlinePersonEditPickerWidget, self).getConfig()410 config = super(InlinePersonEditPickerWidget, self).getConfig()
404 config.update(dict(411 config.update(dict(
405 show_remove_button=self.optional_field,412 show_remove_button=self.optional_field,
406 show_assign_me_button=self.show_assign_me_button,413 show_assign_me_button=self.show_assign_me_button,
414 show_create_team=self.show_create_team,
407 assign_me_text=self.assign_me_text,415 assign_me_text=self.assign_me_text,
408 remove_person_text=self.remove_person_text,416 remove_person_text=self.remove_person_text,
409 remove_team_text=self.remove_team_text))417 remove_team_text=self.remove_team_text))
410418
=== modified file 'lib/lp/app/browser/tests/test_inlineeditpickerwidget.py'
--- lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2012-01-01 02:58:52 +0000
+++ lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2012-06-22 14:19:22 +0000
@@ -15,6 +15,7 @@
15 InlineEditPickerWidget,15 InlineEditPickerWidget,
16 InlinePersonEditPickerWidget,16 InlinePersonEditPickerWidget,
17 )17 )
18from lp.services.features.testing import FeatureFixture
18from lp.testing import (19from lp.testing import (
19 login_person,20 login_person,
20 TestCaseWithFactory,21 TestCaseWithFactory,
@@ -68,7 +69,7 @@
6869
69 layer = DatabaseFunctionalLayer70 layer = DatabaseFunctionalLayer
7071
71 def getWidget(self, widget_value, **kwargs):72 def getWidget(self, widget_value, show_create_team=False, **kwargs):
72 class ITest(Interface):73 class ITest(Interface):
73 test_field = Choice(**kwargs)74 test_field = Choice(**kwargs)
7475
@@ -80,7 +81,8 @@
8081
81 context = Test()82 context = Test()
82 return InlinePersonEditPickerWidget(83 return InlinePersonEditPickerWidget(
83 context, ITest['test_field'], None, edit_url='fake')84 context, ITest['test_field'], None, edit_url='fake',
85 show_create_team=show_create_team)
8486
85 def test_person_selected_value_meta(self):87 def test_person_selected_value_meta(self):
86 # The widget has the correct meta value for a person value.88 # The widget has the correct meta value for a person value.
@@ -115,3 +117,19 @@
115 None, vocabulary='TargetPPAs', required=True)117 None, vocabulary='TargetPPAs', required=True)
116 login_person(self.factory.makePerson())118 login_person(self.factory.makePerson())
117 self.assertFalse(widget.config['show_assign_me_button'])119 self.assertFalse(widget.config['show_assign_me_button'])
120
121 def test_show_create_team_link_with_feature_flag(self):
122 with FeatureFixture(
123 {'disclosure.add-team-person-picker.enabled': 'true'}):
124 widget = self.getWidget(
125 None, vocabulary='ValidPersonOrTeam', required=True,
126 show_create_team=True)
127 login_person(self.factory.makePerson())
128 self.assertTrue(widget.config['show_create_team'])
129
130 def test_show_create_team_link(self):
131 widget = self.getWidget(
132 None, vocabulary='ValidPersonOrTeam', required=True,
133 show_create_team=True)
134 login_person(self.factory.makePerson())
135 self.assertFalse(widget.config['show_create_team'])
118136
=== modified file 'lib/lp/app/javascript/picker/person_picker.js'
--- lib/lp/app/javascript/picker/person_picker.js 2012-06-19 16:21:02 +0000
+++ lib/lp/app/javascript/picker/person_picker.js 2012-06-22 14:19:22 +0000
@@ -16,8 +16,60 @@
16 initializer: function(cfg) {16 initializer: function(cfg) {
17 // If the user isn't logged in, override the show_assign_me value.17 // If the user isn't logged in, override the show_assign_me value.
18 if (!Y.Lang.isValue(LP.links.me)) {18 if (!Y.Lang.isValue(LP.links.me)) {
19 this.set('show_assign_me_button', false); 19 this.set('show_assign_me_button', false);
20 }20 }
21 this.set('new_team_template', this._new_team_template());
22 this.set('new_team_form', this._new_team_form());
23 },
24
25 _new_team_template: function() {
26 return [
27 '<div class="new-team-node">',
28 '<div class="step-on" style="width: 100%;"></div>',
29 '<div class="transparent important-notice-popup">',
30 '{{> new_team_form}}',
31 '<div class="extra-form-buttons">',
32 '<button class="yes_button" type="button"></button>',
33 '<button class="no_button" type="button"></button>',
34 '</div>',
35 '</div>',
36 '</div>'].join('');
37 },
38
39 _new_team_form: function() {
40 // TODO - get the form using ++form++
41 return [
42 "<table id='launchpad-form-widgets' class='form'>",
43 "<tbody><tr><td colspan='2'><div>",
44 "<label for='field.name'>Name:</label><div>",
45 "<input type='text' value='' size='20'",
46 " name='field.name' id='field.name'",
47 " class='lowerCaseText textType'></div>",
48 "<p class='formHelp'>",
49 " A short unique name, beginning with a lower-case letter",
50 " or number, and containing only letters, numbers, dots,",
51 " hyphens, or plus signs.</p>",
52 "</div></td></tr><tr><td colspan='2'><div>",
53 "<label for='field.displayname'>Display Name:</label><div>",
54 "<input type='text' value='' size='20'",
55 " name='field.displayname' id='field.displayname'",
56 " class='textType'></div>",
57 "<p class='formHelp'>",
58 " This team's name as you would like it displayed",
59 " throughout Launchpad.</p>",
60 "</div></td></tr><tr><td colspan='2'><div>",
61 "<label for='field.visibility'>Visibility:</label>",
62 "<div><div><div class='value'>",
63 "<select size='1'",
64 " name='field.visibility' id='field.visibility'>",
65 "<option value='PUBLIC' selected='selected'>Public</option>",
66 "<option value='PRIVATE'>Private</option></select></div>",
67 "</div></div><p class='formHelp'>",
68 " Anyone can see a public team's data. Only team members",
69 " and Launchpad admins can see private team data.",
70 " Private teams cannot become public.</p>",
71 "</div></td></tr></tbody></table>"
72 ].join('');
21 },73 },
2274
23 hide: function() {75 hide: function() {
@@ -31,10 +83,11 @@
31 },83 },
3284
33 _update_button_text: function() {85 _update_button_text: function() {
86 var link_text;
34 if (this.get('selected_value_metadata') === 'team') {87 if (this.get('selected_value_metadata') === 'team') {
35 var link_text = this.get('remove_team_text');88 link_text = this.get('remove_team_text');
36 } else {89 } else {
37 var link_text = this.get('remove_person_text');90 link_text = this.get('remove_person_text');
38 }91 }
39 this.remove_button.set('text', link_text);92 this.remove_button.set('text', link_text);
40 },93 },
@@ -75,32 +128,106 @@
75 });128 });
76 },129 },
77130
131 _cancel_new_team: function(picker) {
132 var node = picker.get('contentBox').one('.new-team-node');
133 picker.hide_extra_content(node);
134 },
135
136 _save_new_team: function(picker) {
137 var node = picker.get('contentBox').one('.new-team-node');
138 var team_name = Y.Node.getDOMNode(node.one('[id=field.name]')).value;
139 var team_display_name =
140 Y.Node.getDOMNode(node.one('[id=field.displayname]')).value;
141 picker.hide_extra_content(node);
142 // TODO - make back end call to save team
143 var value = {
144 "api_uri": "/~" + team_name,
145 "title": team_display_name,
146 "value": team_name,
147 "metadata": "team"};
148 picker.fire('validate', value);
149 },
150
151 show_new_team_form: function () {
152 var partials = {new_team_form: this.get('new_team_form')};
153 var html = Y.lp.mustache.to_html(
154 this.get('new_team_template'), {}, partials);
155 var self = this;
156 var button_callback = function(e, callback_fn) {
157 e.halt();
158 if (Y.Lang.isFunction(callback_fn) ) {
159 callback_fn(self);
160 }
161 };
162 var team_form_node = Y.Node.create(html);
163 team_form_node.one(".yes_button")
164 .set('text', 'Create Team')
165 .on('click', function(e) {
166 button_callback(e, self._save_new_team);
167 });
168
169 team_form_node.one(".no_button")
170 .set('text', 'Cancel')
171 .on('click', function(e) {
172 button_callback(e, self._cancel_new_team);
173 });
174 this.get('contentBox').one('.yui3-widget-bd')
175 .insert(team_form_node, 'before');
176 this.show_extra_content(
177 team_form_node.one(".important-notice-popup"),
178 "Enter new team details");
179 },
180
181 _assign_me_button_html: function() {
182 return [
183 '<a class="yui-picker-assign-me-button bg-image ',
184 'js-action" href="javascript:void(0)" ',
185 'style="background-image: url(/@@/person); ',
186 'padding-right: 1em">',
187 this.get('assign_me_text'),
188 '</a>'].join('');
189 },
190
191 _remove_button_html: function() {
192 return [
193 '<a class="yui-picker-remove-button bg-image js-action" ',
194 'href="javascript:void(0)" ',
195 'style="background-image: url(/@@/remove); ',
196 'padding-right: 1em">',
197 this.get('remove_person_text'),
198 '</a>'].join('');
199 },
200
201 _new_team_button_html: function() {
202 return [
203 '<a class="yui-picker-new-team-button sprite add ',
204 'js-action" href="javascript:void(0)">',
205 'New Team',
206 '</a>'].join('');
207 },
78 renderUI: function() {208 renderUI: function() {
79 Y.lazr.picker.Picker.prototype.renderUI.apply(this, arguments);209 Y.lazr.picker.Picker.prototype.renderUI.apply(this, arguments);
80 var extra_buttons = this.get('extra_buttons');210 var extra_buttons = this.get('extra_buttons');
81 var remove_button, assign_me_button;211 var remove_button, assign_me_button, new_team_button;
82212
83 if (this.get('show_remove_button')) {213 if (this.get('show_remove_button')) {
84 remove_button = Y.Node.create(214 remove_button = Y.Node.create(this._remove_button_html());
85 '<a class="yui-picker-remove-button bg-image" ' +
86 'href="javascript:void(0)" ' +
87 'style="background-image: url(/@@/remove); padding-right: ' +
88 '1em">' + this.get('remove_person_text') + '</a>');
89 remove_button.on('click', this.remove, this);215 remove_button.on('click', this.remove, this);
90 extra_buttons.appendChild(remove_button);216 extra_buttons.appendChild(remove_button);
91 this.remove_button = remove_button;217 this.remove_button = remove_button;
92 }218 }
93219
94 if (this.get('show_assign_me_button')) {220 if (this.get('show_assign_me_button')) {
95 assign_me_button = Y.Node.create(221 assign_me_button = Y.Node.create(this._assign_me_button_html());
96 '<a class="yui-picker-assign-me-button bg-image" ' +
97 'href="javascript:void(0)" ' +
98 'style="background-image: url(/@@/person)">' +
99 this.get('assign_me_text') + '</a>');
100 assign_me_button.on('click', this.assign_me, this);222 assign_me_button.on('click', this.assign_me, this);
101 extra_buttons.appendChild(assign_me_button);223 extra_buttons.appendChild(assign_me_button);
102 this.assign_me_button = assign_me_button;224 this.assign_me_button = assign_me_button;
103 }225 }
226 if (this.get('show_create_team')) {
227 new_team_button = Y.Node.create(this._new_team_button_html());
228 new_team_button.on('click', this.show_new_team_form, this);
229 extra_buttons.appendChild(new_team_button);
230 }
104 this._search_input.insert(231 this._search_input.insert(
105 extra_buttons, this._search_input.get('parentNode'));232 extra_buttons, this._search_input.get('parentNode'));
106 this._show_hide_buttons();233 this._show_hide_buttons();
@@ -112,15 +239,18 @@
112 ATTRS: {239 ATTRS: {
113 extra_buttons: {240 extra_buttons: {
114 valueFn: function () {241 valueFn: function () {
115 return Y.Node.create('<div class="extra-form-buttons"/>')242 return Y.Node.create('<div class="extra-form-buttons"/>');
116 } 243 }
117 },244 },
118 show_assign_me_button: { value: true },245 show_assign_me_button: { value: true },
119 show_remove_button: {value: true },246 show_remove_button: {value: true },
120 assign_me_text: {value: 'Pick me'},247 assign_me_text: {value: 'Pick me'},
121 remove_person_text: {value: 'Remove person'},248 remove_person_text: {value: 'Remove person'},
122 remove_team_text: {value: 'Remove team'},249 remove_team_text: {value: 'Remove team'},
123 min_search_chars: {value: 2}250 min_search_chars: {value: 2},
251 show_create_team: {value: false},
252 new_team_template: {value: null},
253 new_team_form: {value: null}
124 }254 }
125});255});
126}, "0.1", {"requires": ["base", "node", "lazr.picker"]});256}, "0.1", {"requires": ["base", "node", "lazr.picker", "lp.mustache"]});
127257
=== modified file 'lib/lp/app/javascript/picker/picker.js'
--- lib/lp/app/javascript/picker/picker.js 2012-06-20 16:38:28 +0000
+++ lib/lp/app/javascript/picker/picker.js 2012-06-22 14:19:22 +0000
@@ -20,7 +20,7 @@
20 */20 */
21ns.Picker = Y.Base.create('picker', Y.lazr.PrettyOverlay, [], {21ns.Picker = Y.Base.create('picker', Y.lazr.PrettyOverlay, [], {
2222
23 /** 23 /**
24 * The search input node.24 * The search input node.
25 *25 *
26 * @property _search_button26 * @property _search_button
@@ -823,6 +823,61 @@
823 },823 },
824824
825 /*825 /*
826 * Insert the extra content into the form and animate its appearance.
827 */
828 show_extra_content: function(extra_content, header) {
829 if (Y.Lang.isValue(header)) {
830 this.set('picker_header', this.get('headerContent'));
831 this.set(
832 'headerContent',
833 Y.Node.create("<h2></h2>").set('text', header));
834 }
835 this.get('contentBox').one('.yui3-widget-bd').hide();
836 this.get('contentBox').all('.steps').hide();
837 var duration = 0;
838 if (this.get('use_animation')) {
839 duration = 0.9;
840 }
841 var fade_in = new Y.Anim({
842 node: extra_content,
843 to: {opacity: 1},
844 duration: duration
845 });
846 fade_in.run();
847 },
848
849 hide_extra_content: function(extra_content_node) {
850 var saved_header = this.get('picker_header');
851 if (Y.Lang.isValue(saved_header)) {
852 this.set('headerContent', saved_header);
853 this.set('picker_header', null);
854 }
855 this.get('contentBox').all('.steps').show();
856 var content_node = this.get('contentBox').one('.yui3-widget-bd');
857 if (extra_content_node !== null) {
858 extra_content_node.get('parentNode')
859 .removeChild(extra_content_node);
860 content_node.addClass('transparent');
861 content_node.setStyle('opacity', 0);
862 content_node.show();
863 var duration = 0;
864 if (this.get('use_animation')) {
865 duration = 0.6;
866 }
867 var content_fade_in = new Y.Anim({
868 node: content_node,
869 to: {opacity: 1},
870 duration: duration
871 });
872 content_fade_in.run();
873 } else {
874 content_node.removeClass('transparent');
875 content_node.setStyle('opacity', 1);
876 content_node.show();
877 }
878 },
879
880 /*
826 * Clear all elements of the picker, resetting it to its original state.881 * Clear all elements of the picker, resetting it to its original state.
827 *882 *
828 * @method _clear883 * @method _clear
@@ -970,8 +1025,8 @@
970 clear_on_cancel: { value: false },1025 clear_on_cancel: { value: false },
9711026
972 /**1027 /**
973 * A CSS selector for the DOM element that will activate (show) the picker1028 * A CSS selector for the DOM element that will activate (show) the
974 * once clicked.1029 * picker once clicked.
975 *1030 *
976 * @attribute picker_activator1031 * @attribute picker_activator
977 * @type String1032 * @type String
@@ -979,8 +1034,8 @@
979 picker_activator: {},1034 picker_activator: {},
9801035
981 /**1036 /**
982 * An extra CSS class to be added to the picker_activator, generally used1037 * An extra CSS class to be added to the picker_activator, generally
983 * to distinguish regular links from js-triggering ones.1038 * used to distinguish regular links from js-triggering ones.
984 *1039 *
985 * @attribute picker_activator_css_class1040 * @attribute picker_activator_css_class
986 * @type String1041 * @type String
@@ -998,8 +1053,8 @@
998 min_search_chars: { value: 3 },1053 min_search_chars: { value: 3 },
9991054
1000 /**1055 /**
1001 * The current search string, which is needed when clicking on a different1056 * The current search string, which is needed when clicking on a
1002 * batch if the search input has been modified.1057 * different batch if the search input has been modified.
1003 *1058 *
1004 * @attribute current_search_string1059 * @attribute current_search_string
1005 * @type String1060 * @type String
@@ -1015,8 +1070,8 @@
1015 current_filter_value: {value: null},1070 current_filter_value: {value: null},
10161071
1017 /**1072 /**
1018 * A list of attribute name values used to construct the filtering options1073 * A list of attribute name values used to construct the filtering
1019 * for this picker..1074 * options for this picker.
1020 *1075 *
1021 * @attribute filter_options1076 * @attribute filter_options
1022 * @type Object1077 * @type Object
@@ -1079,7 +1134,7 @@
1079 * this value automatically updates the display.1134 * this value automatically updates the display.
1080 *1135 *
1081 * This an array of object containing the two keys, name (used as1136 * This an array of object containing the two keys, name (used as
1082 * the batch label) and value (used as additional details to 'search' 1137 * the batch label) and value (used as additional details to 'search'
1083 * event).1138 * event).
1084 *1139 *
1085 * @attribute batches1140 * @attribute batches
@@ -1091,9 +1146,9 @@
1091 * For simplified batch creation, you can set this to the number of1146 * For simplified batch creation, you can set this to the number of
1092 * batches in the search results. In this case, the batch labels1147 * batches in the search results. In this case, the batch labels
1093 * and values are automatically calculated. The batch name (used as the1148 * and values are automatically calculated. The batch name (used as the
1094 * batch label) will be the batch number starting from 1. The batch value1149 * batch label) will be the batch number starting from 1. The batch
1095 * (used as additional details to the 'search' event) will be the batch1150 * value (used as additional details to the 'search' event) will be the
1096 * number, starting from zero.1151 * batch number, starting from zero.
1097 *1152 *
1098 * If 'batches' is set (see above), batch_count is ignored.1153 * If 'batches' is set (see above), batch_count is ignored.
1099 *1154 *
@@ -1139,8 +1194,8 @@
1139 error: { value: null },1194 error: { value: null },
11401195
1141 /**1196 /**
1142 * The message to display when the search returned no results. This string1197 * The message to display when the search returned no results.
1143 * can contain a 'query' placeholder1198 * This string can contain a 'query' placeholder
1144 *1199 *
1145 * @attribute no_results_search_message1200 * @attribute no_results_search_message
1146 * @type String1201 * @type String
@@ -1148,6 +1203,17 @@
1148 */1203 */
1149 no_results_search_message: {1204 no_results_search_message: {
1150 value: 'No items matched "{query}".'1205 value: 'No items matched "{query}".'
1206 },
1207
1208 /**
1209 * Whether to use animations (fade in/out) for content rendering.
1210 *
1211 * @attribute use_animation
1212 * @type Boolean
1213 * @default true
1214 */
1215 use_animation: {
1216 value: true
1151 }1217 }
1152 }1218 }
1153});1219});
@@ -1186,7 +1252,7 @@
1186 this.get('host').setAttrs({1252 this.get('host').setAttrs({
1187 selected_value_metadata: result.metadata,1253 selected_value_metadata: result.metadata,
1188 selected_value: result.value1254 selected_value: result.value
1189 })1255 });
1190 input.set("value", result.value || '');1256 input.set("value", result.value || '');
1191 // If the search input isn't blurred before it is focused,1257 // If the search input isn't blurred before it is focused,
1192 // then the I-beam disappears.1258 // then the I-beam disappears.
11931259
=== modified file 'lib/lp/app/javascript/picker/picker_patcher.js'
--- lib/lp/app/javascript/picker/picker_patcher.js 2012-06-18 21:52:25 +0000
+++ lib/lp/app/javascript/picker/picker_patcher.js 2012-06-22 14:19:22 +0000
@@ -24,9 +24,21 @@
24 if (show_widget_node.hasClass('js-action')) {24 if (show_widget_node.hasClass('js-action')) {
25 return;25 return;
26 }26 }
27 show_widget_node.set('innerHTML', 'Choose&hellip;');27 var picker_span = show_widget_node.get('parentNode');
28 show_widget_node.addClass('js-action');28 if (config.enhanced_picker) {
29 show_widget_node.get('parentNode').removeClass('unseen');29 var new_node = Y.Node.create('<span>(<a href="#"></a>)</span>');
30 show_widget_node = new_node.one('a');
31 show_widget_node
32 .set('id', show_widget_id)
33 .addClass('js-action')
34 .set('text', 'Choose\u2026');
35 picker_span.empty();
36 picker_span.appendChild(new_node);
37 } else {
38 show_widget_node.set('text', 'Choose\u2026');
39 show_widget_node.addClass('js-action');
40 }
41 picker_span.removeClass('unseen');
30 show_widget_node.on('click', function (e) {42 show_widget_node.on('click', function (e) {
31 if (picker === null) {43 if (picker === null) {
32 picker = namespace.create(44 picker = namespace.create(
@@ -261,7 +273,7 @@
261273
262 node.one(".validation-content-placeholder").replace(content);274 node.one(".validation-content-placeholder").replace(content);
263 picker.get('contentBox').one('.yui3-widget-bd').insert(node, 'before');275 picker.get('contentBox').one('.yui3-widget-bd').insert(node, 'before');
264 animate_validation_content(picker, node.one(".important-notice-popup"));276 picker.show_extra_content(node.one(".important-notice-popup"));
265};277};
266278
267/*279/*
@@ -298,42 +310,11 @@
298};310};
299311
300/*312/*
301 * Insert the validation content into the form and animate its appearance.
302 */
303function animate_validation_content(picker, validation_content) {
304 picker.get('contentBox').one('.yui3-widget-bd').hide();
305 picker.get('contentBox').all('.steps').hide();
306 var validation_fade_in = new Y.Anim({
307 node: validation_content,
308 to: {opacity: 1},
309 duration: 0.9
310 });
311 validation_fade_in.run();
312}
313
314/*
315 * Restore a picker to its functional state after a validation operation.313 * Restore a picker to its functional state after a validation operation.
316 */314 */
317function reset_form(picker) {315function reset_form(picker) {
318 picker.get('contentBox').all('.steps').show();
319 var validation_node = picker.get('contentBox').one('.validation-node');316 var validation_node = picker.get('contentBox').one('.validation-node');
320 var content_node = picker.get('contentBox').one('.yui3-widget-bd');317 picker.hide_extra_content(validation_node);
321 if (validation_node !== null) {
322 validation_node.get('parentNode').removeChild(validation_node);
323 content_node.addClass('transparent');
324 content_node.setStyle('opacity', 0);
325 content_node.show();
326 var content_fade_in = new Y.Anim({
327 node: content_node,
328 to: {opacity: 1},
329 duration: 0.6
330 });
331 content_fade_in.run();
332 } else {
333 content_node.removeClass('transparent');
334 content_node.setStyle('opacity', 1);
335 content_node.show();
336 }
337}318}
338319
339320
340321
=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
--- lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-18 21:52:51 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-22 14:19:22 +0000
@@ -36,6 +36,8 @@
36 <script type="text/javascript"36 <script type="text/javascript"
37 src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>37 src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
38 <script type="text/javascript"38 <script type="text/javascript"
39 src="../../../../../../build/js/lp/app/mustache.js"></script>
40 <script type="text/javascript"
39 src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>41 src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
40 <script type="text/javascript"42 <script type="text/javascript"
41 src="../../../../../../build/js/lp/app/effects/effects.js"></script>43 src="../../../../../../build/js/lp/app/effects/effects.js"></script>
4244
=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.js'
--- lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-20 16:38:28 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-22 14:19:22 +0000
@@ -3,7 +3,7 @@
33
4YUI().use('test', 'console', 'plugin',4YUI().use('test', 'console', 'plugin',
5 'lazr.picker', 'lazr.person-picker', 'lp.app.picker',5 'lazr.picker', 'lazr.person-picker', 'lp.app.picker',
6 'node-event-simulate', function(Y) {6 'lp.app.mustache', 'node-event-simulate', function(Y) {
77
8 var Assert = Y.Assert;8 var Assert = Y.Assert;
99
@@ -92,11 +92,12 @@
92 },92 },
9393
94 _picker_params: function(94 _picker_params: function(
95 show_assign_me_button, show_remove_button,95 show_assign_me_button, show_remove_button, show_create_team,
96 selected_value, selected_value_metadata) {96 selected_value, selected_value_metadata) {
97 return {97 return {
98 "show_assign_me_button": show_assign_me_button,98 "show_assign_me_button": show_assign_me_button,
99 "show_remove_button": show_remove_button,99 "show_remove_button": show_remove_button,
100 "show_create_team": show_create_team,
100 "selected_value": selected_value,101 "selected_value": selected_value,
101 "selected_value_metadata": selected_value_metadata102 "selected_value_metadata": selected_value_metadata
102 };103 };
@@ -185,10 +186,8 @@
185186
186 test_picker_remove_person_button_text: function() {187 test_picker_remove_person_button_text: function() {
187 // The remove button text is correct.188 // The remove button text is correct.
188 this.create_picker(this._picker_params(true,189 this.create_picker(this._picker_params(
189 true,190 true, true, false, "fred", "person"));
190 "fred",
191 "person"));
192 this.picker.render();191 this.picker.render();
193 var remove_button = Y.one('.yui-picker-remove-button');192 var remove_button = Y.one('.yui-picker-remove-button');
194 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));193 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
@@ -196,7 +195,8 @@
196195
197 test_picker_remove_team_button_text: function() {196 test_picker_remove_team_button_text: function() {
198 // The remove button text is correct.197 // The remove button text is correct.
199 this.create_picker(this._picker_params(true, true, "cats", "team"));198 this.create_picker(this._picker_params(
199 true, true, false, "cats", "team"));
200 this.picker.render();200 this.picker.render();
201 var remove_button = Y.one('.yui-picker-remove-button');201 var remove_button = Y.one('.yui-picker-remove-button');
202 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));202 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
@@ -220,7 +220,8 @@
220 test_picker_no_assign_me_button_if_value_is_me: function() {220 test_picker_no_assign_me_button_if_value_is_me: function() {
221 // The assign me button is not shown if the picker is created for a221 // The assign me button is not shown if the picker is created for a
222 // field where the value is "me".222 // field where the value is "me".
223 this.create_picker(this._picker_params(true, true, "me"), this.ME);223 this.create_picker(this._picker_params(
224 true, true, false, "me"), this.ME);
224 this.picker.render();225 this.picker.render();
225 this._check_assign_me_button_state(false);226 this._check_assign_me_button_state(false);
226 },227 },
@@ -236,7 +237,8 @@
236 test_picker_has_remove_button_if_value: function() {237 test_picker_has_remove_button_if_value: function() {
237 // The remove button is shown if the picker is created for a field238 // The remove button is shown if the picker is created for a field
238 // which has a value.239 // which has a value.
239 this.create_picker(this._picker_params(true, true, "me"), this.ME);240 this.create_picker(this._picker_params(
241 true, true, false, "me"), this.ME);
240 this.picker.render();242 this.picker.render();
241 this._check_remove_button_state(true);243 this._check_remove_button_state(true);
242 },244 },
@@ -244,9 +246,151 @@
244 test_picker_no_remove_button_unless_configured: function() {246 test_picker_no_remove_button_unless_configured: function() {
245 // The remove button is only rendered if show_remove_button247 // The remove button is only rendered if show_remove_button
246 // setting is true.248 // setting is true.
247 this.create_picker(this._picker_params(true, false, "me"), this.ME);249 this.create_picker(this._picker_params(
250 true, false, false, "me"), this.ME);
248 this.picker.render();251 this.picker.render();
249 Assert.isNull(Y.one('.yui-picker-remove-button'));252 Assert.isNull(Y.one('.yui-picker-remove-button'));
253 },
254
255 test_picker_assign_me_button_hide_on_save: function() {
256 // The assign me button is shown initially but hidden if the picker
257 // saves a value equal to 'me'.
258 this.create_picker(this._picker_params(true, true));
259 this._check_assign_me_button_state(true);
260 this.picker.set('results', this.vocabulary);
261 this.picker.render();
262 simulate(
263 this.picker.get('boundingBox').one('.yui3-picker-results'),
264 'li:nth-child(1)', 'click');
265 this._check_assign_me_button_state(false);
266 },
267
268 test_picker_remove_button_clicked: function() {
269 // The remove button is hidden once a picker value has been removed.
270 // And the assign me button is shown.
271 this.create_picker(this._picker_params(
272 true, true, false, "me"), this.ME);
273 this.picker.render();
274 this._check_assign_me_button_state(false);
275 var remove = Y.one('.yui-picker-remove-button');
276 remove.simulate('click');
277 this._check_remove_button_state(false);
278 this._check_assign_me_button_state(true);
279 },
280
281 test_picker_assign_me_button_clicked: function() {
282 // The assign me button is hidden once it is clicked.
283 // And the remove button is shown.
284 this.create_picker(this._picker_params(true, true));
285 this.picker.render();
286 var assign_me = Y.one('.yui-picker-assign-me-button');
287 assign_me.simulate('click');
288 this._check_remove_button_state(true);
289 this._check_assign_me_button_state(false);
290 },
291
292 test_picker_assign_me_updates_remove_text: function() {
293 // When Assign me is used, the Remove button text is updated from
294 // the team removal text to the person removal text.
295 this.create_picker(this._picker_params(
296 true, true, false, "cats", "team"));
297 this.picker.render();
298 var remove_button = Y.one('.yui-picker-remove-button');
299 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
300 var assign_me = Y.one('.yui-picker-assign-me-button');
301 assign_me.simulate('click');
302 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
303 },
304
305 test_picker_save_updates_remove_text: function() {
306 // When save is called, the Remove button text is updated
307 // according to the newly saved value.
308 this.create_picker(this._picker_params(
309 true, true, false, "me"), this.ME);
310 var remove_button = Y.one('.yui-picker-remove-button');
311 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
312 this.picker.set('results', this.vocabulary);
313 this.picker.render();
314 simulate(
315 this.picker.get('boundingBox').one('.yui3-picker-results'),
316 'li:nth-child(2)', 'click');
317 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
318 },
319
320 test_picker_no_team_button_unless_configured: function() {
321 // The new team button is only rendered if show_create_team
322 // setting is true.
323 this.create_picker(this._picker_params(true, false, false));
324 this.picker.render();
325 Assert.isNull(Y.one('.yui-picker-new-team-button'));
326 },
327
328 test_picker_new_team_button_click_shows_form: function() {
329 // Clicking the new team button displays the new team form.
330 this.create_picker(this._picker_params(true, true, true));
331 this.picker.render();
332 var new_team = this.picker.get('boundingBox')
333 .one('.yui-picker-new-team-button');
334 new_team.simulate('click');
335 Y.Assert.areEqual(
336 'Enter new team details',
337 this.picker.get('headerContent').get('text'));
338 Y.Assert.isNotNull(
339 this.picker.get('contentBox').one('[id=field.name]'));
340 Y.Assert.areEqual('none',
341 this.picker.get('contentBox').one('.yui3-widget-bd')
342 .getStyle('display'));
343 },
344
345 test_picker_new_team_cancel: function() {
346 // Clicking the cancel button on the new team form reverts back to
347 // the normal picker.
348 this.create_picker(this._picker_params(true, true, true));
349 this.picker.render();
350 var new_team = this.picker.get('boundingBox')
351 .one('.yui-picker-new-team-button');
352 new_team.simulate('click');
353 Y.Assert.areEqual(
354 'Enter new team details',
355 this.picker.get('headerContent').get('text'));
356 var form_buttons = this.picker.get('contentBox')
357 .one('.extra-form-buttons');
358 simulate(
359 form_buttons, 'button:nth-child(2)', 'click');
360 Y.Assert.areEqual(
361 'Pick Someone',
362 this.picker.get('headerContent').get('text'));
363 Y.Assert.isNull(
364 this.picker.get('contentBox').one('[id=field.name]'));
365 Y.Assert.isNotNull(
366 this.picker.get('contentBox').one('.yui3-picker-search'));
367 },
368
369 test_picker_new_team_save: function() {
370 // Clicking the save button on the new team form fires a 'save'
371 // event with the expected data.
372 this.create_picker(this._picker_params(true, true, true));
373 this.picker.render();
374
375 var result_published = false;
376 this.picker.subscribe('save', function(e) {
377 var saved_value =
378 e.details[Y.lazr.picker.Picker.SAVE_RESULT];
379 Y.Assert.areEqual('/~fred', saved_value.api_uri);
380 Y.Assert.areEqual('fred', saved_value.value);
381 result_published = true;
382 });
383
384 var picker_content = this.picker.get('boundingBox');
385 var new_team =
386 picker_content.one('.yui-picker-new-team-button');
387 new_team.simulate('click');
388 var team_name = picker_content.one('[id=field.name]');
389 Y.Node.getDOMNode(team_name).value = 'fred';
390 var form_buttons = picker_content.one('.extra-form-buttons');
391 simulate(
392 form_buttons, 'button:nth-child(1)', 'click');
393 Y.Assert.isTrue(result_published);
250 }394 }
251 };395 };
252396
@@ -266,6 +410,7 @@
266 }410 }
267411
268 var config = {412 var config = {
413 "use_animation": false,
269 "picker_type": "person",414 "picker_type": "person",
270 "step_title": "Choose someone",415 "step_title": "Choose someone",
271 "header": "Pick Someone",416 "header": "Pick Someone",
@@ -275,6 +420,7 @@
275 "show_remove_button": params.show_remove_button,420 "show_remove_button": params.show_remove_button,
276 "selected_value": params.selected_value,421 "selected_value": params.selected_value,
277 "selected_value_metadata": params.selected_value_metadata,422 "selected_value_metadata": params.selected_value_metadata,
423 "show_create_team": params.show_create_team,
278 "assign_me_text": "Assign Moi",424 "assign_me_text": "Assign Moi",
279 "remove_person_text": "Remove someone",425 "remove_person_text": "Remove someone",
280 "remove_team_text": "Remove some team"426 "remove_team_text": "Remove some team"
@@ -285,69 +431,6 @@
285 "test_link",431 "test_link",
286 "picker_id",432 "picker_id",
287 config);433 config);
288 console.log(this.picker);
289 },
290
291 test_picker_assign_me_button_hide_on_save: function() {
292 // The assign me button is shown initially but hidden if the picker
293 // saves a value equal to 'me'.
294 this.create_picker(this._picker_params(true, true));
295 this._check_assign_me_button_state(true);
296 this.picker.set('results', this.vocabulary);
297 this.picker.render();
298 simulate(
299 this.picker.get('boundingBox').one('.yui3-picker-results'),
300 'li:nth-child(1)', 'click');
301 this._check_assign_me_button_state(false);
302 },
303
304 test_picker_remove_button_clicked: function() {
305 // The remove button is hidden once a picker value has been removed.
306 // And the assign me button is shown.
307 this.create_picker(this._picker_params(true, true, "me"), this.ME);
308 this.picker.render();
309 this._check_assign_me_button_state(false);
310 var remove = Y.one('.yui-picker-remove-button');
311 remove.simulate('click');
312 this._check_remove_button_state(false);
313 this._check_assign_me_button_state(true);
314 },
315
316 test_picker_assign_me_button_clicked: function() {
317 // The assign me button is hidden once it is clicked.
318 // And the remove button is shown.
319 this.create_picker(this._picker_params(true, true));
320 this.picker.render();
321 var assign_me = Y.one('.yui-picker-assign-me-button');
322 assign_me.simulate('click');
323 this._check_remove_button_state(true);
324 this._check_assign_me_button_state(false);
325 },
326
327 test_picker_assign_me_updates_remove_text: function() {
328 // When Assign me is used, the Remove button text is updated from
329 // the team removal text to the person removal text.
330 this.create_picker(this._picker_params(true, true, "cats", "team"));
331 this.picker.render();
332 var remove_button = Y.one('.yui-picker-remove-button');
333 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
334 var assign_me = Y.one('.yui-picker-assign-me-button');
335 assign_me.simulate('click');
336 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
337 },
338
339 test_picker_save_updates_remove_text: function() {
340 // When save is called, the Remove button text is updated
341 // according to the newly saved value.
342 this.create_picker(this._picker_params(true, true, "me"), this.ME);
343 var remove_button = Y.one('.yui-picker-remove-button');
344 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
345 this.picker.set('results', this.vocabulary);
346 this.picker.render();
347 simulate(
348 this.picker.get('boundingBox').one('.yui3-picker-results'),
349 'li:nth-child(2)', 'click');
350 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
351 }434 }
352 };435 };
353436
@@ -377,9 +460,11 @@
377 }460 }
378 var config = {461 var config = {
379 "picker_type": "person",462 "picker_type": "person",
463 "header": "Pick Someone",
380 "associated_field_id": associated_field_id,464 "associated_field_id": associated_field_id,
381 "show_assign_me_button": params.show_assign_me_button,465 "show_assign_me_button": params.show_assign_me_button,
382 "show_remove_button": params.show_remove_button,466 "show_remove_button": params.show_remove_button,
467 "show_create_team": params.show_create_team,
383 "selected_value": params.selected_value,468 "selected_value": params.selected_value,
384 "selected_value_metadata": params.selected_value_metadata,469 "selected_value_metadata": params.selected_value_metadata,
385 "assign_me_text": "Assign Moi",470 "assign_me_text": "Assign Moi",
@@ -388,68 +473,6 @@
388 };473 };
389 this.picker = Y.lp.app.picker.create(474 this.picker = Y.lp.app.picker.create(
390 this.vocabulary, config, associated_field_id);475 this.vocabulary, config, associated_field_id);
391 },
392
393 test_picker_assign_me_button_hide_on_save: function() {
394 // The assign me button is shown initially but hidden if the picker
395 // saves a value equal to 'me'.
396 this.create_picker(this._picker_params(true, true));
397 this._check_assign_me_button_state(true);
398 this.picker.set('results', this.vocabulary);
399 this.picker.render();
400 simulate(
401 this.picker.get('boundingBox').one('.yui3-picker-results'),
402 'li:nth-child(1)', 'click');
403 this._check_assign_me_button_state(false);
404 },
405
406 test_picker_remove_button_clicked: function() {
407 // The remove button is hidden once a picker value has been removed.
408 // And the assign me button is shown.
409 this.create_picker(this._picker_params(true, true, "me"), this.ME);
410 this.picker.render();
411 this._check_assign_me_button_state(false);
412 var remove = Y.one('.yui-picker-remove-button');
413 remove.simulate('click');
414 this._check_remove_button_state(false);
415 this._check_assign_me_button_state(true);
416 },
417
418 test_picker_assign_me_button_clicked: function() {
419 // The assign me button is hidden once it is clicked.
420 // And the remove button is shown.
421 this.create_picker(this._picker_params(true, true));
422 this.picker.render();
423 var assign_me = Y.one('.yui-picker-assign-me-button');
424 assign_me.simulate('click');
425 this._check_remove_button_state(true);
426 this._check_assign_me_button_state(false);
427 },
428
429 test_picker_assign_me_updates_remove_text: function() {
430 // When Assign me is used, the Remove button text is updated from
431 // the team removal text to the person removal text.
432 this.create_picker(this._picker_params(true, true, "cats", "team"));
433 this.picker.render();
434 var remove_button = Y.one('.yui-picker-remove-button');
435 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
436 var assign_me = Y.one('.yui-picker-assign-me-button');
437 assign_me.simulate('click');
438 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
439 },
440
441 test_picker_save_updates_remove_text: function() {
442 // When save is called, the Remove button text is updated
443 // according to the newly saved value.
444 this.create_picker(this._picker_params(true, true, "me"), this.ME);
445 var remove_button = Y.one('.yui-picker-remove-button');
446 Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
447 this.picker.set('results', this.vocabulary);
448 this.picker.render();
449 simulate(
450 this.picker.get('boundingBox').one('.yui3-picker-results'),
451 'li:nth-child(2)', 'click');
452 Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
453 }476 }
454 };477 };
455478
456479
=== modified file 'lib/lp/app/widgets/popup.py'
--- lib/lp/app/widgets/popup.py 2012-02-22 05:22:13 +0000
+++ lib/lp/app/widgets/popup.py 2012-06-22 14:19:22 +0000
@@ -23,6 +23,7 @@
23 get_person_picker_entry_metadata,23 get_person_picker_entry_metadata,
24 vocabulary_filters,24 vocabulary_filters,
25 )25 )
26from lp.services.features import getFeatureFlag
26from lp.services.propertycache import cachedproperty27from lp.services.propertycache import cachedproperty
27from lp.services.webapp import canonical_url28from lp.services.webapp import canonical_url
2829
@@ -41,6 +42,7 @@
41 assign_me_text = 'Pick me'42 assign_me_text = 'Pick me'
42 remove_person_text = 'Remove person'43 remove_person_text = 'Remove person'
43 remove_team_text = 'Remove team'44 remove_team_text = 'Remove team'
45 show_create_team_link = False
4446
45 popup_name = 'popup-vocabulary-picker'47 popup_name = 'popup-vocabulary-picker'
4648
@@ -56,6 +58,12 @@
56 # Defaults to self.vocabulary.displayname.58 # Defaults to self.vocabulary.displayname.
57 header = None59 header = None
5860
61 @property
62 def enhanced_picker(self):
63 flag = getFeatureFlag(
64 "disclosure.add-team-person-picker.enabled")
65 return flag and self.show_create_team_link
66
59 @cachedproperty67 @cachedproperty
60 def matches(self):68 def matches(self):
61 """Return a list of matches (as ITokenizedTerm) to whatever the69 """Return a list of matches (as ITokenizedTerm) to whatever the
@@ -143,7 +151,9 @@
143 vocabulary_name=self.vocabulary_name,151 vocabulary_name=self.vocabulary_name,
144 vocabulary_filters=self.vocabulary_filters,152 vocabulary_filters=self.vocabulary_filters,
145 input_element=self.input_id,153 input_element=self.input_id,
146 show_widget_id=self.show_widget_id)154 show_widget_id=self.show_widget_id,
155 enhanced_picker=self.enhanced_picker,
156 show_create_team=self.enhanced_picker)
147157
148 @property158 @property
149 def json_config(self):159 def json_config(self):
@@ -206,8 +216,12 @@
206 else:216 else:
207 css = ''217 css = ''
208 return ('<span class="%s">(<a id="%s" href="%s">'218 return ('<span class="%s">(<a id="%s" href="%s">'
209 'Find&hellip;</a>)</span>') % (219 'Find&hellip;</a>)%s</span>') % (
210 css, self.show_widget_id, self.nonajax_uri or '#')220 css, self.show_widget_id, self.nonajax_uri or '#',
221 self.extraChooseLink() or '')
222
223 def extraChooseLink(self):
224 return None
211225
212 @property226 @property
213 def nonajax_uri(self):227 def nonajax_uri(self):
@@ -220,7 +234,6 @@
220234
221class PersonPickerWidget(VocabularyPickerWidget):235class PersonPickerWidget(VocabularyPickerWidget):
222236
223 include_create_team_link = False
224 show_assign_me_button = True237 show_assign_me_button = True
225 show_remove_button = False238 show_remove_button = False
226 picker_type = 'person'239 picker_type = 'person'
@@ -230,12 +243,11 @@
230 val = self._getFormValue()243 val = self._getFormValue()
231 return get_person_picker_entry_metadata(val)244 return get_person_picker_entry_metadata(val)
232245
233 def chooseLink(self):246 def extraChooseLink(self):
234 link = super(PersonPickerWidget, self).chooseLink()247 if self.show_create_team_link:
235 if self.include_create_team_link:248 return ('or (<a href="/people/+newteam">'
236 link += ('or (<a href="/people/+newteam">'
237 'Create a new team&hellip;</a>)')249 'Create a new team&hellip;</a>)')
238 return link250 return None
239251
240 @property252 @property
241 def nonajax_uri(self):253 def nonajax_uri(self):
@@ -251,10 +263,8 @@
251 >Register an external bug tracker&hellip;</a>)263 >Register an external bug tracker&hellip;</a>)
252 """264 """
253265
254 def chooseLink(self):266 def extraChooseLink(self):
255 link = super(BugTrackerPickerWidget, self).chooseLink()267 return self.link_template
256 link += self.link_template
257 return link
258268
259 @property269 @property
260 def nonajax_uri(self):270 def nonajax_uri(self):
261271
=== modified file 'lib/lp/app/widgets/tests/test_popup.py'
--- lib/lp/app/widgets/tests/test_popup.py 2012-02-03 06:27:17 +0000
+++ lib/lp/app/widgets/tests/test_popup.py 2012-06-22 14:19:22 +0000
@@ -1,5 +1,6 @@
1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3from lp.services.features.testing import FeatureFixture
34
4__metaclass__ = type5__metaclass__ = type
56
@@ -168,6 +169,26 @@
168 # But not the remove button.169 # But not the remove button.
169 self.assertFalse(person_picker_widget.config['show_remove_button'])170 self.assertFalse(person_picker_widget.config['show_remove_button'])
170171
172 def test_create_team_link(self):
173 # The person picker widget shows a create team link if the feature flag
174 # is on.
175 field = ITest['test_valid.item']
176 bound_field = field.bind(self.context)
177
178 with FeatureFixture(
179 {'disclosure.add-team-person-picker.enabled': 'true'}):
180 picker_widget = PersonPickerWidget(
181 bound_field, self.vocabulary, self.request)
182 picker_widget.show_create_team_link = True
183 self.assertTrue(picker_widget.config['show_create_team'])
184 self.assertTrue(picker_widget.config['enhanced_picker'])
185
186 picker_widget = PersonPickerWidget(
187 bound_field, self.vocabulary, self.request)
188 picker_widget.show_create_team_link = True
189 self.assertFalse(picker_widget.config['show_create_team'])
190 self.assertFalse(picker_widget.config['enhanced_picker'])
191
171 def test_widget_personvalue_meta(self):192 def test_widget_personvalue_meta(self):
172 # The person picker has the correct meta value for a person value.193 # The person picker has the correct meta value for a person value.
173 person = self.factory.makePerson()194 person = self.factory.makePerson()
174195
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-06-04 11:41:47 +0000
+++ lib/lp/registry/browser/product.py 2012-06-22 14:19:22 +0000
@@ -938,7 +938,7 @@
938 self.context, IProduct['owner'],938 self.context, IProduct['owner'],
939 format_link(self.context.owner),939 format_link(self.context.owner),
940 header='Change maintainer', edit_view='+edit-people',940 header='Change maintainer', edit_view='+edit-people',
941 step_title='Select a new maintainer')941 step_title='Select a new maintainer', show_create_team=True)
942942
943 @property943 @property
944 def driver_widget(self):944 def driver_widget(self):
@@ -946,7 +946,7 @@
946 self.context, IProduct['driver'],946 self.context, IProduct['driver'],
947 format_link(self.context.driver, empty_value="Not yet selected"),947 format_link(self.context.driver, empty_value="Not yet selected"),
948 header='Change driver', edit_view='+edit-people',948 header='Change driver', edit_view='+edit-people',
949 step_title='Select a new driver',949 step_title='Select a new driver', show_create_team=True,
950 null_display_value="Not yet selected",950 null_display_value="Not yet selected",
951 help_link="/+help-registry/driver.html")951 help_link="/+help-registry/driver.html")
952952
@@ -2269,11 +2269,11 @@
2269 initial_values = {'transfer_to_registry': False}2269 initial_values = {'transfer_to_registry': False}
22702270
2271 custom_widget('owner', PersonPickerWidget, header="Select the maintainer",2271 custom_widget('owner', PersonPickerWidget, header="Select the maintainer",
2272 include_create_team_link=True)2272 show_create_team_link=True)
2273 custom_widget('transfer_to_registry', CheckBoxWidget,2273 custom_widget('transfer_to_registry', CheckBoxWidget,
2274 widget_class='field subordinate')2274 widget_class='field subordinate')
2275 custom_widget('driver', PersonPickerWidget, header="Select the driver",2275 custom_widget('driver', PersonPickerWidget, header="Select the driver",
2276 include_create_team_link=True)2276 show_create_team_link=True)
22772277
2278 @property2278 @property
2279 def page_title(self):2279 def page_title(self):
22802280
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2012-06-08 02:16:58 +0000
+++ lib/lp/services/features/flags.py 2012-06-22 14:19:22 +0000
@@ -223,6 +223,12 @@
223 '',223 '',
224 '',224 '',
225 ''),225 ''),
226 ('disclosure.add-team-person-picker.enabled',
227 'boolean',
228 'Allows users to add a new team directly from the person picker.',
229 '',
230 '',
231 ''),
226 ('bugs.autoconfirm.enabled_distribution_names',232 ('bugs.autoconfirm.enabled_distribution_names',
227 'space delimited',233 'space delimited',
228 ('Enables auto-confirming bugtasks for distributions (and their '234 ('Enables auto-confirming bugtasks for distributions (and their '