Merge lp:~wallyworld/launchpad/new-team-picker into lp:launchpad
- new-team-picker
- Merge into devel
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 | ||||
Related bugs: |
|
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_
The feature is controlled by a feature flag: "disclosure.
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://
== Tests ==
Add tests for the back end widgets: test_popup and test_inlineedit
- test_show_
- test_show_
- test_create_
Move duplicated tests in test_personpicker to a common base class.
Add new yui tests for the create team functionality:
- test_picker_
- test_picker_
- test_picker_
- test_picker_
== Lint ==
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
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.
> I expected
> node.one(
>
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=
> 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
1 | === modified file 'lib/lp/app/browser/lazrjs.py' |
2 | --- lib/lp/app/browser/lazrjs.py 2012-06-04 14:06:41 +0000 |
3 | +++ lib/lp/app/browser/lazrjs.py 2012-06-22 14:19:22 +0000 |
4 | @@ -346,7 +346,8 @@ |
5 | class InlinePersonEditPickerWidget(InlineEditPickerWidget): |
6 | def __init__(self, context, exported_field, default_html, |
7 | content_box_id=None, header='Select an item', |
8 | - step_title='Search', assign_me_text='Pick me', |
9 | + step_title='Search', show_create_team=False, |
10 | + assign_me_text='Pick me', |
11 | remove_person_text='Remove person', |
12 | remove_team_text='Remove team', |
13 | null_display_value='None', |
14 | @@ -372,13 +373,13 @@ |
15 | in and when JS is off. Defaults to the edit_view on the context. |
16 | :param edit_title: Used to set the title attribute of the anchor. |
17 | :param help_link: Used to set a link for help for the widget. |
18 | - :param target_context: The target the person is being set for. |
19 | """ |
20 | super(InlinePersonEditPickerWidget, self).__init__( |
21 | context, exported_field, default_html, content_box_id, header, |
22 | step_title, null_display_value, |
23 | edit_view, edit_url, edit_title, help_link) |
24 | |
25 | + self._show_create_team = show_create_team |
26 | self.assign_me_text = assign_me_text |
27 | self.remove_person_text = remove_person_text |
28 | self.remove_team_text = remove_team_text |
29 | @@ -399,11 +400,18 @@ |
30 | user = getUtility(ILaunchBag).user |
31 | return user and user in vocabulary |
32 | |
33 | + @property |
34 | + def show_create_team(self): |
35 | + return (self._show_create_team |
36 | + and getFeatureFlag( |
37 | + "disclosure.add-team-person-picker.enabled")) |
38 | + |
39 | def getConfig(self): |
40 | config = super(InlinePersonEditPickerWidget, self).getConfig() |
41 | config.update(dict( |
42 | show_remove_button=self.optional_field, |
43 | show_assign_me_button=self.show_assign_me_button, |
44 | + show_create_team=self.show_create_team, |
45 | assign_me_text=self.assign_me_text, |
46 | remove_person_text=self.remove_person_text, |
47 | remove_team_text=self.remove_team_text)) |
48 | |
49 | === modified file 'lib/lp/app/browser/tests/test_inlineeditpickerwidget.py' |
50 | --- lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2012-01-01 02:58:52 +0000 |
51 | +++ lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2012-06-22 14:19:22 +0000 |
52 | @@ -15,6 +15,7 @@ |
53 | InlineEditPickerWidget, |
54 | InlinePersonEditPickerWidget, |
55 | ) |
56 | +from lp.services.features.testing import FeatureFixture |
57 | from lp.testing import ( |
58 | login_person, |
59 | TestCaseWithFactory, |
60 | @@ -68,7 +69,7 @@ |
61 | |
62 | layer = DatabaseFunctionalLayer |
63 | |
64 | - def getWidget(self, widget_value, **kwargs): |
65 | + def getWidget(self, widget_value, show_create_team=False, **kwargs): |
66 | class ITest(Interface): |
67 | test_field = Choice(**kwargs) |
68 | |
69 | @@ -80,7 +81,8 @@ |
70 | |
71 | context = Test() |
72 | return InlinePersonEditPickerWidget( |
73 | - context, ITest['test_field'], None, edit_url='fake') |
74 | + context, ITest['test_field'], None, edit_url='fake', |
75 | + show_create_team=show_create_team) |
76 | |
77 | def test_person_selected_value_meta(self): |
78 | # The widget has the correct meta value for a person value. |
79 | @@ -115,3 +117,19 @@ |
80 | None, vocabulary='TargetPPAs', required=True) |
81 | login_person(self.factory.makePerson()) |
82 | self.assertFalse(widget.config['show_assign_me_button']) |
83 | + |
84 | + def test_show_create_team_link_with_feature_flag(self): |
85 | + with FeatureFixture( |
86 | + {'disclosure.add-team-person-picker.enabled': 'true'}): |
87 | + widget = self.getWidget( |
88 | + None, vocabulary='ValidPersonOrTeam', required=True, |
89 | + show_create_team=True) |
90 | + login_person(self.factory.makePerson()) |
91 | + self.assertTrue(widget.config['show_create_team']) |
92 | + |
93 | + def test_show_create_team_link(self): |
94 | + widget = self.getWidget( |
95 | + None, vocabulary='ValidPersonOrTeam', required=True, |
96 | + show_create_team=True) |
97 | + login_person(self.factory.makePerson()) |
98 | + self.assertFalse(widget.config['show_create_team']) |
99 | |
100 | === modified file 'lib/lp/app/javascript/picker/person_picker.js' |
101 | --- lib/lp/app/javascript/picker/person_picker.js 2012-06-19 16:21:02 +0000 |
102 | +++ lib/lp/app/javascript/picker/person_picker.js 2012-06-22 14:19:22 +0000 |
103 | @@ -16,8 +16,60 @@ |
104 | initializer: function(cfg) { |
105 | // If the user isn't logged in, override the show_assign_me value. |
106 | if (!Y.Lang.isValue(LP.links.me)) { |
107 | - this.set('show_assign_me_button', false); |
108 | + this.set('show_assign_me_button', false); |
109 | } |
110 | + this.set('new_team_template', this._new_team_template()); |
111 | + this.set('new_team_form', this._new_team_form()); |
112 | + }, |
113 | + |
114 | + _new_team_template: function() { |
115 | + return [ |
116 | + '<div class="new-team-node">', |
117 | + '<div class="step-on" style="width: 100%;"></div>', |
118 | + '<div class="transparent important-notice-popup">', |
119 | + '{{> new_team_form}}', |
120 | + '<div class="extra-form-buttons">', |
121 | + '<button class="yes_button" type="button"></button>', |
122 | + '<button class="no_button" type="button"></button>', |
123 | + '</div>', |
124 | + '</div>', |
125 | + '</div>'].join(''); |
126 | + }, |
127 | + |
128 | + _new_team_form: function() { |
129 | + // TODO - get the form using ++form++ |
130 | + return [ |
131 | + "<table id='launchpad-form-widgets' class='form'>", |
132 | + "<tbody><tr><td colspan='2'><div>", |
133 | + "<label for='field.name'>Name:</label><div>", |
134 | + "<input type='text' value='' size='20'", |
135 | + " name='field.name' id='field.name'", |
136 | + " class='lowerCaseText textType'></div>", |
137 | + "<p class='formHelp'>", |
138 | + " A short unique name, beginning with a lower-case letter", |
139 | + " or number, and containing only letters, numbers, dots,", |
140 | + " hyphens, or plus signs.</p>", |
141 | + "</div></td></tr><tr><td colspan='2'><div>", |
142 | + "<label for='field.displayname'>Display Name:</label><div>", |
143 | + "<input type='text' value='' size='20'", |
144 | + " name='field.displayname' id='field.displayname'", |
145 | + " class='textType'></div>", |
146 | + "<p class='formHelp'>", |
147 | + " This team's name as you would like it displayed", |
148 | + " throughout Launchpad.</p>", |
149 | + "</div></td></tr><tr><td colspan='2'><div>", |
150 | + "<label for='field.visibility'>Visibility:</label>", |
151 | + "<div><div><div class='value'>", |
152 | + "<select size='1'", |
153 | + " name='field.visibility' id='field.visibility'>", |
154 | + "<option value='PUBLIC' selected='selected'>Public</option>", |
155 | + "<option value='PRIVATE'>Private</option></select></div>", |
156 | + "</div></div><p class='formHelp'>", |
157 | + " Anyone can see a public team's data. Only team members", |
158 | + " and Launchpad admins can see private team data.", |
159 | + " Private teams cannot become public.</p>", |
160 | + "</div></td></tr></tbody></table>" |
161 | + ].join(''); |
162 | }, |
163 | |
164 | hide: function() { |
165 | @@ -31,10 +83,11 @@ |
166 | }, |
167 | |
168 | _update_button_text: function() { |
169 | + var link_text; |
170 | if (this.get('selected_value_metadata') === 'team') { |
171 | - var link_text = this.get('remove_team_text'); |
172 | + link_text = this.get('remove_team_text'); |
173 | } else { |
174 | - var link_text = this.get('remove_person_text'); |
175 | + link_text = this.get('remove_person_text'); |
176 | } |
177 | this.remove_button.set('text', link_text); |
178 | }, |
179 | @@ -75,32 +128,106 @@ |
180 | }); |
181 | }, |
182 | |
183 | + _cancel_new_team: function(picker) { |
184 | + var node = picker.get('contentBox').one('.new-team-node'); |
185 | + picker.hide_extra_content(node); |
186 | + }, |
187 | + |
188 | + _save_new_team: function(picker) { |
189 | + var node = picker.get('contentBox').one('.new-team-node'); |
190 | + var team_name = Y.Node.getDOMNode(node.one('[id=field.name]')).value; |
191 | + var team_display_name = |
192 | + Y.Node.getDOMNode(node.one('[id=field.displayname]')).value; |
193 | + picker.hide_extra_content(node); |
194 | + // TODO - make back end call to save team |
195 | + var value = { |
196 | + "api_uri": "/~" + team_name, |
197 | + "title": team_display_name, |
198 | + "value": team_name, |
199 | + "metadata": "team"}; |
200 | + picker.fire('validate', value); |
201 | + }, |
202 | + |
203 | + show_new_team_form: function () { |
204 | + var partials = {new_team_form: this.get('new_team_form')}; |
205 | + var html = Y.lp.mustache.to_html( |
206 | + this.get('new_team_template'), {}, partials); |
207 | + var self = this; |
208 | + var button_callback = function(e, callback_fn) { |
209 | + e.halt(); |
210 | + if (Y.Lang.isFunction(callback_fn) ) { |
211 | + callback_fn(self); |
212 | + } |
213 | + }; |
214 | + var team_form_node = Y.Node.create(html); |
215 | + team_form_node.one(".yes_button") |
216 | + .set('text', 'Create Team') |
217 | + .on('click', function(e) { |
218 | + button_callback(e, self._save_new_team); |
219 | + }); |
220 | + |
221 | + team_form_node.one(".no_button") |
222 | + .set('text', 'Cancel') |
223 | + .on('click', function(e) { |
224 | + button_callback(e, self._cancel_new_team); |
225 | + }); |
226 | + this.get('contentBox').one('.yui3-widget-bd') |
227 | + .insert(team_form_node, 'before'); |
228 | + this.show_extra_content( |
229 | + team_form_node.one(".important-notice-popup"), |
230 | + "Enter new team details"); |
231 | + }, |
232 | + |
233 | + _assign_me_button_html: function() { |
234 | + return [ |
235 | + '<a class="yui-picker-assign-me-button bg-image ', |
236 | + 'js-action" href="javascript:void(0)" ', |
237 | + 'style="background-image: url(/@@/person); ', |
238 | + 'padding-right: 1em">', |
239 | + this.get('assign_me_text'), |
240 | + '</a>'].join(''); |
241 | + }, |
242 | + |
243 | + _remove_button_html: function() { |
244 | + return [ |
245 | + '<a class="yui-picker-remove-button bg-image js-action" ', |
246 | + 'href="javascript:void(0)" ', |
247 | + 'style="background-image: url(/@@/remove); ', |
248 | + 'padding-right: 1em">', |
249 | + this.get('remove_person_text'), |
250 | + '</a>'].join(''); |
251 | + }, |
252 | + |
253 | + _new_team_button_html: function() { |
254 | + return [ |
255 | + '<a class="yui-picker-new-team-button sprite add ', |
256 | + 'js-action" href="javascript:void(0)">', |
257 | + 'New Team', |
258 | + '</a>'].join(''); |
259 | + }, |
260 | renderUI: function() { |
261 | Y.lazr.picker.Picker.prototype.renderUI.apply(this, arguments); |
262 | var extra_buttons = this.get('extra_buttons'); |
263 | - var remove_button, assign_me_button; |
264 | + var remove_button, assign_me_button, new_team_button; |
265 | |
266 | if (this.get('show_remove_button')) { |
267 | - remove_button = Y.Node.create( |
268 | - '<a class="yui-picker-remove-button bg-image" ' + |
269 | - 'href="javascript:void(0)" ' + |
270 | - 'style="background-image: url(/@@/remove); padding-right: ' + |
271 | - '1em">' + this.get('remove_person_text') + '</a>'); |
272 | + remove_button = Y.Node.create(this._remove_button_html()); |
273 | remove_button.on('click', this.remove, this); |
274 | extra_buttons.appendChild(remove_button); |
275 | this.remove_button = remove_button; |
276 | } |
277 | |
278 | if (this.get('show_assign_me_button')) { |
279 | - assign_me_button = Y.Node.create( |
280 | - '<a class="yui-picker-assign-me-button bg-image" ' + |
281 | - 'href="javascript:void(0)" ' + |
282 | - 'style="background-image: url(/@@/person)">' + |
283 | - this.get('assign_me_text') + '</a>'); |
284 | + assign_me_button = Y.Node.create(this._assign_me_button_html()); |
285 | assign_me_button.on('click', this.assign_me, this); |
286 | extra_buttons.appendChild(assign_me_button); |
287 | this.assign_me_button = assign_me_button; |
288 | } |
289 | + if (this.get('show_create_team')) { |
290 | + new_team_button = Y.Node.create(this._new_team_button_html()); |
291 | + new_team_button.on('click', this.show_new_team_form, this); |
292 | + extra_buttons.appendChild(new_team_button); |
293 | + } |
294 | this._search_input.insert( |
295 | extra_buttons, this._search_input.get('parentNode')); |
296 | this._show_hide_buttons(); |
297 | @@ -112,15 +239,18 @@ |
298 | ATTRS: { |
299 | extra_buttons: { |
300 | valueFn: function () { |
301 | - return Y.Node.create('<div class="extra-form-buttons"/>') |
302 | - } |
303 | + return Y.Node.create('<div class="extra-form-buttons"/>'); |
304 | + } |
305 | }, |
306 | show_assign_me_button: { value: true }, |
307 | show_remove_button: {value: true }, |
308 | assign_me_text: {value: 'Pick me'}, |
309 | remove_person_text: {value: 'Remove person'}, |
310 | remove_team_text: {value: 'Remove team'}, |
311 | - min_search_chars: {value: 2} |
312 | + min_search_chars: {value: 2}, |
313 | + show_create_team: {value: false}, |
314 | + new_team_template: {value: null}, |
315 | + new_team_form: {value: null} |
316 | } |
317 | }); |
318 | -}, "0.1", {"requires": ["base", "node", "lazr.picker"]}); |
319 | +}, "0.1", {"requires": ["base", "node", "lazr.picker", "lp.mustache"]}); |
320 | |
321 | === modified file 'lib/lp/app/javascript/picker/picker.js' |
322 | --- lib/lp/app/javascript/picker/picker.js 2012-06-20 16:38:28 +0000 |
323 | +++ lib/lp/app/javascript/picker/picker.js 2012-06-22 14:19:22 +0000 |
324 | @@ -20,7 +20,7 @@ |
325 | */ |
326 | ns.Picker = Y.Base.create('picker', Y.lazr.PrettyOverlay, [], { |
327 | |
328 | - /** |
329 | + /** |
330 | * The search input node. |
331 | * |
332 | * @property _search_button |
333 | @@ -823,6 +823,61 @@ |
334 | }, |
335 | |
336 | /* |
337 | + * Insert the extra content into the form and animate its appearance. |
338 | + */ |
339 | + show_extra_content: function(extra_content, header) { |
340 | + if (Y.Lang.isValue(header)) { |
341 | + this.set('picker_header', this.get('headerContent')); |
342 | + this.set( |
343 | + 'headerContent', |
344 | + Y.Node.create("<h2></h2>").set('text', header)); |
345 | + } |
346 | + this.get('contentBox').one('.yui3-widget-bd').hide(); |
347 | + this.get('contentBox').all('.steps').hide(); |
348 | + var duration = 0; |
349 | + if (this.get('use_animation')) { |
350 | + duration = 0.9; |
351 | + } |
352 | + var fade_in = new Y.Anim({ |
353 | + node: extra_content, |
354 | + to: {opacity: 1}, |
355 | + duration: duration |
356 | + }); |
357 | + fade_in.run(); |
358 | + }, |
359 | + |
360 | + hide_extra_content: function(extra_content_node) { |
361 | + var saved_header = this.get('picker_header'); |
362 | + if (Y.Lang.isValue(saved_header)) { |
363 | + this.set('headerContent', saved_header); |
364 | + this.set('picker_header', null); |
365 | + } |
366 | + this.get('contentBox').all('.steps').show(); |
367 | + var content_node = this.get('contentBox').one('.yui3-widget-bd'); |
368 | + if (extra_content_node !== null) { |
369 | + extra_content_node.get('parentNode') |
370 | + .removeChild(extra_content_node); |
371 | + content_node.addClass('transparent'); |
372 | + content_node.setStyle('opacity', 0); |
373 | + content_node.show(); |
374 | + var duration = 0; |
375 | + if (this.get('use_animation')) { |
376 | + duration = 0.6; |
377 | + } |
378 | + var content_fade_in = new Y.Anim({ |
379 | + node: content_node, |
380 | + to: {opacity: 1}, |
381 | + duration: duration |
382 | + }); |
383 | + content_fade_in.run(); |
384 | + } else { |
385 | + content_node.removeClass('transparent'); |
386 | + content_node.setStyle('opacity', 1); |
387 | + content_node.show(); |
388 | + } |
389 | + }, |
390 | + |
391 | + /* |
392 | * Clear all elements of the picker, resetting it to its original state. |
393 | * |
394 | * @method _clear |
395 | @@ -970,8 +1025,8 @@ |
396 | clear_on_cancel: { value: false }, |
397 | |
398 | /** |
399 | - * A CSS selector for the DOM element that will activate (show) the picker |
400 | - * once clicked. |
401 | + * A CSS selector for the DOM element that will activate (show) the |
402 | + * picker once clicked. |
403 | * |
404 | * @attribute picker_activator |
405 | * @type String |
406 | @@ -979,8 +1034,8 @@ |
407 | picker_activator: {}, |
408 | |
409 | /** |
410 | - * An extra CSS class to be added to the picker_activator, generally used |
411 | - * to distinguish regular links from js-triggering ones. |
412 | + * An extra CSS class to be added to the picker_activator, generally |
413 | + * used to distinguish regular links from js-triggering ones. |
414 | * |
415 | * @attribute picker_activator_css_class |
416 | * @type String |
417 | @@ -998,8 +1053,8 @@ |
418 | min_search_chars: { value: 3 }, |
419 | |
420 | /** |
421 | - * The current search string, which is needed when clicking on a different |
422 | - * batch if the search input has been modified. |
423 | + * The current search string, which is needed when clicking on a |
424 | + * different batch if the search input has been modified. |
425 | * |
426 | * @attribute current_search_string |
427 | * @type String |
428 | @@ -1015,8 +1070,8 @@ |
429 | current_filter_value: {value: null}, |
430 | |
431 | /** |
432 | - * A list of attribute name values used to construct the filtering options |
433 | - * for this picker.. |
434 | + * A list of attribute name values used to construct the filtering |
435 | + * options for this picker. |
436 | * |
437 | * @attribute filter_options |
438 | * @type Object |
439 | @@ -1079,7 +1134,7 @@ |
440 | * this value automatically updates the display. |
441 | * |
442 | * This an array of object containing the two keys, name (used as |
443 | - * the batch label) and value (used as additional details to 'search' |
444 | + * the batch label) and value (used as additional details to 'search' |
445 | * event). |
446 | * |
447 | * @attribute batches |
448 | @@ -1091,9 +1146,9 @@ |
449 | * For simplified batch creation, you can set this to the number of |
450 | * batches in the search results. In this case, the batch labels |
451 | * and values are automatically calculated. The batch name (used as the |
452 | - * batch label) will be the batch number starting from 1. The batch value |
453 | - * (used as additional details to the 'search' event) will be the batch |
454 | - * number, starting from zero. |
455 | + * batch label) will be the batch number starting from 1. The batch |
456 | + * value (used as additional details to the 'search' event) will be the |
457 | + * batch number, starting from zero. |
458 | * |
459 | * If 'batches' is set (see above), batch_count is ignored. |
460 | * |
461 | @@ -1139,8 +1194,8 @@ |
462 | error: { value: null }, |
463 | |
464 | /** |
465 | - * The message to display when the search returned no results. This string |
466 | - * can contain a 'query' placeholder |
467 | + * The message to display when the search returned no results. |
468 | + * This string can contain a 'query' placeholder |
469 | * |
470 | * @attribute no_results_search_message |
471 | * @type String |
472 | @@ -1148,6 +1203,17 @@ |
473 | */ |
474 | no_results_search_message: { |
475 | value: 'No items matched "{query}".' |
476 | + }, |
477 | + |
478 | + /** |
479 | + * Whether to use animations (fade in/out) for content rendering. |
480 | + * |
481 | + * @attribute use_animation |
482 | + * @type Boolean |
483 | + * @default true |
484 | + */ |
485 | + use_animation: { |
486 | + value: true |
487 | } |
488 | } |
489 | }); |
490 | @@ -1186,7 +1252,7 @@ |
491 | this.get('host').setAttrs({ |
492 | selected_value_metadata: result.metadata, |
493 | selected_value: result.value |
494 | - }) |
495 | + }); |
496 | input.set("value", result.value || ''); |
497 | // If the search input isn't blurred before it is focused, |
498 | // then the I-beam disappears. |
499 | |
500 | === modified file 'lib/lp/app/javascript/picker/picker_patcher.js' |
501 | --- lib/lp/app/javascript/picker/picker_patcher.js 2012-06-18 21:52:25 +0000 |
502 | +++ lib/lp/app/javascript/picker/picker_patcher.js 2012-06-22 14:19:22 +0000 |
503 | @@ -24,9 +24,21 @@ |
504 | if (show_widget_node.hasClass('js-action')) { |
505 | return; |
506 | } |
507 | - show_widget_node.set('innerHTML', 'Choose…'); |
508 | - show_widget_node.addClass('js-action'); |
509 | - show_widget_node.get('parentNode').removeClass('unseen'); |
510 | + var picker_span = show_widget_node.get('parentNode'); |
511 | + if (config.enhanced_picker) { |
512 | + var new_node = Y.Node.create('<span>(<a href="#"></a>)</span>'); |
513 | + show_widget_node = new_node.one('a'); |
514 | + show_widget_node |
515 | + .set('id', show_widget_id) |
516 | + .addClass('js-action') |
517 | + .set('text', 'Choose\u2026'); |
518 | + picker_span.empty(); |
519 | + picker_span.appendChild(new_node); |
520 | + } else { |
521 | + show_widget_node.set('text', 'Choose\u2026'); |
522 | + show_widget_node.addClass('js-action'); |
523 | + } |
524 | + picker_span.removeClass('unseen'); |
525 | show_widget_node.on('click', function (e) { |
526 | if (picker === null) { |
527 | picker = namespace.create( |
528 | @@ -261,7 +273,7 @@ |
529 | |
530 | node.one(".validation-content-placeholder").replace(content); |
531 | picker.get('contentBox').one('.yui3-widget-bd').insert(node, 'before'); |
532 | - animate_validation_content(picker, node.one(".important-notice-popup")); |
533 | + picker.show_extra_content(node.one(".important-notice-popup")); |
534 | }; |
535 | |
536 | /* |
537 | @@ -298,42 +310,11 @@ |
538 | }; |
539 | |
540 | /* |
541 | - * Insert the validation content into the form and animate its appearance. |
542 | - */ |
543 | -function animate_validation_content(picker, validation_content) { |
544 | - picker.get('contentBox').one('.yui3-widget-bd').hide(); |
545 | - picker.get('contentBox').all('.steps').hide(); |
546 | - var validation_fade_in = new Y.Anim({ |
547 | - node: validation_content, |
548 | - to: {opacity: 1}, |
549 | - duration: 0.9 |
550 | - }); |
551 | - validation_fade_in.run(); |
552 | -} |
553 | - |
554 | -/* |
555 | * Restore a picker to its functional state after a validation operation. |
556 | */ |
557 | function reset_form(picker) { |
558 | - picker.get('contentBox').all('.steps').show(); |
559 | var validation_node = picker.get('contentBox').one('.validation-node'); |
560 | - var content_node = picker.get('contentBox').one('.yui3-widget-bd'); |
561 | - if (validation_node !== null) { |
562 | - validation_node.get('parentNode').removeChild(validation_node); |
563 | - content_node.addClass('transparent'); |
564 | - content_node.setStyle('opacity', 0); |
565 | - content_node.show(); |
566 | - var content_fade_in = new Y.Anim({ |
567 | - node: content_node, |
568 | - to: {opacity: 1}, |
569 | - duration: 0.6 |
570 | - }); |
571 | - content_fade_in.run(); |
572 | - } else { |
573 | - content_node.removeClass('transparent'); |
574 | - content_node.setStyle('opacity', 1); |
575 | - content_node.show(); |
576 | - } |
577 | + picker.hide_extra_content(validation_node); |
578 | } |
579 | |
580 | |
581 | |
582 | === modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html' |
583 | --- lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-18 21:52:51 +0000 |
584 | +++ lib/lp/app/javascript/picker/tests/test_personpicker.html 2012-06-22 14:19:22 +0000 |
585 | @@ -36,6 +36,8 @@ |
586 | <script type="text/javascript" |
587 | src="../../../../../../build/js/lp/app/lazr/lazr.js"></script> |
588 | <script type="text/javascript" |
589 | + src="../../../../../../build/js/lp/app/mustache.js"></script> |
590 | + <script type="text/javascript" |
591 | src="../../../../../../build/js/lp/app/overlay/overlay.js"></script> |
592 | <script type="text/javascript" |
593 | src="../../../../../../build/js/lp/app/effects/effects.js"></script> |
594 | |
595 | === modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.js' |
596 | --- lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-20 16:38:28 +0000 |
597 | +++ lib/lp/app/javascript/picker/tests/test_personpicker.js 2012-06-22 14:19:22 +0000 |
598 | @@ -3,7 +3,7 @@ |
599 | |
600 | YUI().use('test', 'console', 'plugin', |
601 | 'lazr.picker', 'lazr.person-picker', 'lp.app.picker', |
602 | - 'node-event-simulate', function(Y) { |
603 | + 'lp.app.mustache', 'node-event-simulate', function(Y) { |
604 | |
605 | var Assert = Y.Assert; |
606 | |
607 | @@ -92,11 +92,12 @@ |
608 | }, |
609 | |
610 | _picker_params: function( |
611 | - show_assign_me_button, show_remove_button, |
612 | + show_assign_me_button, show_remove_button, show_create_team, |
613 | selected_value, selected_value_metadata) { |
614 | return { |
615 | "show_assign_me_button": show_assign_me_button, |
616 | "show_remove_button": show_remove_button, |
617 | + "show_create_team": show_create_team, |
618 | "selected_value": selected_value, |
619 | "selected_value_metadata": selected_value_metadata |
620 | }; |
621 | @@ -185,10 +186,8 @@ |
622 | |
623 | test_picker_remove_person_button_text: function() { |
624 | // The remove button text is correct. |
625 | - this.create_picker(this._picker_params(true, |
626 | - true, |
627 | - "fred", |
628 | - "person")); |
629 | + this.create_picker(this._picker_params( |
630 | + true, true, false, "fred", "person")); |
631 | this.picker.render(); |
632 | var remove_button = Y.one('.yui-picker-remove-button'); |
633 | Assert.areEqual('Remove someone', remove_button.get('innerHTML')); |
634 | @@ -196,7 +195,8 @@ |
635 | |
636 | test_picker_remove_team_button_text: function() { |
637 | // The remove button text is correct. |
638 | - this.create_picker(this._picker_params(true, true, "cats", "team")); |
639 | + this.create_picker(this._picker_params( |
640 | + true, true, false, "cats", "team")); |
641 | this.picker.render(); |
642 | var remove_button = Y.one('.yui-picker-remove-button'); |
643 | Assert.areEqual('Remove some team', remove_button.get('innerHTML')); |
644 | @@ -220,7 +220,8 @@ |
645 | test_picker_no_assign_me_button_if_value_is_me: function() { |
646 | // The assign me button is not shown if the picker is created for a |
647 | // field where the value is "me". |
648 | - this.create_picker(this._picker_params(true, true, "me"), this.ME); |
649 | + this.create_picker(this._picker_params( |
650 | + true, true, false, "me"), this.ME); |
651 | this.picker.render(); |
652 | this._check_assign_me_button_state(false); |
653 | }, |
654 | @@ -236,7 +237,8 @@ |
655 | test_picker_has_remove_button_if_value: function() { |
656 | // The remove button is shown if the picker is created for a field |
657 | // which has a value. |
658 | - this.create_picker(this._picker_params(true, true, "me"), this.ME); |
659 | + this.create_picker(this._picker_params( |
660 | + true, true, false, "me"), this.ME); |
661 | this.picker.render(); |
662 | this._check_remove_button_state(true); |
663 | }, |
664 | @@ -244,9 +246,151 @@ |
665 | test_picker_no_remove_button_unless_configured: function() { |
666 | // The remove button is only rendered if show_remove_button |
667 | // setting is true. |
668 | - this.create_picker(this._picker_params(true, false, "me"), this.ME); |
669 | + this.create_picker(this._picker_params( |
670 | + true, false, false, "me"), this.ME); |
671 | this.picker.render(); |
672 | Assert.isNull(Y.one('.yui-picker-remove-button')); |
673 | + }, |
674 | + |
675 | + test_picker_assign_me_button_hide_on_save: function() { |
676 | + // The assign me button is shown initially but hidden if the picker |
677 | + // saves a value equal to 'me'. |
678 | + this.create_picker(this._picker_params(true, true)); |
679 | + this._check_assign_me_button_state(true); |
680 | + this.picker.set('results', this.vocabulary); |
681 | + this.picker.render(); |
682 | + simulate( |
683 | + this.picker.get('boundingBox').one('.yui3-picker-results'), |
684 | + 'li:nth-child(1)', 'click'); |
685 | + this._check_assign_me_button_state(false); |
686 | + }, |
687 | + |
688 | + test_picker_remove_button_clicked: function() { |
689 | + // The remove button is hidden once a picker value has been removed. |
690 | + // And the assign me button is shown. |
691 | + this.create_picker(this._picker_params( |
692 | + true, true, false, "me"), this.ME); |
693 | + this.picker.render(); |
694 | + this._check_assign_me_button_state(false); |
695 | + var remove = Y.one('.yui-picker-remove-button'); |
696 | + remove.simulate('click'); |
697 | + this._check_remove_button_state(false); |
698 | + this._check_assign_me_button_state(true); |
699 | + }, |
700 | + |
701 | + test_picker_assign_me_button_clicked: function() { |
702 | + // The assign me button is hidden once it is clicked. |
703 | + // And the remove button is shown. |
704 | + this.create_picker(this._picker_params(true, true)); |
705 | + this.picker.render(); |
706 | + var assign_me = Y.one('.yui-picker-assign-me-button'); |
707 | + assign_me.simulate('click'); |
708 | + this._check_remove_button_state(true); |
709 | + this._check_assign_me_button_state(false); |
710 | + }, |
711 | + |
712 | + test_picker_assign_me_updates_remove_text: function() { |
713 | + // When Assign me is used, the Remove button text is updated from |
714 | + // the team removal text to the person removal text. |
715 | + this.create_picker(this._picker_params( |
716 | + true, true, false, "cats", "team")); |
717 | + this.picker.render(); |
718 | + var remove_button = Y.one('.yui-picker-remove-button'); |
719 | + Assert.areEqual('Remove some team', remove_button.get('innerHTML')); |
720 | + var assign_me = Y.one('.yui-picker-assign-me-button'); |
721 | + assign_me.simulate('click'); |
722 | + Assert.areEqual('Remove someone', remove_button.get('innerHTML')); |
723 | + }, |
724 | + |
725 | + test_picker_save_updates_remove_text: function() { |
726 | + // When save is called, the Remove button text is updated |
727 | + // according to the newly saved value. |
728 | + this.create_picker(this._picker_params( |
729 | + true, true, false, "me"), this.ME); |
730 | + var remove_button = Y.one('.yui-picker-remove-button'); |
731 | + Assert.areEqual('Remove someone', remove_button.get('innerHTML')); |
732 | + this.picker.set('results', this.vocabulary); |
733 | + this.picker.render(); |
734 | + simulate( |
735 | + this.picker.get('boundingBox').one('.yui3-picker-results'), |
736 | + 'li:nth-child(2)', 'click'); |
737 | + Assert.areEqual('Remove some team', remove_button.get('innerHTML')); |
738 | + }, |
739 | + |
740 | + test_picker_no_team_button_unless_configured: function() { |
741 | + // The new team button is only rendered if show_create_team |
742 | + // setting is true. |
743 | + this.create_picker(this._picker_params(true, false, false)); |
744 | + this.picker.render(); |
745 | + Assert.isNull(Y.one('.yui-picker-new-team-button')); |
746 | + }, |
747 | + |
748 | + test_picker_new_team_button_click_shows_form: function() { |
749 | + // Clicking the new team button displays the new team form. |
750 | + this.create_picker(this._picker_params(true, true, true)); |
751 | + this.picker.render(); |
752 | + var new_team = this.picker.get('boundingBox') |
753 | + .one('.yui-picker-new-team-button'); |
754 | + new_team.simulate('click'); |
755 | + Y.Assert.areEqual( |
756 | + 'Enter new team details', |
757 | + this.picker.get('headerContent').get('text')); |
758 | + Y.Assert.isNotNull( |
759 | + this.picker.get('contentBox').one('[id=field.name]')); |
760 | + Y.Assert.areEqual('none', |
761 | + this.picker.get('contentBox').one('.yui3-widget-bd') |
762 | + .getStyle('display')); |
763 | + }, |
764 | + |
765 | + test_picker_new_team_cancel: function() { |
766 | + // Clicking the cancel button on the new team form reverts back to |
767 | + // the normal picker. |
768 | + this.create_picker(this._picker_params(true, true, true)); |
769 | + this.picker.render(); |
770 | + var new_team = this.picker.get('boundingBox') |
771 | + .one('.yui-picker-new-team-button'); |
772 | + new_team.simulate('click'); |
773 | + Y.Assert.areEqual( |
774 | + 'Enter new team details', |
775 | + this.picker.get('headerContent').get('text')); |
776 | + var form_buttons = this.picker.get('contentBox') |
777 | + .one('.extra-form-buttons'); |
778 | + simulate( |
779 | + form_buttons, 'button:nth-child(2)', 'click'); |
780 | + Y.Assert.areEqual( |
781 | + 'Pick Someone', |
782 | + this.picker.get('headerContent').get('text')); |
783 | + Y.Assert.isNull( |
784 | + this.picker.get('contentBox').one('[id=field.name]')); |
785 | + Y.Assert.isNotNull( |
786 | + this.picker.get('contentBox').one('.yui3-picker-search')); |
787 | + }, |
788 | + |
789 | + test_picker_new_team_save: function() { |
790 | + // Clicking the save button on the new team form fires a 'save' |
791 | + // event with the expected data. |
792 | + this.create_picker(this._picker_params(true, true, true)); |
793 | + this.picker.render(); |
794 | + |
795 | + var result_published = false; |
796 | + this.picker.subscribe('save', function(e) { |
797 | + var saved_value = |
798 | + e.details[Y.lazr.picker.Picker.SAVE_RESULT]; |
799 | + Y.Assert.areEqual('/~fred', saved_value.api_uri); |
800 | + Y.Assert.areEqual('fred', saved_value.value); |
801 | + result_published = true; |
802 | + }); |
803 | + |
804 | + var picker_content = this.picker.get('boundingBox'); |
805 | + var new_team = |
806 | + picker_content.one('.yui-picker-new-team-button'); |
807 | + new_team.simulate('click'); |
808 | + var team_name = picker_content.one('[id=field.name]'); |
809 | + Y.Node.getDOMNode(team_name).value = 'fred'; |
810 | + var form_buttons = picker_content.one('.extra-form-buttons'); |
811 | + simulate( |
812 | + form_buttons, 'button:nth-child(1)', 'click'); |
813 | + Y.Assert.isTrue(result_published); |
814 | } |
815 | }; |
816 | |
817 | @@ -266,6 +410,7 @@ |
818 | } |
819 | |
820 | var config = { |
821 | + "use_animation": false, |
822 | "picker_type": "person", |
823 | "step_title": "Choose someone", |
824 | "header": "Pick Someone", |
825 | @@ -275,6 +420,7 @@ |
826 | "show_remove_button": params.show_remove_button, |
827 | "selected_value": params.selected_value, |
828 | "selected_value_metadata": params.selected_value_metadata, |
829 | + "show_create_team": params.show_create_team, |
830 | "assign_me_text": "Assign Moi", |
831 | "remove_person_text": "Remove someone", |
832 | "remove_team_text": "Remove some team" |
833 | @@ -285,69 +431,6 @@ |
834 | "test_link", |
835 | "picker_id", |
836 | config); |
837 | - console.log(this.picker); |
838 | - }, |
839 | - |
840 | - test_picker_assign_me_button_hide_on_save: function() { |
841 | - // The assign me button is shown initially but hidden if the picker |
842 | - // saves a value equal to 'me'. |
843 | - this.create_picker(this._picker_params(true, true)); |
844 | - this._check_assign_me_button_state(true); |
845 | - this.picker.set('results', this.vocabulary); |
846 | - this.picker.render(); |
847 | - simulate( |
848 | - this.picker.get('boundingBox').one('.yui3-picker-results'), |
849 | - 'li:nth-child(1)', 'click'); |
850 | - this._check_assign_me_button_state(false); |
851 | - }, |
852 | - |
853 | - test_picker_remove_button_clicked: function() { |
854 | - // The remove button is hidden once a picker value has been removed. |
855 | - // And the assign me button is shown. |
856 | - this.create_picker(this._picker_params(true, true, "me"), this.ME); |
857 | - this.picker.render(); |
858 | - this._check_assign_me_button_state(false); |
859 | - var remove = Y.one('.yui-picker-remove-button'); |
860 | - remove.simulate('click'); |
861 | - this._check_remove_button_state(false); |
862 | - this._check_assign_me_button_state(true); |
863 | - }, |
864 | - |
865 | - test_picker_assign_me_button_clicked: function() { |
866 | - // The assign me button is hidden once it is clicked. |
867 | - // And the remove button is shown. |
868 | - this.create_picker(this._picker_params(true, true)); |
869 | - this.picker.render(); |
870 | - var assign_me = Y.one('.yui-picker-assign-me-button'); |
871 | - assign_me.simulate('click'); |
872 | - this._check_remove_button_state(true); |
873 | - this._check_assign_me_button_state(false); |
874 | - }, |
875 | - |
876 | - test_picker_assign_me_updates_remove_text: function() { |
877 | - // When Assign me is used, the Remove button text is updated from |
878 | - // the team removal text to the person removal text. |
879 | - this.create_picker(this._picker_params(true, true, "cats", "team")); |
880 | - this.picker.render(); |
881 | - var remove_button = Y.one('.yui-picker-remove-button'); |
882 | - Assert.areEqual('Remove some team', remove_button.get('innerHTML')); |
883 | - var assign_me = Y.one('.yui-picker-assign-me-button'); |
884 | - assign_me.simulate('click'); |
885 | - Assert.areEqual('Remove someone', remove_button.get('innerHTML')); |
886 | - }, |
887 | - |
888 | - test_picker_save_updates_remove_text: function() { |
889 | - // When save is called, the Remove button text is updated |
890 | - // according to the newly saved value. |
891 | - this.create_picker(this._picker_params(true, true, "me"), this.ME); |
892 | - var remove_button = Y.one('.yui-picker-remove-button'); |
893 | - Assert.areEqual('Remove someone', remove_button.get('innerHTML')); |
894 | - this.picker.set('results', this.vocabulary); |
895 | - this.picker.render(); |
896 | - simulate( |
897 | - this.picker.get('boundingBox').one('.yui3-picker-results'), |
898 | - 'li:nth-child(2)', 'click'); |
899 | - Assert.areEqual('Remove some team', remove_button.get('innerHTML')); |
900 | } |
901 | }; |
902 | |
903 | @@ -377,9 +460,11 @@ |
904 | } |
905 | var config = { |
906 | "picker_type": "person", |
907 | + "header": "Pick Someone", |
908 | "associated_field_id": associated_field_id, |
909 | "show_assign_me_button": params.show_assign_me_button, |
910 | "show_remove_button": params.show_remove_button, |
911 | + "show_create_team": params.show_create_team, |
912 | "selected_value": params.selected_value, |
913 | "selected_value_metadata": params.selected_value_metadata, |
914 | "assign_me_text": "Assign Moi", |
915 | @@ -388,68 +473,6 @@ |
916 | }; |
917 | this.picker = Y.lp.app.picker.create( |
918 | this.vocabulary, config, associated_field_id); |
919 | - }, |
920 | - |
921 | - test_picker_assign_me_button_hide_on_save: function() { |
922 | - // The assign me button is shown initially but hidden if the picker |
923 | - // saves a value equal to 'me'. |
924 | - this.create_picker(this._picker_params(true, true)); |
925 | - this._check_assign_me_button_state(true); |
926 | - this.picker.set('results', this.vocabulary); |
927 | - this.picker.render(); |
928 | - simulate( |
929 | - this.picker.get('boundingBox').one('.yui3-picker-results'), |
930 | - 'li:nth-child(1)', 'click'); |
931 | - this._check_assign_me_button_state(false); |
932 | - }, |
933 | - |
934 | - test_picker_remove_button_clicked: function() { |
935 | - // The remove button is hidden once a picker value has been removed. |
936 | - // And the assign me button is shown. |
937 | - this.create_picker(this._picker_params(true, true, "me"), this.ME); |
938 | - this.picker.render(); |
939 | - this._check_assign_me_button_state(false); |
940 | - var remove = Y.one('.yui-picker-remove-button'); |
941 | - remove.simulate('click'); |
942 | - this._check_remove_button_state(false); |
943 | - this._check_assign_me_button_state(true); |
944 | - }, |
945 | - |
946 | - test_picker_assign_me_button_clicked: function() { |
947 | - // The assign me button is hidden once it is clicked. |
948 | - // And the remove button is shown. |
949 | - this.create_picker(this._picker_params(true, true)); |
950 | - this.picker.render(); |
951 | - var assign_me = Y.one('.yui-picker-assign-me-button'); |
952 | - assign_me.simulate('click'); |
953 | - this._check_remove_button_state(true); |
954 | - this._check_assign_me_button_state(false); |
955 | - }, |
956 | - |
957 | - test_picker_assign_me_updates_remove_text: function() { |
958 | - // When Assign me is used, the Remove button text is updated from |
959 | - // the team removal text to the person removal text. |
960 | - this.create_picker(this._picker_params(true, true, "cats", "team")); |
961 | - this.picker.render(); |
962 | - var remove_button = Y.one('.yui-picker-remove-button'); |
963 | - Assert.areEqual('Remove some team', remove_button.get('innerHTML')); |
964 | - var assign_me = Y.one('.yui-picker-assign-me-button'); |
965 | - assign_me.simulate('click'); |
966 | - Assert.areEqual('Remove someone', remove_button.get('innerHTML')); |
967 | - }, |
968 | - |
969 | - test_picker_save_updates_remove_text: function() { |
970 | - // When save is called, the Remove button text is updated |
971 | - // according to the newly saved value. |
972 | - this.create_picker(this._picker_params(true, true, "me"), this.ME); |
973 | - var remove_button = Y.one('.yui-picker-remove-button'); |
974 | - Assert.areEqual('Remove someone', remove_button.get('innerHTML')); |
975 | - this.picker.set('results', this.vocabulary); |
976 | - this.picker.render(); |
977 | - simulate( |
978 | - this.picker.get('boundingBox').one('.yui3-picker-results'), |
979 | - 'li:nth-child(2)', 'click'); |
980 | - Assert.areEqual('Remove some team', remove_button.get('innerHTML')); |
981 | } |
982 | }; |
983 | |
984 | |
985 | === modified file 'lib/lp/app/widgets/popup.py' |
986 | --- lib/lp/app/widgets/popup.py 2012-02-22 05:22:13 +0000 |
987 | +++ lib/lp/app/widgets/popup.py 2012-06-22 14:19:22 +0000 |
988 | @@ -23,6 +23,7 @@ |
989 | get_person_picker_entry_metadata, |
990 | vocabulary_filters, |
991 | ) |
992 | +from lp.services.features import getFeatureFlag |
993 | from lp.services.propertycache import cachedproperty |
994 | from lp.services.webapp import canonical_url |
995 | |
996 | @@ -41,6 +42,7 @@ |
997 | assign_me_text = 'Pick me' |
998 | remove_person_text = 'Remove person' |
999 | remove_team_text = 'Remove team' |
1000 | + show_create_team_link = False |
1001 | |
1002 | popup_name = 'popup-vocabulary-picker' |
1003 | |
1004 | @@ -56,6 +58,12 @@ |
1005 | # Defaults to self.vocabulary.displayname. |
1006 | header = None |
1007 | |
1008 | + @property |
1009 | + def enhanced_picker(self): |
1010 | + flag = getFeatureFlag( |
1011 | + "disclosure.add-team-person-picker.enabled") |
1012 | + return flag and self.show_create_team_link |
1013 | + |
1014 | @cachedproperty |
1015 | def matches(self): |
1016 | """Return a list of matches (as ITokenizedTerm) to whatever the |
1017 | @@ -143,7 +151,9 @@ |
1018 | vocabulary_name=self.vocabulary_name, |
1019 | vocabulary_filters=self.vocabulary_filters, |
1020 | input_element=self.input_id, |
1021 | - show_widget_id=self.show_widget_id) |
1022 | + show_widget_id=self.show_widget_id, |
1023 | + enhanced_picker=self.enhanced_picker, |
1024 | + show_create_team=self.enhanced_picker) |
1025 | |
1026 | @property |
1027 | def json_config(self): |
1028 | @@ -206,8 +216,12 @@ |
1029 | else: |
1030 | css = '' |
1031 | return ('<span class="%s">(<a id="%s" href="%s">' |
1032 | - 'Find…</a>)</span>') % ( |
1033 | - css, self.show_widget_id, self.nonajax_uri or '#') |
1034 | + 'Find…</a>)%s</span>') % ( |
1035 | + css, self.show_widget_id, self.nonajax_uri or '#', |
1036 | + self.extraChooseLink() or '') |
1037 | + |
1038 | + def extraChooseLink(self): |
1039 | + return None |
1040 | |
1041 | @property |
1042 | def nonajax_uri(self): |
1043 | @@ -220,7 +234,6 @@ |
1044 | |
1045 | class PersonPickerWidget(VocabularyPickerWidget): |
1046 | |
1047 | - include_create_team_link = False |
1048 | show_assign_me_button = True |
1049 | show_remove_button = False |
1050 | picker_type = 'person' |
1051 | @@ -230,12 +243,11 @@ |
1052 | val = self._getFormValue() |
1053 | return get_person_picker_entry_metadata(val) |
1054 | |
1055 | - def chooseLink(self): |
1056 | - link = super(PersonPickerWidget, self).chooseLink() |
1057 | - if self.include_create_team_link: |
1058 | - link += ('or (<a href="/people/+newteam">' |
1059 | + def extraChooseLink(self): |
1060 | + if self.show_create_team_link: |
1061 | + return ('or (<a href="/people/+newteam">' |
1062 | 'Create a new team…</a>)') |
1063 | - return link |
1064 | + return None |
1065 | |
1066 | @property |
1067 | def nonajax_uri(self): |
1068 | @@ -251,10 +263,8 @@ |
1069 | >Register an external bug tracker…</a>) |
1070 | """ |
1071 | |
1072 | - def chooseLink(self): |
1073 | - link = super(BugTrackerPickerWidget, self).chooseLink() |
1074 | - link += self.link_template |
1075 | - return link |
1076 | + def extraChooseLink(self): |
1077 | + return self.link_template |
1078 | |
1079 | @property |
1080 | def nonajax_uri(self): |
1081 | |
1082 | === modified file 'lib/lp/app/widgets/tests/test_popup.py' |
1083 | --- lib/lp/app/widgets/tests/test_popup.py 2012-02-03 06:27:17 +0000 |
1084 | +++ lib/lp/app/widgets/tests/test_popup.py 2012-06-22 14:19:22 +0000 |
1085 | @@ -1,5 +1,6 @@ |
1086 | # Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
1087 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1088 | +from lp.services.features.testing import FeatureFixture |
1089 | |
1090 | __metaclass__ = type |
1091 | |
1092 | @@ -168,6 +169,26 @@ |
1093 | # But not the remove button. |
1094 | self.assertFalse(person_picker_widget.config['show_remove_button']) |
1095 | |
1096 | + def test_create_team_link(self): |
1097 | + # The person picker widget shows a create team link if the feature flag |
1098 | + # is on. |
1099 | + field = ITest['test_valid.item'] |
1100 | + bound_field = field.bind(self.context) |
1101 | + |
1102 | + with FeatureFixture( |
1103 | + {'disclosure.add-team-person-picker.enabled': 'true'}): |
1104 | + picker_widget = PersonPickerWidget( |
1105 | + bound_field, self.vocabulary, self.request) |
1106 | + picker_widget.show_create_team_link = True |
1107 | + self.assertTrue(picker_widget.config['show_create_team']) |
1108 | + self.assertTrue(picker_widget.config['enhanced_picker']) |
1109 | + |
1110 | + picker_widget = PersonPickerWidget( |
1111 | + bound_field, self.vocabulary, self.request) |
1112 | + picker_widget.show_create_team_link = True |
1113 | + self.assertFalse(picker_widget.config['show_create_team']) |
1114 | + self.assertFalse(picker_widget.config['enhanced_picker']) |
1115 | + |
1116 | def test_widget_personvalue_meta(self): |
1117 | # The person picker has the correct meta value for a person value. |
1118 | person = self.factory.makePerson() |
1119 | |
1120 | === modified file 'lib/lp/registry/browser/product.py' |
1121 | --- lib/lp/registry/browser/product.py 2012-06-04 11:41:47 +0000 |
1122 | +++ lib/lp/registry/browser/product.py 2012-06-22 14:19:22 +0000 |
1123 | @@ -938,7 +938,7 @@ |
1124 | self.context, IProduct['owner'], |
1125 | format_link(self.context.owner), |
1126 | header='Change maintainer', edit_view='+edit-people', |
1127 | - step_title='Select a new maintainer') |
1128 | + step_title='Select a new maintainer', show_create_team=True) |
1129 | |
1130 | @property |
1131 | def driver_widget(self): |
1132 | @@ -946,7 +946,7 @@ |
1133 | self.context, IProduct['driver'], |
1134 | format_link(self.context.driver, empty_value="Not yet selected"), |
1135 | header='Change driver', edit_view='+edit-people', |
1136 | - step_title='Select a new driver', |
1137 | + step_title='Select a new driver', show_create_team=True, |
1138 | null_display_value="Not yet selected", |
1139 | help_link="/+help-registry/driver.html") |
1140 | |
1141 | @@ -2269,11 +2269,11 @@ |
1142 | initial_values = {'transfer_to_registry': False} |
1143 | |
1144 | custom_widget('owner', PersonPickerWidget, header="Select the maintainer", |
1145 | - include_create_team_link=True) |
1146 | + show_create_team_link=True) |
1147 | custom_widget('transfer_to_registry', CheckBoxWidget, |
1148 | widget_class='field subordinate') |
1149 | custom_widget('driver', PersonPickerWidget, header="Select the driver", |
1150 | - include_create_team_link=True) |
1151 | + show_create_team_link=True) |
1152 | |
1153 | @property |
1154 | def page_title(self): |
1155 | |
1156 | === modified file 'lib/lp/services/features/flags.py' |
1157 | --- lib/lp/services/features/flags.py 2012-06-08 02:16:58 +0000 |
1158 | +++ lib/lp/services/features/flags.py 2012-06-22 14:19:22 +0000 |
1159 | @@ -223,6 +223,12 @@ |
1160 | '', |
1161 | '', |
1162 | ''), |
1163 | + ('disclosure.add-team-person-picker.enabled', |
1164 | + 'boolean', |
1165 | + 'Allows users to add a new team directly from the person picker.', |
1166 | + '', |
1167 | + '', |
1168 | + ''), |
1169 | ('bugs.autoconfirm.enabled_distribution_names', |
1170 | 'space delimited', |
1171 | ('Enables auto-confirming bugtasks for distributions (and their ' |
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? Node.getDOMNode (node.one( '[id=field. name]') ).value; one('[id= field.name] ').get( 'value' );
Y.
I expected
node.
On 237 I see an image being loaded when the sprite is already loaded and the padding does not match the image: "background- image: url(/@@/person); ',
'style=
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.