Merge lp:~danilo/launchpad/bugtask-index-portlet-setup into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 12391
Proposed branch: lp:~danilo/launchpad/bugtask-index-portlet-setup
Merge into: lp:launchpad
Diff against target: 2350 lines (+1161/-1113)
4 files modified
lib/lp/bugs/javascript/bugtask_index.js (+4/-1109)
lib/lp/bugs/javascript/bugtask_index_portlets.js (+1152/-0)
lib/lp/bugs/templates/bug-portlet-subscribers.pt (+3/-3)
lib/lp/bugs/templates/bugtask-index.pt (+2/-1)
To merge this branch: bzr merge lp:~danilo/launchpad/bugtask-index-portlet-setup
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+49818@code.launchpad.net

Commit message

[r=gmb][no-qa] Split portlet setup for subscriber widgets from bugtask-index.js.

Description of the change

= Split bugtask-index.js into two files =

Warning: this is a branch with a huge diff, but it's a move of ~1100 lines of JS from bugtask_index.js to a new bugtask_index_portlets.js. Changes other than the move are minimal.

At the moment, bugtask-index.js is a behemoth of >2k lines of JS, doing many unrelated things:
 - handling duplication
 - handling branch linking
 - handling "me too" functionality
 - handling subscription portlets

The idea is to eventually split it all up by functionality, but a first step is to start with setup_portlet_handlers and move that all out into a separate file. That includes duplication and subscribing portlets set-up.

Future branches will split duplication from subscription stuff.

== Pre-implementation notes ==

Talked this over with Graham who suggested starting with setup_portlet_handlers.

== Implementation details ==

Only two functions are namespaced using lp.bugs.bugtask_index.portlets, because they are the actual entry points. If it is agreed to namespace all the other methods, I'd kind of prefer to do it in a separate branch (to make it easier to bisect if any problems).

Rest of it is a simple code move from bugtask-index.js to bugtask-index-portlets.js.

Fixes in other parts of the tree are due to failing windmill tests.

I am not sure how to best figure out what is the minimum list of required modules for a certain module (I just copied the list over from bugtask_index.js), so if a reviewer has any suggestions, I'd be happy to hear them out.

== Tests ==

bin/test -cvvm lp.bugs --layer=WindmillLayer

== Demo and Q/A ==

Go to eg. https://bugs.launchpad.dev/tomcat/+bug/2 and try setting a bug as a duplicate of another bug (say number 3), subscribing and unsubscribing from a bug. Nothing should change compared to the existing behaviour.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/templates/bug-portlet-subscribers.pt
  lib/lp/bugs/javascript/bugtask_index_portlets.js
  lib/lp/bugs/javascript/bugtask_index.js

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

1184 +var use_advanced_subscriptions = false;

ha, so here's an interesting thing. that should be
`var namespace.use_advanced_subscriptions = false;`. i guess that it's
never been a problem because our code doesn't check for
namespace.use_advanced_subscriptions being undefined. you can get rid of
the xxx above this too since rob and i agreed it wasn't a bug after all.

you're also going to need to update the setting of
y.lp.bugs.bugtask_index.use_advanced_subscriptions in bugtask-index.pt
to make sure it uses the new namespace, otherwise all of the advanced
subscriptions functionality is going to break.

You can double-check the new functionality by running the following query and then checking that the "Subscribe / Edit subscription" link behaves like it does for you on production:

`insert into featureflag (scope, priority, flag, value, date_modified) values ('default', 1, 'malone.advanced-subscriptions.enabled', 'on', now() at time zone 'UTC');`

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
2--- lib/lp/bugs/javascript/bugtask_index.js 2011-02-10 09:52:06 +0000
3+++ lib/lp/bugs/javascript/bugtask_index.js 2011-02-15 18:19:47 +0000
4@@ -39,125 +39,9 @@
5 var privacy_spinner;
6 var link_branch_link;
7
8-// The set of subscriber CSS IDs as a JSON struct.
9-var subscriber_ids;
10-
11-// A boolean telling us whether advanced subscription features are to be
12-// used or not.
13-// XXX 2011-01-14 gmb bug=702859:
14-// We need to expose feature flags via the API to avoid this kind of
15-// thing.
16-var use_advanced_subscriptions = false;
17-var subscription_labels = Y.lp.bugs.subscriber.subscription_labels;
18-
19-/*
20- * An object representing the bugtask subscribers portlet.
21- *
22- * Since the portlet loads via XHR and inline subscribing
23- * depends on that portlet being loaded, setup a custom
24- * event object, to provide a hook for initializing subscription
25- * link callbacks after custom events.
26- */
27-var PortletTarget = function() {};
28-Y.augment(PortletTarget, Y.Event.Target);
29-namespace.portlet = new PortletTarget();
30-
31-function setup_portlet_handlers() {
32- namespace.portlet.subscribe('bugs:portletloaded', function() {
33- load_subscriber_ids();
34- });
35- namespace.portlet.subscribe('bugs:dupeportletloaded', function() {
36- setup_unsubscribe_icon_handlers();
37- });
38- /*
39- * If the subscribers portlet fails to load, clear any
40- * click handlers, so the normal subscribe page can be reached.
41- */
42- namespace.portlet.subscribe('bugs:portletloadfailed', function(handlers) {
43- if (Y.Lang.isArray(handlers)) {
44- var click_handler = handlers[0];
45- click_handler.detach();
46- }
47- });
48- /* If the dupe subscribers portlet fails to load,
49- * be sure to try to handle any unsub icons that may
50- * exist for others.
51- */
52- namespace.portlet.subscribe(
53- 'bugs:dupeportletloadfailed',
54- function(handlers) {
55- setup_unsubscribe_icon_handlers();
56- });
57-
58- /* If loading the subscriber IDs JSON has succeeded, set up the
59- * subscription link handlers and load the subscribers from dupes.
60- */
61- namespace.portlet.subscribe(
62- 'bugs:portletsubscriberidsloaded',
63- function() {
64- setup_subscription_link_handlers();
65- load_subscribers_from_duplicates();
66- });
67-
68- /* If loading the subscriber IDs JSON fails we still need to load the
69- * subscribers from duplicates but we don't set up the subscription link
70- * handlers.
71- */
72- namespace.portlet.subscribe(
73- 'bugs:portletsubscriberidsfailed',
74- function() {
75- load_subscribers_from_duplicates();
76- });
77-
78- /*
79- * Subscribing someone else requires loading a grayed out
80- * username into the DOM until the subscribe action completes.
81- * There are a couple XHR requests in check_can_be_unsubscribed
82- * before the subscribe work can be done, so fire a custom event
83- * bugs:nameloaded and do the work here when the event fires.
84- */
85- namespace.portlet.subscribe('bugs:nameloaded', function(subscription) {
86- var error_handler = new LP.client.ErrorHandler();
87- error_handler.clearProgressUI = function() {
88- var temp_link = Y.one('#temp-username');
89- if (temp_link) {
90- var temp_parent = temp_link.get('parentNode');
91- temp_parent.removeChild(temp_link);
92- }
93- };
94- error_handler.showError = function(error_msg) {
95- Y.lp.app.errors.display_error(
96- Y.one('.menu-link-addsubscriber'), error_msg);
97- };
98-
99- var config = {
100- on: {
101- success: function() {
102- var temp_link = Y.one('#temp-username');
103- var temp_spinner = Y.one('#temp-name-spinner');
104- temp_link.removeChild(temp_spinner);
105- var anim = Y.lazr.anim.green_flash({ node: temp_link });
106- anim.on('end', function() {
107- add_user_name_link(subscription);
108- var temp_parent = temp_link.get('parentNode');
109- temp_parent.removeChild(temp_link);
110- });
111- anim.run();
112- },
113- failure: error_handler.getFailureHandler()
114- },
115- parameters: {
116- person: LP.client.get_absolute_uri(
117- subscription.get('person').get('escaped_uri')),
118- suppress_notify: false
119- }
120- };
121- lp_client.named_post(bug_repr.self_link, 'subscribe', config);
122- });
123-}
124-
125 namespace.setup_bugtask_index = function() {
126- setup_portlet_handlers();
127+ Y.lp.bugs.bugtask_index.portlets.setup_portlet_handlers();
128+
129 /*
130 * Check the page for links related to overlay forms and request the HTML
131 * for these forms.
132@@ -275,203 +159,6 @@
133
134
135 /*
136- * Initialize click handler for the subscribe someone else link.
137- *
138- * @method setup_subscribe_someone_else_handler
139- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
140- */
141-function setup_subscribe_someone_else_handler(subscription) {
142- var config = {
143- header: 'Subscribe someone else',
144- step_title: 'Search',
145- picker_activator: '.menu-link-addsubscriber'
146- };
147-
148- config.save = function(result) {
149- subscribe_someone_else(result, subscription);
150- };
151- var picker = Y.lp.app.picker.create('ValidPersonOrTeam', config);
152-}
153-
154-
155-/*
156- * Handle the advanced_subscription_overlay's form submissions.
157- *
158- * @method handle_advanced_subscription_overlay
159- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
160- * @param form_data {Object} The data from the submitted form.
161- */
162-function handle_advanced_subscription_overlay(subscription, form_data) {
163- var link = subscription.get('link');
164- var link_parent = link.get('parentNode');
165- if (link_parent.hasClass('subscribed-false') &&
166- link_parent.hasClass('dup-subscribed-false')) {
167- // The user isn't subscribed, so subscribe them.
168- subscription.set(
169- 'bug_notification_level',
170- form_data['field.bug_notification_level']);
171- subscribe_current_user(subscription);
172- } else if (
173- form_data['field.subscription'] == 'update-subscription') {
174- // The user is already subscribed and wants to update their
175- // subscription.
176- setup_client_and_bug();
177- var person_name = subscription.get('person').get('name');
178- var subscription_url =
179- lp_bug_entry.get('self_link') + '/+subscription/' +
180- person_name;
181- config = {
182- on: {
183- success: function(lp_subscription) {
184- subscription.enable_spinner('Updating subscription...');
185- lp_subscription.set(
186- 'bug_notification_level',
187- form_data['field.bug_notification_level'][0])
188- save_config = {
189- on: {
190- success: function(e) {
191- subscription.disable_spinner(
192- 'Edit subscription');
193- var anim = Y.lazr.anim.green_flash({
194- node: link_parent
195- });
196- anim.run();
197- },
198- failure: function(e) {
199- subscription.disable_spinner(
200- 'Edit subscription');
201- var anim = Y.lazr.anim.red_flash({
202- node: link_parent
203- });
204- anim.run();
205- }
206- }
207- }
208- lp_subscription.lp_save(save_config);
209- }
210- }
211- }
212- lp_client.get(subscription_url, config);
213- } else {
214- // The user is already subscribed and wants to unsubscribe.
215- unsubscribe_current_user(subscription);
216- }
217-}
218-
219-
220-/*
221- * Create and return a FormOverlay for advanced subscription
222- * interactions.
223- *
224- * @method setup_advanced_subscription_overlay
225- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
226- */
227-function setup_advanced_subscription_overlay(subscription) {
228- var subscription_overlay = new Y.lazr.FormOverlay({
229- headerContent: '<h2>Subscribe to bug</h2>',
230- form_submit_button:
231- Y.Node.create(submit_button_html),
232- form_cancel_button:
233- Y.Node.create(cancel_button_html),
234- centered: true,
235- visible: false
236- });
237- subscription_overlay.set(
238- 'form_submit_callback', function(form_data) {
239- handle_advanced_subscription_overlay(subscription, form_data);
240- subscription_overlay.hide();
241- });
242-
243- var subscription_link_url = subscription.get(
244- 'link').get('href') + '/++form++';
245- subscription_overlay.loadFormContentAndRender(
246- subscription_link_url);
247- subscription_overlay.render('#privacy-form-container');
248- return subscription_overlay
249-}
250-
251-
252-/*
253- * Initialize callbacks for subscribe/unsubscribe links.
254- *
255- * @method setup_subscription_link_handlers
256- */
257-function setup_subscription_link_handlers() {
258- if (LP.client.links.me === undefined) {
259- return;
260- }
261-
262- setup_client_and_bug();
263- var subscription = new Y.lp.bugs.subscriber.Subscription({
264- link: Y.one('.menu-link-subscription'),
265- spinner: Y.one('#sub-unsub-spinner'),
266- subscriber: new Y.lp.bugs.subscriber.Subscriber({
267- uri: LP.client.links.me,
268- subscriber_ids: subscriber_ids
269- })
270- });
271-
272- var is_direct = subscription.get(
273- 'link').get('parentNode').hasClass('subscribed-true');
274- var has_dupes = subscription.get(
275- 'link').get('parentNode').hasClass('dup-subscribed-true');
276- subscription.set('is_direct', is_direct);
277- subscription.set('has_dupes', has_dupes);
278-
279- if (subscription.is_node()) {
280- subscription.get('link').on('click', function(e) {
281- e.halt();
282- subscription.set('can_be_unsubscribed', true);
283- subscription.set('person', subscription.get('subscriber'));
284- subscription.set('is_team', false);
285- var parent = e.target.get('parentNode');
286- if (namespace.use_advanced_subscriptions) {
287- var subscription_overlay =
288- setup_advanced_subscription_overlay(subscription);
289- subscription_overlay.show();
290- } else {
291- // Look for the false conditions of subscription, which
292- // is_direct_subscription, etc. don't report correctly,
293- // to make sure we only use subscribe_current_user for
294- // the current user.
295- if (parent.hasClass('subscribed-false') &&
296- parent.hasClass('dup-subscribed-false')) {
297- subscribe_current_user(subscription);
298- }
299- else {
300- unsubscribe_current_user(subscription);
301- }
302- }
303- });
304- subscription.get('link').addClass('js-action');
305- }
306-
307- setup_subscribe_someone_else_handler(subscription);
308-}
309-
310-/*
311- * Set click handlers for unsubscribe remove icons.
312- *
313- * @method setup_unsubscribe_icon_handlers
314- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
315- */
316-function setup_unsubscribe_icon_handlers() {
317- var subscription = new Y.lp.bugs.subscriber.Subscription({
318- link: Y.one('.menu-link-subscription'),
319- spinner: Y.one('#sub-unsub-spinner'),
320- subscriber: new Y.lp.bugs.subscriber.Subscriber({
321- uri: LP.client.links.me,
322- subscriber_ids: subscriber_ids
323- })
324- });
325-
326- Y.on('click', function(e) {
327- e.halt();
328- unsubscribe_user_via_icon(e.target, subscription);
329- }, '.unsub-icon');
330-}
331-
332-/*
333 * Create the lp client and bug entry if we haven't done so already.
334 *
335 * @method setup_client_and_bug
336@@ -850,594 +537,6 @@
337 Y.fire('lp:branch-linked', bug_branch_node);
338 }
339
340-/*
341- * Traverse the DOM of a given remove icon to find
342- * the user's link. Returns a URI of the form "/~username".
343- *
344- * @method get_user_uri_from_icon
345- * @param icon {Node} The node representing a remove icon.
346- * @return user_uri {String} The user's uri, without the hostname.
347- */
348-function get_user_uri_from_icon(icon) {
349- var parent_div = icon.get('parentNode').get('parentNode');
350- // This should be parent_div.firstChild, but because of #text
351- // and cross-browser issues, using the YUI query syntax is
352- // safer here.
353- var user_uri = parent_div.one('a').getAttribute('href');
354-
355- // Strip the domain off. We just want a path.
356- var host_start = user_uri.indexOf('//');
357- if (host_start != -1) {
358- var host_end = user_uri.indexOf('/', host_start+2);
359- return user_uri.substring(host_end, user_uri.length);
360- }
361-
362- return user_uri;
363-}
364-
365-
366-/*
367- * Build the HTML for a user link for the subscribers list.
368- *
369- * @method build_user_link_html
370- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
371- * @return html {String} The HTML used for creating a subscriber link.
372- */
373-function build_user_link_html(subscription) {
374- var name = subscription.get('person').get('name');
375- var css_name = subscription.get('person').get('css_name');
376- var full_name = subscription.get('person').get('full_display_name');
377- // Be paranoid about display_name, since timeouts or other errors
378- // could mean display_name wasn't set on initialization.
379- if (subscription.get('person').get('display_name') === '') {
380- subscription.get('person').set_display_name();
381- }
382- var display_name = subscription.get('person').get('display_name');
383- var terms = {
384- name: name,
385- css_name: css_name,
386- display_name: display_name,
387- full_name: full_name
388- };
389-
390- if (subscription.is_current_user_subscribing()) {
391- terms.subscribed_by = 'themselves';
392- } else {
393- terms.subscribed_by = 'by ' + full_name;
394- }
395-
396- var html = Y.Node.create('<div><a></a></div>');
397- html.addClass(terms.css_name);
398-
399- if (subscription.is_direct_subscription()) {
400- html.set('id', 'direct-' + terms.css_name);
401- } else {
402- html.set('id', 'dupe-' + terms.css_name);
403- }
404-
405- html.one('a')
406- .set('href', '/~' + terms.name)
407- .set('name', terms.full_name)
408- .set('title', 'Subscribed ' + terms.subscribed_by);
409-
410- var span;
411- if (subscription.is_team()) {
412- span = '<span class="sprite team"></span>';
413- } else {
414- span = '<span class="sprite person"></span>';
415- }
416-
417- html.one('a')
418- .appendChild(Y.Node.create(span))
419- .appendChild(document.createTextNode(terms.display_name));
420-
421- // Add remove icon if the current user can unsubscribe the subscriber.
422- if (subscription.can_be_unsubscribed_by_user()) {
423- var icon_html = Y.Node.create(
424- '<a href="+subscribe">' +
425- '<img class="unsub-icon" src="/@@/remove" alt="Remove" /></a>');
426- icon_html
427- .set('id', 'unsubscribe-' + terms.css_name)
428- .set('title', 'Unsubscribe ' + terms.full_name);
429- icon_html.one('img')
430- .set('id', 'unsubscribe-icon-' + terms.css_name);
431- html.appendChild(icon_html);
432- }
433-
434- return html;
435-}
436-
437-/*
438- * Used to remove the user's name from the subscriber's list.
439- *
440- * @method remove_user_name_link
441- * @param user_node {Node} Node representing the user name link.
442- */
443-function remove_user_name_link(user_node) {
444- var parent = user_node.get('parentNode');
445- parent.removeChild(user_node);
446-}
447-
448-/*
449- * Returns the next node in alphabetical order after the subscriber
450- * node now being added. No node is returned to append to end of list.
451- *
452- * The name can appear in one of two different lists. 1) The list of
453- * subscribers that can be unsubscribed by the current user, and
454- * 2) the list of subscribers that cannont be unsubscribed.
455- *
456- * @method get_next_subscriber_node
457- * @param subscription_link {Node} The sub/unsub link.
458- * @return {Node} The node appearing next in the subscriber list or
459- * undefined if no node is next.
460- */
461-function get_next_subscriber_node(subscription) {
462- var full_name = subscription.get('person').get('full_display_name');
463- var can_be_unsubscribed = subscription.can_be_unsubscribed_by_user();
464- var nodes_by_name = {};
465- var unsubscribables = [];
466- var not_unsubscribables = [];
467-
468- // Use the list of subscribers pulled from the DOM to have sortable
469- // lists of unsubscribable vs. not unsubscribale person links.
470- var all_subscribers = Y.all('#subscribers-links div');
471- if (all_subscribers.size() > 0) {
472- all_subscribers.each(function(sub_link) {
473- if (sub_link.getAttribute('id') != 'temp-username') {
474- // User's displayname is found via the link's "name"
475- // attribute.
476- var sub_link_name = sub_link.one('a').getAttribute('name');
477- nodes_by_name[sub_link_name] = sub_link;
478- if (sub_link.one('img.unsub-icon')) {
479- unsubscribables.push(sub_link_name);
480- } else {
481- not_unsubscribables.push(sub_link_name);
482- }
483- }
484- });
485-
486- // Add the current subscription.
487- if (can_be_unsubscribed) {
488- unsubscribables.push(full_name);
489- } else {
490- not_unsubscribables.push(full_name);
491- }
492- unsubscribables.sort();
493- not_unsubscribables.sort();
494- } else {
495- // If there is no all_subscribers, then we're dealing with
496- // the printed None, so return.
497- return;
498- }
499-
500- var i;
501- if ((!unsubscribables && !not_unsubscribables) ||
502- // If A) neither list exists, B) the user belongs in the second
503- // list but the second list doesn't exist, or C) user belongs in the
504- // first list and the second doesn't exist, return no node to append.
505- (!can_be_unsubscribed && !not_unsubscribables) ||
506- (can_be_unsubscribed && unsubscribables && !not_unsubscribables)) {
507- return;
508- } else if (
509- // If the user belongs in the first list, and the first list
510- // doesn't exist, but the second one does, return the first node
511- // in the second list.
512- can_be_unsubscribed && !unsubscribables && not_unsubscribables) {
513- return nodes_by_name[not_unsubscribables[0]];
514- } else if (can_be_unsubscribed) {
515- // If the user belongs in the first list, loop the list for position.
516- for (i=0; i<unsubscribables.length; i++) {
517- if (unsubscribables[i] == full_name) {
518- if (i+1 < unsubscribables.length) {
519- return nodes_by_name[unsubscribables[i+1]];
520- // If the current link should go at the end of the first
521- // list and we're at the end of that list, return the
522- // first node of the second list. Due to earlier checks
523- // we can be sure this list exists.
524- } else if (i+1 >= unsubscribables.length) {
525- return nodes_by_name[not_unsubscribables[0]];
526- }
527- }
528- }
529- } else if (!can_be_unsubscribed) {
530- // If user belongs in the second list, loop the list for position.
531- for (i=0; i<not_unsubscribables.length; i++) {
532- if (not_unsubscribables[i] == full_name) {
533- if (i+1 < not_unsubscribables.length) {
534- return nodes_by_name[not_unsubscribables[i+1]];
535- } else {
536- return;
537- }
538- }
539- }
540- }
541-}
542-
543-/*
544- * Add the user name to the subscriber's list.
545- *
546- * @method add_user_name_link
547- */
548-function add_user_name_link(subscription) {
549- var person = subscription.get('person');
550- var link_node = build_user_link_html(subscription);
551- var subscribers = Y.one('#subscribers-links');
552- if (subscription.is_current_user_subscribing()) {
553- // If this is the current user, then top post the name and be done.
554- subscribers.insertBefore(link_node, subscribers.get('firstChild'));
555- } else {
556- var next = get_next_subscriber_node(subscription);
557- if (next) {
558- subscribers.insertBefore(link_node, next);
559- } else {
560- // Handle the case of the displayed "None".
561- var none_subscribers = Y.one('#none-subscribers');
562- if (none_subscribers) {
563- var none_parent = none_subscribers.get('parentNode');
564- none_parent.removeChild(none_subscribers);
565- }
566- subscribers.appendChild(link_node);
567- }
568- }
569-
570- // Set the click handler if adding a remove icon.
571- if (subscription.can_be_unsubscribed_by_user()) {
572- var remove_icon =
573- Y.one('#unsubscribe-icon-' + person.get('css_name'));
574- remove_icon.on('click', function(e) {
575- e.halt();
576- unsubscribe_user_via_icon(e.target, subscription);
577- });
578- }
579-}
580-
581-/*
582- * Add a grayed out, temporary user name when subscribing
583- * someone else.
584- *
585- * @method add_temp_user_name
586- * @param subscription_link {Node} The sub/unsub link.
587- */
588-function add_temp_user_name(subscription) {
589- // Be paranoid about display_name, since timeouts or other errors
590- // could mean display_name wasn't set on initialization.
591- if (subscription.get('person').get('display_name') === '') {
592- subscription.get('person').set_display_name();
593- }
594- var display_name = subscription.get('person').get('display_name');
595- var img_src;
596- if (subscription.is_team()) {
597- img_src = '/@@/teamgray';
598- } else {
599- img_src = '/@@/persongray';
600- }
601-
602- // The <span>...</span> below must *not* be <span/>. On FF (maybe
603- // others, but at least on FF 3.0.11) will then not notice any
604- // following sibling nodes, like the spinner image.
605- var link_node = Y.Node.create([
606- '<div id="temp-username"> ',
607- ' <img alt="" width="14" height="14" />',
608- ' <span>Other Display Name</span>',
609- ' <img id="temp-name-spinner" src="/@@/spinner" alt="" ',
610- ' style="position:absolute;right:8px" /></div>'].join(''));
611- link_node.one('img').set('src', img_src);
612- link_node.replaceChild(
613- document.createTextNode(display_name),
614- link_node.one('span'));
615-
616- var subscribers = Y.one('#subscribers-links');
617- var next = get_next_subscriber_node(subscription);
618- if (next) {
619- subscribers.insertBefore(link_node, next);
620- } else {
621- // Handle the case of the displayed "None".
622- var none_subscribers = Y.one('#none-subscribers');
623- if (none_subscribers) {
624- var none_parent = none_subscribers.get('parentNode');
625- none_parent.removeChild(none_subscribers);
626- }
627- subscribers.appendChild(link_node);
628- }
629-
630- // Fire a custom event to know it's safe to begin
631- // any actual subscribing work.
632- namespace.portlet.fire('bugs:nameloaded', subscription);
633-}
634-
635-/*
636- * Add the "None" div to the subscribers list if
637- * there aren't any subscribers left.
638- *
639- * @method set_none_for_empty_subscribers
640- */
641-function set_none_for_empty_subscribers() {
642- var subscriber_list = Y.one('#subscribers-links');
643- // Assume if subscriber_list has no child divs
644- // then the list of subscribers is empty.
645- if (!Y.Lang.isValue(subscriber_list.one('div')) &&
646- !Y.Lang.isValue(Y.one('#none-subscribers'))) {
647- var none_div = Y.Node.create('<div id="none-subscribers">None</div>');
648- subscriber_list.appendChild(none_div);
649- }
650-
651- // Clear the empty duplicate subscribers list if it exists.
652- var dup_list = Y.one('#subscribers-from-duplicates');
653- if (Y.Lang.isValue(dup_list) &&
654- !Y.Lang.isValue(dup_list.one('div'))) {
655- var parent = dup_list.get('parentNode');
656- parent.removeChild(dup_list);
657- }
658-}
659-
660-/*
661- * Set the class on subscription link's parentNode.
662- *
663- * This is used to reset the class used by the
664- * click handler to know which link was clicked.
665- *
666- * @method set_subscription_link_parent_class
667- * @param subscription_link {Node} The sub/unsub link.
668- * @param subscribed {Boolean} The sub/unsub'ed flag for the class.
669- * @param dupe_subscribed {Boolean} The sub/unsub'ed flag for dupes
670- * on the class.
671- */
672-function set_subscription_link_parent_class(
673- user_link, subscribed, dupe_subscribed) {
674-
675- var parent = user_link.get('parentNode');
676- if (subscribed) {
677- parent.removeClass('subscribed-false');
678- parent.addClass('subscribed-true');
679- } else {
680- parent.removeClass('subscribed-true');
681- parent.addClass('subscribed-false');
682- }
683-
684- if (dupe_subscribed) {
685- parent.removeClass('dup-subscribed-false');
686- parent.addClass('dup-subscribed-true');
687- } else {
688- parent.removeClass('dup-subscribed-true');
689- parent.addClass('dup-subscribed-false');
690- }
691-}
692-
693-/*
694- * Unsubscribe a user from this bugtask when a remove icon is clicked.
695- *
696- * @method unsubscribe_user_via_icon
697- * @param icon {Node} The remove icon that was clicked.
698- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
699-*/
700-function unsubscribe_user_via_icon(icon, subscription) {
701- icon.set('src', '/@@/spinner');
702- var icon_parent = icon.get('parentNode');
703-
704- var user_uri = get_user_uri_from_icon(icon);
705- var person = new Y.lp.bugs.subscriber.Subscriber({
706- uri: user_uri,
707- subscriber_ids: subscriber_ids
708- });
709- subscription.set('person', person);
710-
711- // Determine if this is a dupe.
712- var is_dupe;
713- var icon_parent_div = icon_parent.get('parentNode');
714- var dupe_id = 'dupe-' + person.get('css_name');
715- if (icon_parent_div.get('id') == dupe_id) {
716- is_dupe = true;
717- } else {
718- is_dupe = false;
719- }
720-
721- var error_handler = new LP.client.ErrorHandler();
722- error_handler.clearProgressUI = function () {
723- icon.set('src', '/@@/remove');
724- // Grab the icon again to reset to click handler.
725- var unsubscribe_icon = Y.one(
726- '#unsubscribe-icon-' + person.get('css_name'));
727- unsubscribe_icon.on('click', function(e) {
728- e.halt();
729- unsubscribe_user_via_icon(e.target, subscription);
730- });
731-
732- };
733- error_handler.showError = function (error_msg) {
734- var flash_node = Y.one('.' + person.get('css_name'));
735- Y.lp.app.errors.display_error(flash_node, error_msg);
736-
737- };
738-
739- var subscription_link = subscription.get('link');
740- var config = {
741- on: {
742- success: function(client) {
743- icon_parent.removeChild(icon);
744- var anim = Y.lazr.anim.green_flash({ node: icon_parent_div });
745- anim.on('end', function(e) {
746- remove_user_name_link(icon_parent_div);
747- set_none_for_empty_subscribers();
748- var person_link = Y.one('.' + person.get('css_name'));
749- if (Y.Lang.isNull(person_link) &&
750- subscription.is_current_user_subscribing()) {
751- // Current user has been completely unsubscribed.
752- subscription.disable_spinner(
753- subscription_labels.SUBSCRIBE);
754- set_subscription_link_parent_class(
755- subscription_link, false, false);
756- subscription.set('is_direct', false);
757- subscription.set('has_dupes', false);
758- } else {
759- if (is_dupe) {
760- // A direct subscription remains.
761- set_subscription_link_parent_class(
762- subscription_link, true, false);
763- subscription.set('is_direct', true);
764- subscription.set('has_dupes', false);
765- } else {
766- // A dupe subscription remains.
767- set_subscription_link_parent_class(
768- subscription_link, false, true);
769- subscription.set('is_direct', false);
770- subscription.set('has_dupes', true);
771- }
772- }
773- });
774- anim.run();
775- },
776-
777- failure: error_handler.getFailureHandler()
778- }
779- };
780-
781- if (!subscription.is_current_user_subscribing()) {
782- config.parameters = {
783- person: LP.client.get_absolute_uri(user_uri)
784- };
785- }
786-
787- if (is_dupe) {
788- lp_client.named_post(
789- bug_repr.self_link, 'unsubscribeFromDupes', config);
790- } else {
791- lp_client.named_post(bug_repr.self_link, 'unsubscribe', config);
792- }
793-}
794-
795-/*
796- * Subscribe the current user via the LP API.
797- *
798- * @method subscribe_current_user
799- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
800- */
801-function subscribe_current_user(subscription) {
802- subscription.enable_spinner('Subscribing...');
803- var subscription_link = subscription.get('link');
804- var subscriber = subscription.get('subscriber');
805- var bug_notification_level = subscription.get('bug_notification_level');
806-
807- var error_handler = new LP.client.ErrorHandler();
808- error_handler.clearProgressUI = function () {
809- subscription.disable_spinner();
810- };
811- error_handler.showError = function (error_msg) {
812- Y.lp.app.errors.display_error(subscription_link, error_msg);
813- };
814-
815- var config = {
816- on: {
817- success: function(client) {
818- if (namespace.use_advanced_subscriptions) {
819- subscription.disable_spinner(
820- subscription_labels.EDIT);
821- } else {
822- subscription.disable_spinner(
823- subscription_labels.UNSUBSCRIBE);
824- }
825-
826- if (subscription.has_duplicate_subscriptions()) {
827- set_subscription_link_parent_class(
828- subscription_link, true, true);
829- } else {
830- set_subscription_link_parent_class(
831- subscription_link, true, false);
832- }
833-
834- // Handle the case where the subscriber's list displays
835- // "None".
836- var empty_subscribers = Y.one("#none-subscribers");
837- if (empty_subscribers) {
838- var parent = empty_subscribers.get('parentNode');
839- parent.removeChild(empty_subscribers);
840- }
841-
842- add_user_name_link(subscription);
843-
844- var flash_node = Y.one('.' + subscriber.get('css_name'));
845- var anim = Y.lazr.anim.green_flash({ node: flash_node });
846- anim.run();
847- },
848-
849- failure: error_handler.getFailureHandler()
850- },
851-
852- parameters: {
853- person: LP.client.get_absolute_uri(subscriber.get('escaped_uri')),
854- suppress_notify: false,
855- level: bug_notification_level
856- }
857- };
858- lp_client.named_post(bug_repr.self_link, 'subscribe', config);
859-}
860-
861-/*
862- * Unsubscribe the current user via the LP API.
863- *
864- * @method unsubscribe_current_user
865- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
866- */
867-function unsubscribe_current_user(subscription) {
868- subscription.enable_spinner('Unsubscribing...');
869- var subscription_link = subscription.get('link');
870- var subscriber = subscription.get('subscriber');
871-
872- var error_handler = new LP.client.ErrorHandler();
873- error_handler.clearProgressUI = function () {
874- subscription.disable_spinner();
875- };
876- error_handler.showError = function (error_msg) {
877- Y.lp.app.errors.display_error(subscription_link, error_msg);
878- };
879-
880- var config = {
881- on: {
882- success: function(client) {
883- if (subscription.is_direct_subscription() &&
884- subscription.has_duplicate_subscriptions()) {
885- // Don't change the 'Unsubscribe' text if
886- // dupe subscriptions remain.
887- subscription.disable_spinner();
888- set_subscription_link_parent_class(
889- subscription_link, false, true);
890- subscription.set('is_direct', false);
891- } else if (subscription.is_direct_subscription() &&
892- !subscription.has_duplicate_subscriptions()) {
893- // Only unsub'ing a direct subscriber here.
894- subscription.disable_spinner(
895- subscription_labels.SUBSCRIBE);
896- set_subscription_link_parent_class(
897- subscription_link, false, false);
898- subscription.set('is_direct', false);
899- } else {
900- // Only unsub'ing dupes here.
901- subscription.disable_spinner(
902- subscription_labels.SUBSCRIBE);
903- set_subscription_link_parent_class(
904- subscription_link, false, false);
905- subscription.set('has_dupes', false);
906- }
907-
908- var flash_node = Y.one('.' + subscriber.get('css_name'));
909- var anim = Y.lazr.anim.green_flash({ node: flash_node });
910- anim.on('end', function(e) {
911- remove_user_name_link(flash_node);
912- set_none_for_empty_subscribers();
913- });
914- anim.run();
915- },
916-
917- failure: error_handler.getFailureHandler()
918- }
919- };
920- if (subscription.is_direct_subscription()) {
921- lp_client.named_post(bug_repr.self_link, 'unsubscribe', config);
922- } else {
923- lp_client.named_post(
924- bug_repr.self_link, 'unsubscribeFromDupes', config);
925- }
926-}
927-
928
929 /**
930 * Set up a bug task table row.
931@@ -1815,106 +914,6 @@
932 LP.client.cache.bug.self_link, 'markUserAffected', config);
933 }
934 });
935-
936-/*
937- * Check if the current user can unsubscribe the person
938- * being subscribed.
939- *
940- * This must be done in JavaScript, since the subscription
941- * hasn't completed yet, and so, can_be_unsubscribed_by_user
942- * cannot be used.
943- *
944- * @method check_can_be_unsubscribed
945- * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
946- */
947-function check_can_be_unsubscribed(subscription) {
948- var error_handler = new LP.client.ErrorHandler();
949- error_handler.showError = function (error_msg) {
950- Y.lp.app.errors.display_error(
951- Y.one('.menu-link-addsubscriber'), error_msg);
952- };
953-
954- var config = {
955- on: {
956- success: function(result) {
957- var is_team = result.get('is_team');
958- subscription.set('is_team', is_team);
959- var final_config = {
960- on: {
961- success: function(result) {
962- var team_member = false;
963- for (var i=0; i<result.entries.length; i++) {
964- if (result.entries[i].member_link ==
965- LP.client.get_absolute_uri(
966- subscription.get(
967- 'subscriber').get('uri'))) {
968- team_member = true;
969- }
970- }
971-
972- if (team_member) {
973- subscription.set('can_be_unsubscribed', true);
974- add_temp_user_name(subscription);
975- } else {
976- subscription.set(
977- 'can_be_unsubscribed', false);
978- add_temp_user_name(subscription);
979- }
980- },
981-
982- failure: error_handler.getFailureHandler()
983- }
984- };
985-
986- if (is_team) {
987- // Get a list of members to see if current user
988- // is a team member.
989- var members = result.get(
990- 'members_details_collection_link');
991- lp_client.get(members, final_config);
992- } else {
993- subscription.set('can_be_unsubscribed', false);
994- add_temp_user_name(subscription);
995- }
996- },
997-
998- failure: error_handler.getFailureHandler()
999- }
1000- };
1001- lp_client.get(LP.client.get_absolute_uri(
1002- subscription.get('person').get('escaped_uri')), config);
1003-}
1004-
1005-/*
1006- * Subscribe a person or team other than the current user.
1007- * This is a callback for the subscribe someone else picker.
1008- *
1009- * @method subscribe_someone_else
1010- * @result {Object} The object representing a person returned by the API.
1011- */
1012-function subscribe_someone_else(result, subscription) {
1013- var person = new Y.lp.bugs.subscriber.Subscriber({
1014- uri: result.api_uri,
1015- display_name: result.title,
1016- subscriber_ids: subscriber_ids
1017- });
1018- subscription.set('person', person);
1019-
1020- var error_handler = new LP.client.ErrorHandler();
1021- error_handler.showError = function(error_msg) {
1022- Y.lp.app.errors.display_error(
1023- Y.one('.menu-link-addsubscriber'), error_msg);
1024- };
1025-
1026- if (subscription.is_already_subscribed()) {
1027- error_handler.showError(
1028- subscription.get('person').get('full_display_name') +
1029- ' has already been subscribed');
1030- } else {
1031- check_can_be_unsubscribed(subscription);
1032- }
1033-}
1034-
1035 /*
1036 * Click handling to pass comment text to the attachment
1037 * page if there is a comment.
1038@@ -1935,114 +934,10 @@
1039 });
1040 }
1041
1042-function load_subscribers_from_duplicates() {
1043- if (Y.UA.ie) {
1044- return null;
1045- }
1046-
1047- Y.one('#subscribers-portlet-dupe-spinner').setStyle(
1048- 'display', 'block');
1049-
1050- function hide_spinner() {
1051- Y.one('#subscribers-portlet-dupe-spinner').setStyle(
1052- 'display', 'none');
1053- // Fire a custom event to signal failure, so that
1054- // any remaining unsub icons can be hooked up.
1055- namespace.portlet.fire('bugs:dupeportletloadfailed');
1056- }
1057-
1058- function on_success(transactionid, response, args) {
1059- hide_spinner();
1060-
1061- var dupe_subscribers_container = Y.one(
1062- '#subscribers-from-duplicates-container');
1063- dupe_subscribers_container.set(
1064- 'innerHTML',
1065- dupe_subscribers_container.get('innerHTML') +
1066- response.responseText);
1067-
1068- // Fire a custom portlet loaded event to notify when
1069- // it's safe to setup dupe subscriber link callbacks.
1070- namespace.portlet.fire('bugs:dupeportletloaded');
1071- }
1072-
1073- var config = {on: {success: on_success,
1074- failure: hide_spinner}};
1075- var url = Y.one(
1076- '#subscribers-from-dupes-content-link').getAttribute(
1077- 'href').replace('bugs.', '');
1078- Y.io(url, config);
1079-}
1080-
1081-namespace.load_subscribers_portlet = function(
1082- subscription_link, subscription_link_handler) {
1083- if (Y.UA.ie) {
1084- return null;
1085- }
1086-
1087- Y.one('#subscribers-portlet-spinner').setStyle('display', 'block');
1088-
1089- function hide_spinner() {
1090- Y.one('#subscribers-portlet-spinner').setStyle('display', 'none');
1091- // Fire a custom event to notify that the initial click
1092- // handler on subscription_link set above should be
1093- // cleared.
1094- if (namespace) {
1095- namespace.portlet.fire(
1096- 'bugs:portletloadfailed', subscription_link_handler);
1097- }
1098- }
1099-
1100- function setup_portlet(transactionid, response, args) {
1101- hide_spinner();
1102- var portlet = Y.one('#portlet-subscribers');
1103- portlet.set('innerHTML',
1104- portlet.get('innerHTML') + response.responseText);
1105-
1106- // Fire a custom portlet loaded event to notify when
1107- // it's safe to setup subscriber link callbacks.
1108- namespace.portlet.fire('bugs:portletloaded');
1109- }
1110-
1111- var config = {on: {success: setup_portlet,
1112- failure: hide_spinner}};
1113- var url = Y.one(
1114- '#subscribers-content-link').getAttribute('href').replace(
1115- 'bugs.', '');
1116- Y.io(url, config);
1117-};
1118-
1119-function load_subscriber_ids() {
1120- function on_success(transactionid, response, args) {
1121- try {
1122- subscriber_ids = Y.JSON.parse(response.responseText);
1123-
1124- // Fire a custom event to trigger the setting-up of the
1125- // subscription handlers.
1126- namespace.portlet.fire('bugs:portletsubscriberidsloaded');
1127- } catch (e) {
1128- // Fire an event to signal failure. This ensures that the
1129- // subscribers-from-dupes still get loaded into the portlet.
1130- namespace.portlet.fire('bugs:portletsubscriberidsfailed');
1131- }
1132- }
1133-
1134- function on_failure() {
1135- // Fire an event to signal failure. This ensures that the
1136- // subscribers-from-dupes still get loaded into the portlet.
1137- namespace.portlet.fire('bugs:portletsubscriberidsfailed');
1138- }
1139-
1140- var config = {on: {success: on_success,
1141- failure: on_failure}};
1142- var url = Y.one(
1143- '#subscribers-ids-link').getAttribute('href');
1144- Y.io(url, config);
1145-}
1146
1147 }, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
1148 "json-parse", "substitute", "widget-position-ext",
1149 "lazr.formoverlay", "lazr.anim", "lazr.base",
1150 "lazr.overlay", "lazr.choiceedit", "lp.app.picker",
1151- "lp.client.plugins", "lp.bugs.subscriber",
1152- "lp.app.errors"]});
1153+ "lp.client.plugins", "lp.bugs.bugtask_index.portlets",
1154+ "lp.bugs.subscriber", "lp.app.errors"]});
1155
1156=== added file 'lib/lp/bugs/javascript/bugtask_index_portlets.js'
1157--- lib/lp/bugs/javascript/bugtask_index_portlets.js 1970-01-01 00:00:00 +0000
1158+++ lib/lp/bugs/javascript/bugtask_index_portlets.js 2011-02-15 18:19:47 +0000
1159@@ -0,0 +1,1152 @@
1160+/* Copyright 2011 Canonical Ltd. This software is licensed under the
1161+ * GNU Affero General Public License version 3 (see the file LICENSE).
1162+ *
1163+ * Form overlay widgets and subscriber handling for bug pages.
1164+ *
1165+ * @module bugs
1166+ * @submodule bugtask_index.portlets
1167+ */
1168+
1169+YUI.add('lp.bugs.bugtask_index.portlets', function(Y) {
1170+
1171+var namespace = Y.namespace('lp.bugs.bugtask_index.portlets');
1172+
1173+// The launchpad js client used.
1174+var lp_client;
1175+
1176+// The launchpad client entry for the current bug.
1177+var lp_bug_entry;
1178+
1179+// The bug itself, taken from cache.
1180+var bug_repr;
1181+
1182+// A boolean telling us whether advanced subscription features are to be
1183+// used or not.
1184+var use_advanced_subscriptions = false;
1185+
1186+var subscription_labels = Y.lp.bugs.subscriber.subscription_labels;
1187+
1188+submit_button_html =
1189+ '<button type="submit" name="field.actions.change" ' +
1190+ 'value="Change" class="lazr-pos lazr-btn" >OK</button>';
1191+cancel_button_html =
1192+ '<button type="button" name="field.actions.cancel" ' +
1193+ 'class="lazr-neg lazr-btn" >Cancel</button>';
1194+
1195+// The set of subscriber CSS IDs as a JSON struct.
1196+var subscriber_ids;
1197+
1198+/*
1199+ * An object representing the bugtask subscribers portlet.
1200+ *
1201+ * Since the portlet loads via XHR and inline subscribing
1202+ * depends on that portlet being loaded, setup a custom
1203+ * event object, to provide a hook for initializing subscription
1204+ * link callbacks after custom events.
1205+ */
1206+var PortletTarget = function() {};
1207+Y.augment(PortletTarget, Y.Event.Target);
1208+namespace.portlet = new PortletTarget();
1209+
1210+/*
1211+ * Create the lp client and bug entry if we haven't done so already.
1212+ *
1213+ * @method setup_client_and_bug
1214+ */
1215+function setup_client_and_bug() {
1216+ lp_client = new LP.client.Launchpad();
1217+
1218+ if (bug_repr === undefined) {
1219+ bug_repr = LP.client.cache.bug;
1220+ lp_bug_entry = new LP.client.Entry(
1221+ lp_client, bug_repr, bug_repr.self_link);
1222+ }
1223+}
1224+
1225+namespace.load_subscribers_portlet = function(
1226+ subscription_link, subscription_link_handler) {
1227+ if (Y.UA.ie) {
1228+ return null;
1229+ }
1230+
1231+ Y.one('#subscribers-portlet-spinner').setStyle('display', 'block');
1232+
1233+ function hide_spinner() {
1234+ Y.one('#subscribers-portlet-spinner').setStyle('display', 'none');
1235+ // Fire a custom event to notify that the initial click
1236+ // handler on subscription_link set above should be
1237+ // cleared.
1238+ if (namespace) {
1239+ namespace.portlet.fire(
1240+ 'bugs:portletloadfailed', subscription_link_handler);
1241+ }
1242+ }
1243+
1244+ function setup_portlet(transactionid, response, args) {
1245+ hide_spinner();
1246+ var portlet = Y.one('#portlet-subscribers');
1247+ portlet.set('innerHTML',
1248+ portlet.get('innerHTML') + response.responseText);
1249+
1250+ // Fire a custom portlet loaded event to notify when
1251+ // it's safe to setup subscriber link callbacks.
1252+ namespace.portlet.fire('bugs:portletloaded');
1253+ }
1254+
1255+ var config = {on: {success: setup_portlet,
1256+ failure: hide_spinner}};
1257+ var url = Y.one(
1258+ '#subscribers-content-link').getAttribute('href').replace(
1259+ 'bugs.', '');
1260+ Y.io(url, config);
1261+};
1262+
1263+
1264+namespace.setup_portlet_handlers = function() {
1265+ namespace.portlet.subscribe('bugs:portletloaded', function() {
1266+ load_subscriber_ids();
1267+ });
1268+ namespace.portlet.subscribe('bugs:dupeportletloaded', function() {
1269+ setup_unsubscribe_icon_handlers();
1270+ });
1271+ /*
1272+ * If the subscribers portlet fails to load, clear any
1273+ * click handlers, so the normal subscribe page can be reached.
1274+ */
1275+ namespace.portlet.subscribe('bugs:portletloadfailed', function(handlers) {
1276+ if (Y.Lang.isArray(handlers)) {
1277+ var click_handler = handlers[0];
1278+ click_handler.detach();
1279+ }
1280+ });
1281+ /* If the dupe subscribers portlet fails to load,
1282+ * be sure to try to handle any unsub icons that may
1283+ * exist for others.
1284+ */
1285+ namespace.portlet.subscribe(
1286+ 'bugs:dupeportletloadfailed',
1287+ function(handlers) {
1288+ setup_unsubscribe_icon_handlers();
1289+ });
1290+
1291+ /* If loading the subscriber IDs JSON has succeeded, set up the
1292+ * subscription link handlers and load the subscribers from dupes.
1293+ */
1294+ namespace.portlet.subscribe(
1295+ 'bugs:portletsubscriberidsloaded',
1296+ function() {
1297+ setup_subscription_link_handlers();
1298+ load_subscribers_from_duplicates();
1299+ });
1300+
1301+ /* If loading the subscriber IDs JSON fails we still need to load the
1302+ * subscribers from duplicates but we don't set up the subscription link
1303+ * handlers.
1304+ */
1305+ namespace.portlet.subscribe(
1306+ 'bugs:portletsubscriberidsfailed',
1307+ function() {
1308+ load_subscribers_from_duplicates();
1309+ });
1310+
1311+ /*
1312+ * Subscribing someone else requires loading a grayed out
1313+ * username into the DOM until the subscribe action completes.
1314+ * There are a couple XHR requests in check_can_be_unsubscribed
1315+ * before the subscribe work can be done, so fire a custom event
1316+ * bugs:nameloaded and do the work here when the event fires.
1317+ */
1318+ namespace.portlet.subscribe('bugs:nameloaded', function(subscription) {
1319+ var error_handler = new LP.client.ErrorHandler();
1320+ error_handler.clearProgressUI = function() {
1321+ var temp_link = Y.one('#temp-username');
1322+ if (temp_link) {
1323+ var temp_parent = temp_link.get('parentNode');
1324+ temp_parent.removeChild(temp_link);
1325+ }
1326+ };
1327+ error_handler.showError = function(error_msg) {
1328+ Y.lp.app.errors.display_error(
1329+ Y.one('.menu-link-addsubscriber'), error_msg);
1330+ };
1331+
1332+ var config = {
1333+ on: {
1334+ success: function() {
1335+ var temp_link = Y.one('#temp-username');
1336+ var temp_spinner = Y.one('#temp-name-spinner');
1337+ temp_link.removeChild(temp_spinner);
1338+ var anim = Y.lazr.anim.green_flash({ node: temp_link });
1339+ anim.on('end', function() {
1340+ add_user_name_link(subscription);
1341+ var temp_parent = temp_link.get('parentNode');
1342+ temp_parent.removeChild(temp_link);
1343+ });
1344+ anim.run();
1345+ },
1346+ failure: error_handler.getFailureHandler()
1347+ },
1348+ parameters: {
1349+ person: LP.client.get_absolute_uri(
1350+ subscription.get('person').get('escaped_uri')),
1351+ suppress_notify: false
1352+ }
1353+ };
1354+ lp_client.named_post(bug_repr.self_link, 'subscribe', config);
1355+ });
1356+}
1357+
1358+function load_subscriber_ids() {
1359+ function on_success(transactionid, response, args) {
1360+ try {
1361+ subscriber_ids = Y.JSON.parse(response.responseText);
1362+
1363+ // Fire a custom event to trigger the setting-up of the
1364+ // subscription handlers.
1365+ namespace.portlet.fire('bugs:portletsubscriberidsloaded');
1366+ } catch (e) {
1367+ // Fire an event to signal failure. This ensures that the
1368+ // subscribers-from-dupes still get loaded into the portlet.
1369+ namespace.portlet.fire('bugs:portletsubscriberidsfailed');
1370+ }
1371+ }
1372+
1373+ function on_failure() {
1374+ // Fire an event to signal failure. This ensures that the
1375+ // subscribers-from-dupes still get loaded into the portlet.
1376+ namespace.portlet.fire('bugs:portletsubscriberidsfailed');
1377+ }
1378+
1379+ var config = {on: {success: on_success,
1380+ failure: on_failure}};
1381+ var url = Y.one(
1382+ '#subscribers-ids-link').getAttribute('href');
1383+ Y.io(url, config);
1384+}
1385+
1386+/*
1387+ * Set click handlers for unsubscribe remove icons.
1388+ *
1389+ * @method setup_unsubscribe_icon_handlers
1390+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1391+ */
1392+function setup_unsubscribe_icon_handlers() {
1393+ var subscription = new Y.lp.bugs.subscriber.Subscription({
1394+ link: Y.one('.menu-link-subscription'),
1395+ spinner: Y.one('#sub-unsub-spinner'),
1396+ subscriber: new Y.lp.bugs.subscriber.Subscriber({
1397+ uri: LP.client.links.me,
1398+ subscriber_ids: subscriber_ids
1399+ })
1400+ });
1401+
1402+ Y.on('click', function(e) {
1403+ e.halt();
1404+ unsubscribe_user_via_icon(e.target, subscription);
1405+ }, '.unsub-icon');
1406+}
1407+
1408+/*
1409+ * Initialize callbacks for subscribe/unsubscribe links.
1410+ *
1411+ * @method setup_subscription_link_handlers
1412+ */
1413+function setup_subscription_link_handlers() {
1414+ if (LP.client.links.me === undefined) {
1415+ return;
1416+ }
1417+
1418+ setup_client_and_bug();
1419+ var subscription = new Y.lp.bugs.subscriber.Subscription({
1420+ link: Y.one('.menu-link-subscription'),
1421+ spinner: Y.one('#sub-unsub-spinner'),
1422+ subscriber: new Y.lp.bugs.subscriber.Subscriber({
1423+ uri: LP.client.links.me,
1424+ subscriber_ids: subscriber_ids
1425+ })
1426+ });
1427+
1428+ var is_direct = subscription.get(
1429+ 'link').get('parentNode').hasClass('subscribed-true');
1430+ var has_dupes = subscription.get(
1431+ 'link').get('parentNode').hasClass('dup-subscribed-true');
1432+ subscription.set('is_direct', is_direct);
1433+ subscription.set('has_dupes', has_dupes);
1434+
1435+ if (subscription.is_node()) {
1436+ subscription.get('link').on('click', function(e) {
1437+ e.halt();
1438+ subscription.set('can_be_unsubscribed', true);
1439+ subscription.set('person', subscription.get('subscriber'));
1440+ subscription.set('is_team', false);
1441+ var parent = e.target.get('parentNode');
1442+ if (namespace.use_advanced_subscriptions) {
1443+ var subscription_overlay =
1444+ setup_advanced_subscription_overlay(subscription);
1445+ subscription_overlay.show();
1446+ } else {
1447+ // Look for the false conditions of subscription, which
1448+ // is_direct_subscription, etc. don't report correctly,
1449+ // to make sure we only use subscribe_current_user for
1450+ // the current user.
1451+ if (parent.hasClass('subscribed-false') &&
1452+ parent.hasClass('dup-subscribed-false')) {
1453+ subscribe_current_user(subscription);
1454+ }
1455+ else {
1456+ unsubscribe_current_user(subscription);
1457+ }
1458+ }
1459+ });
1460+ subscription.get('link').addClass('js-action');
1461+ }
1462+
1463+ setup_subscribe_someone_else_handler(subscription);
1464+}
1465+
1466+function load_subscribers_from_duplicates() {
1467+ if (Y.UA.ie) {
1468+ return null;
1469+ }
1470+
1471+ Y.one('#subscribers-portlet-dupe-spinner').setStyle(
1472+ 'display', 'block');
1473+
1474+ function hide_spinner() {
1475+ Y.one('#subscribers-portlet-dupe-spinner').setStyle(
1476+ 'display', 'none');
1477+ // Fire a custom event to signal failure, so that
1478+ // any remaining unsub icons can be hooked up.
1479+ namespace.portlet.fire('bugs:dupeportletloadfailed');
1480+ }
1481+
1482+ function on_success(transactionid, response, args) {
1483+ hide_spinner();
1484+
1485+ var dupe_subscribers_container = Y.one(
1486+ '#subscribers-from-duplicates-container');
1487+ dupe_subscribers_container.set(
1488+ 'innerHTML',
1489+ dupe_subscribers_container.get('innerHTML') +
1490+ response.responseText);
1491+
1492+ // Fire a custom portlet loaded event to notify when
1493+ // it's safe to setup dupe subscriber link callbacks.
1494+ namespace.portlet.fire('bugs:dupeportletloaded');
1495+ }
1496+
1497+ var config = {on: {success: on_success,
1498+ failure: hide_spinner}};
1499+ var url = Y.one(
1500+ '#subscribers-from-dupes-content-link').getAttribute(
1501+ 'href').replace('bugs.', '');
1502+ Y.io(url, config);
1503+}
1504+
1505+/*
1506+ * Add the user name to the subscriber's list.
1507+ *
1508+ * @method add_user_name_link
1509+ */
1510+function add_user_name_link(subscription) {
1511+ var person = subscription.get('person');
1512+ var link_node = build_user_link_html(subscription);
1513+ var subscribers = Y.one('#subscribers-links');
1514+ if (subscription.is_current_user_subscribing()) {
1515+ // If this is the current user, then top post the name and be done.
1516+ subscribers.insertBefore(link_node, subscribers.get('firstChild'));
1517+ } else {
1518+ var next = get_next_subscriber_node(subscription);
1519+ if (next) {
1520+ subscribers.insertBefore(link_node, next);
1521+ } else {
1522+ // Handle the case of the displayed "None".
1523+ var none_subscribers = Y.one('#none-subscribers');
1524+ if (none_subscribers) {
1525+ var none_parent = none_subscribers.get('parentNode');
1526+ none_parent.removeChild(none_subscribers);
1527+ }
1528+ subscribers.appendChild(link_node);
1529+ }
1530+ }
1531+
1532+ // Set the click handler if adding a remove icon.
1533+ if (subscription.can_be_unsubscribed_by_user()) {
1534+ var remove_icon =
1535+ Y.one('#unsubscribe-icon-' + person.get('css_name'));
1536+ remove_icon.on('click', function(e) {
1537+ e.halt();
1538+ unsubscribe_user_via_icon(e.target, subscription);
1539+ });
1540+ }
1541+}
1542+
1543+/*
1544+ * Unsubscribe a user from this bugtask when a remove icon is clicked.
1545+ *
1546+ * @method unsubscribe_user_via_icon
1547+ * @param icon {Node} The remove icon that was clicked.
1548+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1549+*/
1550+function unsubscribe_user_via_icon(icon, subscription) {
1551+ icon.set('src', '/@@/spinner');
1552+ var icon_parent = icon.get('parentNode');
1553+
1554+ var user_uri = get_user_uri_from_icon(icon);
1555+ var person = new Y.lp.bugs.subscriber.Subscriber({
1556+ uri: user_uri,
1557+ subscriber_ids: subscriber_ids
1558+ });
1559+ subscription.set('person', person);
1560+
1561+ // Determine if this is a dupe.
1562+ var is_dupe;
1563+ var icon_parent_div = icon_parent.get('parentNode');
1564+ var dupe_id = 'dupe-' + person.get('css_name');
1565+ if (icon_parent_div.get('id') == dupe_id) {
1566+ is_dupe = true;
1567+ } else {
1568+ is_dupe = false;
1569+ }
1570+
1571+ var error_handler = new LP.client.ErrorHandler();
1572+ error_handler.clearProgressUI = function () {
1573+ icon.set('src', '/@@/remove');
1574+ // Grab the icon again to reset to click handler.
1575+ var unsubscribe_icon = Y.one(
1576+ '#unsubscribe-icon-' + person.get('css_name'));
1577+ unsubscribe_icon.on('click', function(e) {
1578+ e.halt();
1579+ unsubscribe_user_via_icon(e.target, subscription);
1580+ });
1581+
1582+ };
1583+ error_handler.showError = function (error_msg) {
1584+ var flash_node = Y.one('.' + person.get('css_name'));
1585+ Y.lp.app.errors.display_error(flash_node, error_msg);
1586+
1587+ };
1588+
1589+ var subscription_link = subscription.get('link');
1590+ var config = {
1591+ on: {
1592+ success: function(client) {
1593+ icon_parent.removeChild(icon);
1594+ var anim = Y.lazr.anim.green_flash({ node: icon_parent_div });
1595+ anim.on('end', function(e) {
1596+ remove_user_name_link(icon_parent_div);
1597+ set_none_for_empty_subscribers();
1598+ var person_link = Y.one('.' + person.get('css_name'));
1599+ if (Y.Lang.isNull(person_link) &&
1600+ subscription.is_current_user_subscribing()) {
1601+ // Current user has been completely unsubscribed.
1602+ subscription.disable_spinner(
1603+ subscription_labels.SUBSCRIBE);
1604+ set_subscription_link_parent_class(
1605+ subscription_link, false, false);
1606+ subscription.set('is_direct', false);
1607+ subscription.set('has_dupes', false);
1608+ } else {
1609+ if (is_dupe) {
1610+ // A direct subscription remains.
1611+ set_subscription_link_parent_class(
1612+ subscription_link, true, false);
1613+ subscription.set('is_direct', true);
1614+ subscription.set('has_dupes', false);
1615+ } else {
1616+ // A dupe subscription remains.
1617+ set_subscription_link_parent_class(
1618+ subscription_link, false, true);
1619+ subscription.set('is_direct', false);
1620+ subscription.set('has_dupes', true);
1621+ }
1622+ }
1623+ });
1624+ anim.run();
1625+ },
1626+
1627+ failure: error_handler.getFailureHandler()
1628+ }
1629+ };
1630+
1631+ if (!subscription.is_current_user_subscribing()) {
1632+ config.parameters = {
1633+ person: LP.client.get_absolute_uri(user_uri)
1634+ };
1635+ }
1636+
1637+ if (is_dupe) {
1638+ lp_client.named_post(
1639+ bug_repr.self_link, 'unsubscribeFromDupes', config);
1640+ } else {
1641+ lp_client.named_post(bug_repr.self_link, 'unsubscribe', config);
1642+ }
1643+}
1644+
1645+/*
1646+ * Create and return a FormOverlay for advanced subscription
1647+ * interactions.
1648+ *
1649+ * @method setup_advanced_subscription_overlay
1650+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1651+ */
1652+function setup_advanced_subscription_overlay(subscription) {
1653+ var subscription_overlay = new Y.lazr.FormOverlay({
1654+ headerContent: '<h2>Subscribe to bug</h2>',
1655+ form_submit_button:
1656+ Y.Node.create(submit_button_html),
1657+ form_cancel_button:
1658+ Y.Node.create(cancel_button_html),
1659+ centered: true,
1660+ visible: false
1661+ });
1662+ subscription_overlay.set(
1663+ 'form_submit_callback', function(form_data) {
1664+ handle_advanced_subscription_overlay(subscription, form_data);
1665+ subscription_overlay.hide();
1666+ });
1667+
1668+ var subscription_link_url = subscription.get(
1669+ 'link').get('href') + '/++form++';
1670+ subscription_overlay.loadFormContentAndRender(
1671+ subscription_link_url);
1672+ subscription_overlay.render('#privacy-form-container');
1673+ return subscription_overlay
1674+}
1675+
1676+/*
1677+ * Subscribe the current user via the LP API.
1678+ *
1679+ * @method subscribe_current_user
1680+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1681+ */
1682+function subscribe_current_user(subscription) {
1683+ subscription.enable_spinner('Subscribing...');
1684+ var subscription_link = subscription.get('link');
1685+ var subscriber = subscription.get('subscriber');
1686+ var bug_notification_level = subscription.get('bug_notification_level');
1687+
1688+ var error_handler = new LP.client.ErrorHandler();
1689+ error_handler.clearProgressUI = function () {
1690+ subscription.disable_spinner();
1691+ };
1692+ error_handler.showError = function (error_msg) {
1693+ Y.lp.app.errors.display_error(subscription_link, error_msg);
1694+ };
1695+
1696+ var config = {
1697+ on: {
1698+ success: function(client) {
1699+ if (namespace.use_advanced_subscriptions) {
1700+ subscription.disable_spinner(
1701+ subscription_labels.EDIT);
1702+ } else {
1703+ subscription.disable_spinner(
1704+ subscription_labels.UNSUBSCRIBE);
1705+ }
1706+
1707+ if (subscription.has_duplicate_subscriptions()) {
1708+ set_subscription_link_parent_class(
1709+ subscription_link, true, true);
1710+ } else {
1711+ set_subscription_link_parent_class(
1712+ subscription_link, true, false);
1713+ }
1714+
1715+ // Handle the case where the subscriber's list displays
1716+ // "None".
1717+ var empty_subscribers = Y.one("#none-subscribers");
1718+ if (empty_subscribers) {
1719+ var parent = empty_subscribers.get('parentNode');
1720+ parent.removeChild(empty_subscribers);
1721+ }
1722+
1723+ add_user_name_link(subscription);
1724+
1725+ var flash_node = Y.one('.' + subscriber.get('css_name'));
1726+ var anim = Y.lazr.anim.green_flash({ node: flash_node });
1727+ anim.run();
1728+ },
1729+
1730+ failure: error_handler.getFailureHandler()
1731+ },
1732+
1733+ parameters: {
1734+ person: LP.client.get_absolute_uri(subscriber.get('escaped_uri')),
1735+ suppress_notify: false,
1736+ level: bug_notification_level
1737+ }
1738+ };
1739+ lp_client.named_post(bug_repr.self_link, 'subscribe', config);
1740+}
1741+
1742+/*
1743+ * Unsubscribe the current user via the LP API.
1744+ *
1745+ * @method unsubscribe_current_user
1746+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1747+ */
1748+function unsubscribe_current_user(subscription) {
1749+ subscription.enable_spinner('Unsubscribing...');
1750+ var subscription_link = subscription.get('link');
1751+ var subscriber = subscription.get('subscriber');
1752+
1753+ var error_handler = new LP.client.ErrorHandler();
1754+ error_handler.clearProgressUI = function () {
1755+ subscription.disable_spinner();
1756+ };
1757+ error_handler.showError = function (error_msg) {
1758+ Y.lp.app.errors.display_error(subscription_link, error_msg);
1759+ };
1760+
1761+ var config = {
1762+ on: {
1763+ success: function(client) {
1764+ if (subscription.is_direct_subscription() &&
1765+ subscription.has_duplicate_subscriptions()) {
1766+ // Don't change the 'Unsubscribe' text if
1767+ // dupe subscriptions remain.
1768+ subscription.disable_spinner();
1769+ set_subscription_link_parent_class(
1770+ subscription_link, false, true);
1771+ subscription.set('is_direct', false);
1772+ } else if (subscription.is_direct_subscription() &&
1773+ !subscription.has_duplicate_subscriptions()) {
1774+ // Only unsub'ing a direct subscriber here.
1775+ subscription.disable_spinner(
1776+ subscription_labels.SUBSCRIBE);
1777+ set_subscription_link_parent_class(
1778+ subscription_link, false, false);
1779+ subscription.set('is_direct', false);
1780+ } else {
1781+ // Only unsub'ing dupes here.
1782+ subscription.disable_spinner(
1783+ subscription_labels.SUBSCRIBE);
1784+ set_subscription_link_parent_class(
1785+ subscription_link, false, false);
1786+ subscription.set('has_dupes', false);
1787+ }
1788+
1789+ var flash_node = Y.one('.' + subscriber.get('css_name'));
1790+ var anim = Y.lazr.anim.green_flash({ node: flash_node });
1791+ anim.on('end', function(e) {
1792+ remove_user_name_link(flash_node);
1793+ set_none_for_empty_subscribers();
1794+ });
1795+ anim.run();
1796+ },
1797+
1798+ failure: error_handler.getFailureHandler()
1799+ }
1800+ };
1801+ if (subscription.is_direct_subscription()) {
1802+ lp_client.named_post(bug_repr.self_link, 'unsubscribe', config);
1803+ } else {
1804+ lp_client.named_post(
1805+ bug_repr.self_link, 'unsubscribeFromDupes', config);
1806+ }
1807+}
1808+
1809+/*
1810+ * Initialize click handler for the subscribe someone else link.
1811+ *
1812+ * @method setup_subscribe_someone_else_handler
1813+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1814+ */
1815+function setup_subscribe_someone_else_handler(subscription) {
1816+ var config = {
1817+ header: 'Subscribe someone else',
1818+ step_title: 'Search',
1819+ picker_activator: '.menu-link-addsubscriber'
1820+ };
1821+
1822+ config.save = function(result) {
1823+ subscribe_someone_else(result, subscription);
1824+ };
1825+ var picker = Y.lp.app.picker.create('ValidPersonOrTeam', config);
1826+}
1827+
1828+/*
1829+ * Build the HTML for a user link for the subscribers list.
1830+ *
1831+ * @method build_user_link_html
1832+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1833+ * @return html {String} The HTML used for creating a subscriber link.
1834+ */
1835+function build_user_link_html(subscription) {
1836+ var name = subscription.get('person').get('name');
1837+ var css_name = subscription.get('person').get('css_name');
1838+ var full_name = subscription.get('person').get('full_display_name');
1839+ // Be paranoid about display_name, since timeouts or other errors
1840+ // could mean display_name wasn't set on initialization.
1841+ if (subscription.get('person').get('display_name') === '') {
1842+ subscription.get('person').set_display_name();
1843+ }
1844+ var display_name = subscription.get('person').get('display_name');
1845+ var terms = {
1846+ name: name,
1847+ css_name: css_name,
1848+ display_name: display_name,
1849+ full_name: full_name
1850+ };
1851+
1852+ if (subscription.is_current_user_subscribing()) {
1853+ terms.subscribed_by = 'themselves';
1854+ } else {
1855+ terms.subscribed_by = 'by ' + full_name;
1856+ }
1857+
1858+ var html = Y.Node.create('<div><a></a></div>');
1859+ html.addClass(terms.css_name);
1860+
1861+ if (subscription.is_direct_subscription()) {
1862+ html.set('id', 'direct-' + terms.css_name);
1863+ } else {
1864+ html.set('id', 'dupe-' + terms.css_name);
1865+ }
1866+
1867+ html.one('a')
1868+ .set('href', '/~' + terms.name)
1869+ .set('name', terms.full_name)
1870+ .set('title', 'Subscribed ' + terms.subscribed_by);
1871+
1872+ var span;
1873+ if (subscription.is_team()) {
1874+ span = '<span class="sprite team"></span>';
1875+ } else {
1876+ span = '<span class="sprite person"></span>';
1877+ }
1878+
1879+ html.one('a')
1880+ .appendChild(Y.Node.create(span))
1881+ .appendChild(document.createTextNode(terms.display_name));
1882+
1883+ // Add remove icon if the current user can unsubscribe the subscriber.
1884+ if (subscription.can_be_unsubscribed_by_user()) {
1885+ var icon_html = Y.Node.create(
1886+ '<a href="+subscribe">' +
1887+ '<img class="unsub-icon" src="/@@/remove" alt="Remove" /></a>');
1888+ icon_html
1889+ .set('id', 'unsubscribe-' + terms.css_name)
1890+ .set('title', 'Unsubscribe ' + terms.full_name);
1891+ icon_html.one('img')
1892+ .set('id', 'unsubscribe-icon-' + terms.css_name);
1893+ html.appendChild(icon_html);
1894+ }
1895+
1896+ return html;
1897+}
1898+
1899+/*
1900+ * Returns the next node in alphabetical order after the subscriber
1901+ * node now being added. No node is returned to append to end of list.
1902+ *
1903+ * The name can appear in one of two different lists. 1) The list of
1904+ * subscribers that can be unsubscribed by the current user, and
1905+ * 2) the list of subscribers that cannot be unsubscribed.
1906+ *
1907+ * @method get_next_subscriber_node
1908+ * @param subscription_link {Node} The sub/unsub link.
1909+ * @return {Node} The node appearing next in the subscriber list or
1910+ * undefined if no node is next.
1911+ */
1912+function get_next_subscriber_node(subscription) {
1913+ var full_name = subscription.get('person').get('full_display_name');
1914+ var can_be_unsubscribed = subscription.can_be_unsubscribed_by_user();
1915+ var nodes_by_name = {};
1916+ var unsubscribables = [];
1917+ var not_unsubscribables = [];
1918+
1919+ // Use the list of subscribers pulled from the DOM to have sortable
1920+ // lists of unsubscribable vs. not unsubscribale person links.
1921+ var all_subscribers = Y.all('#subscribers-links div');
1922+ if (all_subscribers.size() > 0) {
1923+ all_subscribers.each(function(sub_link) {
1924+ if (sub_link.getAttribute('id') != 'temp-username') {
1925+ // User's displayname is found via the link's "name"
1926+ // attribute.
1927+ var sub_link_name = sub_link.one('a').getAttribute('name');
1928+ nodes_by_name[sub_link_name] = sub_link;
1929+ if (sub_link.one('img.unsub-icon')) {
1930+ unsubscribables.push(sub_link_name);
1931+ } else {
1932+ not_unsubscribables.push(sub_link_name);
1933+ }
1934+ }
1935+ });
1936+
1937+ // Add the current subscription.
1938+ if (can_be_unsubscribed) {
1939+ unsubscribables.push(full_name);
1940+ } else {
1941+ not_unsubscribables.push(full_name);
1942+ }
1943+ unsubscribables.sort();
1944+ not_unsubscribables.sort();
1945+ } else {
1946+ // If there is no all_subscribers, then we're dealing with
1947+ // the printed None, so return.
1948+ return;
1949+ }
1950+
1951+ var i;
1952+ if ((!unsubscribables && !not_unsubscribables) ||
1953+ // If A) neither list exists, B) the user belongs in the second
1954+ // list but the second list doesn't exist, or C) user belongs in the
1955+ // first list and the second doesn't exist, return no node to append.
1956+ (!can_be_unsubscribed && !not_unsubscribables) ||
1957+ (can_be_unsubscribed && unsubscribables && !not_unsubscribables)) {
1958+ return;
1959+ } else if (
1960+ // If the user belongs in the first list, and the first list
1961+ // doesn't exist, but the second one does, return the first node
1962+ // in the second list.
1963+ can_be_unsubscribed && !unsubscribables && not_unsubscribables) {
1964+ return nodes_by_name[not_unsubscribables[0]];
1965+ } else if (can_be_unsubscribed) {
1966+ // If the user belongs in the first list, loop the list for position.
1967+ for (i=0; i<unsubscribables.length; i++) {
1968+ if (unsubscribables[i] == full_name) {
1969+ if (i+1 < unsubscribables.length) {
1970+ return nodes_by_name[unsubscribables[i+1]];
1971+ // If the current link should go at the end of the first
1972+ // list and we're at the end of that list, return the
1973+ // first node of the second list. Due to earlier checks
1974+ // we can be sure this list exists.
1975+ } else if (i+1 >= unsubscribables.length) {
1976+ return nodes_by_name[not_unsubscribables[0]];
1977+ }
1978+ }
1979+ }
1980+ } else if (!can_be_unsubscribed) {
1981+ // If user belongs in the second list, loop the list for position.
1982+ for (i=0; i<not_unsubscribables.length; i++) {
1983+ if (not_unsubscribables[i] == full_name) {
1984+ if (i+1 < not_unsubscribables.length) {
1985+ return nodes_by_name[not_unsubscribables[i+1]];
1986+ } else {
1987+ return;
1988+ }
1989+ }
1990+ }
1991+ }
1992+}
1993+
1994+/*
1995+ * Traverse the DOM of a given remove icon to find
1996+ * the user's link. Returns a URI of the form "/~username".
1997+ *
1998+ * @method get_user_uri_from_icon
1999+ * @param icon {Node} The node representing a remove icon.
2000+ * @return user_uri {String} The user's uri, without the hostname.
2001+ */
2002+function get_user_uri_from_icon(icon) {
2003+ var parent_div = icon.get('parentNode').get('parentNode');
2004+ // This should be parent_div.firstChild, but because of #text
2005+ // and cross-browser issues, using the YUI query syntax is
2006+ // safer here.
2007+ var user_uri = parent_div.one('a').getAttribute('href');
2008+
2009+ // Strip the domain off. We just want a path.
2010+ var host_start = user_uri.indexOf('//');
2011+ if (host_start != -1) {
2012+ var host_end = user_uri.indexOf('/', host_start+2);
2013+ return user_uri.substring(host_end, user_uri.length);
2014+ }
2015+
2016+ return user_uri;
2017+}
2018+
2019+/*
2020+ * Used to remove the user's name from the subscriber's list.
2021+ *
2022+ * @method remove_user_name_link
2023+ * @param user_node {Node} Node representing the user name link.
2024+ */
2025+function remove_user_name_link(user_node) {
2026+ var parent = user_node.get('parentNode');
2027+ parent.removeChild(user_node);
2028+}
2029+
2030+/*
2031+ * Add the "None" div to the subscribers list if
2032+ * there aren't any subscribers left.
2033+ *
2034+ * @method set_none_for_empty_subscribers
2035+ */
2036+function set_none_for_empty_subscribers() {
2037+ var subscriber_list = Y.one('#subscribers-links');
2038+ // Assume if subscriber_list has no child divs
2039+ // then the list of subscribers is empty.
2040+ if (!Y.Lang.isValue(subscriber_list.one('div')) &&
2041+ !Y.Lang.isValue(Y.one('#none-subscribers'))) {
2042+ var none_div = Y.Node.create('<div id="none-subscribers">None</div>');
2043+ subscriber_list.appendChild(none_div);
2044+ }
2045+
2046+ // Clear the empty duplicate subscribers list if it exists.
2047+ var dup_list = Y.one('#subscribers-from-duplicates');
2048+ if (Y.Lang.isValue(dup_list) &&
2049+ !Y.Lang.isValue(dup_list.one('div'))) {
2050+ var parent = dup_list.get('parentNode');
2051+ parent.removeChild(dup_list);
2052+ }
2053+}
2054+
2055+/*
2056+ * Set the class on subscription link's parentNode.
2057+ *
2058+ * This is used to reset the class used by the
2059+ * click handler to know which link was clicked.
2060+ *
2061+ * @method set_subscription_link_parent_class
2062+ * @param subscription_link {Node} The sub/unsub link.
2063+ * @param subscribed {Boolean} The sub/unsub'ed flag for the class.
2064+ * @param dupe_subscribed {Boolean} The sub/unsub'ed flag for dupes
2065+ * on the class.
2066+ */
2067+function set_subscription_link_parent_class(
2068+ user_link, subscribed, dupe_subscribed) {
2069+
2070+ var parent = user_link.get('parentNode');
2071+ if (subscribed) {
2072+ parent.removeClass('subscribed-false');
2073+ parent.addClass('subscribed-true');
2074+ } else {
2075+ parent.removeClass('subscribed-true');
2076+ parent.addClass('subscribed-false');
2077+ }
2078+
2079+ if (dupe_subscribed) {
2080+ parent.removeClass('dup-subscribed-false');
2081+ parent.addClass('dup-subscribed-true');
2082+ } else {
2083+ parent.removeClass('dup-subscribed-true');
2084+ parent.addClass('dup-subscribed-false');
2085+ }
2086+}
2087+
2088+
2089+/*
2090+ * Handle the advanced_subscription_overlay's form submissions.
2091+ *
2092+ * @method handle_advanced_subscription_overlay
2093+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
2094+ * @param form_data {Object} The data from the submitted form.
2095+ */
2096+function handle_advanced_subscription_overlay(subscription, form_data) {
2097+ var link = subscription.get('link');
2098+ var link_parent = link.get('parentNode');
2099+ if (link_parent.hasClass('subscribed-false') &&
2100+ link_parent.hasClass('dup-subscribed-false')) {
2101+ // The user isn't subscribed, so subscribe them.
2102+ subscription.set(
2103+ 'bug_notification_level',
2104+ form_data['field.bug_notification_level']);
2105+ subscribe_current_user(subscription);
2106+ } else if (
2107+ form_data['field.subscription'] == 'update-subscription') {
2108+ // The user is already subscribed and wants to update their
2109+ // subscription.
2110+ setup_client_and_bug();
2111+ var person_name = subscription.get('person').get('name');
2112+ var subscription_url =
2113+ lp_bug_entry.get('self_link') + '/+subscription/' +
2114+ person_name;
2115+ config = {
2116+ on: {
2117+ success: function(lp_subscription) {
2118+ subscription.enable_spinner('Updating subscription...');
2119+ lp_subscription.set(
2120+ 'bug_notification_level',
2121+ form_data['field.bug_notification_level'][0])
2122+ save_config = {
2123+ on: {
2124+ success: function(e) {
2125+ subscription.disable_spinner(
2126+ 'Edit subscription');
2127+ var anim = Y.lazr.anim.green_flash({
2128+ node: link_parent
2129+ });
2130+ anim.run();
2131+ },
2132+ failure: function(e) {
2133+ subscription.disable_spinner(
2134+ 'Edit subscription');
2135+ var anim = Y.lazr.anim.red_flash({
2136+ node: link_parent
2137+ });
2138+ anim.run();
2139+ }
2140+ }
2141+ }
2142+ lp_subscription.lp_save(save_config);
2143+ }
2144+ }
2145+ }
2146+ lp_client.get(subscription_url, config);
2147+ } else {
2148+ // The user is already subscribed and wants to unsubscribe.
2149+ unsubscribe_current_user(subscription);
2150+ }
2151+}
2152+
2153+/*
2154+ * Subscribe a person or team other than the current user.
2155+ * This is a callback for the subscribe someone else picker.
2156+ *
2157+ * @method subscribe_someone_else
2158+ * @result {Object} The object representing a person returned by the API.
2159+ */
2160+function subscribe_someone_else(result, subscription) {
2161+ var person = new Y.lp.bugs.subscriber.Subscriber({
2162+ uri: result.api_uri,
2163+ display_name: result.title,
2164+ subscriber_ids: subscriber_ids
2165+ });
2166+ subscription.set('person', person);
2167+
2168+ var error_handler = new LP.client.ErrorHandler();
2169+ error_handler.showError = function(error_msg) {
2170+ Y.lp.app.errors.display_error(
2171+ Y.one('.menu-link-addsubscriber'), error_msg);
2172+ };
2173+
2174+ if (subscription.is_already_subscribed()) {
2175+ error_handler.showError(
2176+ subscription.get('person').get('full_display_name') +
2177+ ' has already been subscribed');
2178+ } else {
2179+ check_can_be_unsubscribed(subscription);
2180+ }
2181+}
2182+
2183+/*
2184+ * Check if the current user can unsubscribe the person
2185+ * being subscribed.
2186+ *
2187+ * This must be done in JavaScript, since the subscription
2188+ * hasn't completed yet, and so, can_be_unsubscribed_by_user
2189+ * cannot be used.
2190+ *
2191+ * @method check_can_be_unsubscribed
2192+ * @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
2193+ */
2194+function check_can_be_unsubscribed(subscription) {
2195+ var error_handler = new LP.client.ErrorHandler();
2196+ error_handler.showError = function (error_msg) {
2197+ Y.lp.app.errors.display_error(
2198+ Y.one('.menu-link-addsubscriber'), error_msg);
2199+ };
2200+
2201+ var config = {
2202+ on: {
2203+ success: function(result) {
2204+ var is_team = result.get('is_team');
2205+ subscription.set('is_team', is_team);
2206+ var final_config = {
2207+ on: {
2208+ success: function(result) {
2209+ var team_member = false;
2210+ for (var i=0; i<result.entries.length; i++) {
2211+ if (result.entries[i].member_link ==
2212+ LP.client.get_absolute_uri(
2213+ subscription.get(
2214+ 'subscriber').get('uri'))) {
2215+ team_member = true;
2216+ }
2217+ }
2218+
2219+ if (team_member) {
2220+ subscription.set('can_be_unsubscribed', true);
2221+ add_temp_user_name(subscription);
2222+ } else {
2223+ subscription.set(
2224+ 'can_be_unsubscribed', false);
2225+ add_temp_user_name(subscription);
2226+ }
2227+ },
2228+
2229+ failure: error_handler.getFailureHandler()
2230+ }
2231+ };
2232+
2233+ if (is_team) {
2234+ // Get a list of members to see if current user
2235+ // is a team member.
2236+ var members = result.get(
2237+ 'members_details_collection_link');
2238+ lp_client.get(members, final_config);
2239+ } else {
2240+ subscription.set('can_be_unsubscribed', false);
2241+ add_temp_user_name(subscription);
2242+ }
2243+ },
2244+
2245+ failure: error_handler.getFailureHandler()
2246+ }
2247+ };
2248+ lp_client.get(LP.client.get_absolute_uri(
2249+ subscription.get('person').get('escaped_uri')), config);
2250+}
2251+
2252+/*
2253+ * Add a grayed out, temporary user name when subscribing
2254+ * someone else.
2255+ *
2256+ * @method add_temp_user_name
2257+ * @param subscription_link {Node} The sub/unsub link.
2258+ */
2259+function add_temp_user_name(subscription) {
2260+ // Be paranoid about display_name, since timeouts or other errors
2261+ // could mean display_name wasn't set on initialization.
2262+ if (subscription.get('person').get('display_name') === '') {
2263+ subscription.get('person').set_display_name();
2264+ }
2265+ var display_name = subscription.get('person').get('display_name');
2266+ var img_src;
2267+ if (subscription.is_team()) {
2268+ img_src = '/@@/teamgray';
2269+ } else {
2270+ img_src = '/@@/persongray';
2271+ }
2272+
2273+ // The <span>...</span> below must *not* be <span/>. On FF (maybe
2274+ // others, but at least on FF 3.0.11) will then not notice any
2275+ // following sibling nodes, like the spinner image.
2276+ var link_node = Y.Node.create([
2277+ '<div id="temp-username"> ',
2278+ ' <img alt="" width="14" height="14" />',
2279+ ' <span>Other Display Name</span>',
2280+ ' <img id="temp-name-spinner" src="/@@/spinner" alt="" ',
2281+ ' style="position:absolute;right:8px" /></div>'].join(''));
2282+ link_node.one('img').set('src', img_src);
2283+ link_node.replaceChild(
2284+ document.createTextNode(display_name),
2285+ link_node.one('span'));
2286+
2287+ var subscribers = Y.one('#subscribers-links');
2288+ var next = get_next_subscriber_node(subscription);
2289+ if (next) {
2290+ subscribers.insertBefore(link_node, next);
2291+ } else {
2292+ // Handle the case of the displayed "None".
2293+ var none_subscribers = Y.one('#none-subscribers');
2294+ if (none_subscribers) {
2295+ var none_parent = none_subscribers.get('parentNode');
2296+ none_parent.removeChild(none_subscribers);
2297+ }
2298+ subscribers.appendChild(link_node);
2299+ }
2300+
2301+ // Fire a custom event to know it's safe to begin
2302+ // any actual subscribing work.
2303+ namespace.portlet.fire('bugs:nameloaded', subscription);
2304+}
2305+
2306+}, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
2307+ "json-parse", "substitute", "widget-position-ext",
2308+ "lazr.formoverlay", "lazr.anim", "lazr.base",
2309+ "lazr.overlay", "lazr.choiceedit", "lp.app.picker",
2310+ "lp.client.plugins", "lp.bugs.subscriber",
2311+ "lp.app.errors"]});
2312
2313=== modified file 'lib/lp/bugs/templates/bug-portlet-subscribers.pt'
2314--- lib/lp/bugs/templates/bug-portlet-subscribers.pt 2010-06-15 13:51:55 +0000
2315+++ lib/lp/bugs/templates/bug-portlet-subscribers.pt 2011-02-15 18:19:47 +0000
2316@@ -25,7 +25,7 @@
2317 <img src="/@@/spinner" />
2318 </div>
2319 <script type="text/javascript">
2320- LPS.use('io-base', 'node', 'lp.bugs.bugtask_index', function(Y) {
2321+ LPS.use('io-base', 'node', 'lp.bugs.bugtask_index.portlets', function(Y) {
2322 // Must be done inline here to ensure the load event fires.
2323 // This is a work around for a YUI3 issue with event handling.
2324 var subscription_link = Y.one('.menu-link-subscription');
2325@@ -37,8 +37,8 @@
2326 }
2327
2328 Y.on('domready', function() {
2329- if (Y.lp.bugs.bugtask_index) {
2330- Y.lp.bugs.bugtask_index.load_subscribers_portlet(
2331+ if (Y.lp.bugs.bugtask_index.portlets) {
2332+ Y.lp.bugs.bugtask_index.portlets.load_subscribers_portlet(
2333 subscription_link, subscription_link_handler);
2334 }
2335 });
2336
2337=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
2338--- lib/lp/bugs/templates/bugtask-index.pt 2011-02-07 11:44:51 +0000
2339+++ lib/lp/bugs/templates/bugtask-index.pt 2011-02-15 18:19:47 +0000
2340@@ -23,8 +23,9 @@
2341 </tal:subscriptions-js>
2342 <script type="text/javascript">
2343 LPS.use('base', 'node', 'oop', 'event', 'lp.bugs.bugtask_index',
2344+ 'lp.bugs.bugtask_index.portlets',
2345 'lp.code.branchmergeproposal.diff', function(Y) {
2346- Y.lp.bugs.bugtask_index.use_advanced_subscriptions =
2347+ Y.lp.bugs.bugtask_index.portlets.use_advanced_subscriptions =
2348 use_advanced_subscriptions;
2349 Y.lp.bugs.bugtask_index.setup_bugtask_index();
2350 Y.on('load', function(e) {