1
/* Copyright 2011 Canonical Ltd. This software is licensed under the
2
* GNU Affero General Public License version 3 (see the file LICENSE).
4
* Form overlay widgets and subscriber handling for bug pages.
7
* @submodule bugtask_index.portlets
10
YUI.add('lp.bugs.bugtask_index.portlets', function(Y) {
12
var namespace = Y.namespace('lp.bugs.bugtask_index.portlets');
14
// The launchpad js client used.
17
// The launchpad client entry for the current bug.
20
// The bug itself, taken from cache.
23
// A boolean telling us whether advanced subscription features are to be
25
var use_advanced_subscriptions = false;
27
var subscription_labels = Y.lp.bugs.subscriber.subscription_labels;
30
'<button type="submit" name="field.actions.change" ' +
31
'value="Change" class="lazr-pos lazr-btn" >OK</button>';
33
'<button type="button" name="field.actions.cancel" ' +
34
'class="lazr-neg lazr-btn" >Cancel</button>';
36
// The set of subscriber CSS IDs as a JSON struct.
40
* An object representing the bugtask subscribers portlet.
42
* Since the portlet loads via XHR and inline subscribing
43
* depends on that portlet being loaded, setup a custom
44
* event object, to provide a hook for initializing subscription
45
* link callbacks after custom events.
47
var PortletTarget = function() {};
48
Y.augment(PortletTarget, Y.Event.Target);
49
namespace.portlet = new PortletTarget();
52
* Create the lp client and bug entry if we haven't done so already.
54
* @method setup_client_and_bug
56
function setup_client_and_bug() {
57
lp_client = new Y.lp.client.Launchpad();
59
if (bug_repr === undefined) {
60
bug_repr = LP.cache.bug;
61
lp_bug_entry = new Y.lp.client.Entry(
62
lp_client, bug_repr, bug_repr.self_link);
66
namespace.load_subscribers_portlet = function(
67
subscription_link, subscription_link_handler) {
72
Y.one('#subscribers-portlet-spinner').setStyle('display', 'block');
74
function hide_spinner() {
75
Y.one('#subscribers-portlet-spinner').setStyle('display', 'none');
76
// Fire a custom event to notify that the initial click
77
// handler on subscription_link set above should be
80
namespace.portlet.fire(
81
'bugs:portletloadfailed', subscription_link_handler);
85
function setup_portlet(transactionid, response, args) {
87
Y.one('#portlet-subscribers')
88
.appendChild(Y.Node.create(response.responseText));
90
// Fire a custom portlet loaded event to notify when
91
// it's safe to setup subscriber link callbacks.
92
namespace.portlet.fire('bugs:portletloaded');
95
var config = {on: {success: setup_portlet,
96
failure: hide_spinner}};
98
'#subscribers-content-link').getAttribute('href').replace(
104
namespace.setup_portlet_handlers = function() {
105
namespace.portlet.subscribe('bugs:portletloaded', function() {
106
load_subscriber_ids();
109
* If the subscribers portlet fails to load, clear any
110
* click handlers, so the normal subscribe page can be reached.
112
namespace.portlet.subscribe('bugs:portletloadfailed', function(handler) {
115
namespace.portlet.subscribe('bugs:dupeportletloaded', function() {
116
setup_subscription_link_handlers();
117
setup_unsubscribe_icon_handlers();
119
/* If the dupe subscribers portlet fails to load,
120
* be sure to try to handle any unsub icons that may
123
namespace.portlet.subscribe(
124
'bugs:dupeportletloadfailed',
126
setup_subscription_link_handlers();
127
setup_unsubscribe_icon_handlers();
130
/* If loading the subscriber IDs JSON has succeeded, set up the
131
* subscription link handlers and load the subscribers from dupes.
133
namespace.portlet.subscribe(
134
'bugs:portletsubscriberidsloaded',
136
load_subscribers_from_duplicates();
139
/* If loading the subscriber IDs JSON fails we still need to load the
140
* subscribers from duplicates but we don't set up the subscription link
143
namespace.portlet.subscribe(
144
'bugs:portletsubscriberidsfailed',
146
load_subscribers_from_duplicates();
150
* Subscribing someone else requires loading a grayed out
151
* username into the DOM until the subscribe action completes.
152
* There are a couple XHR requests in check_can_be_unsubscribed
153
* before the subscribe work can be done, so fire a custom event
154
* bugs:nameloaded and do the work here when the event fires.
156
namespace.portlet.subscribe('bugs:nameloaded', function(subscription) {
157
var error_handler = new Y.lp.client.ErrorHandler();
158
error_handler.clearProgressUI = function() {
159
var temp_link = Y.one('#temp-username');
161
var temp_parent = temp_link.get('parentNode');
162
temp_parent.removeChild(temp_link);
165
error_handler.showError = function(error_msg) {
166
Y.lp.app.errors.display_error(
167
Y.one('.menu-link-addsubscriber'), error_msg);
172
success: function() {
173
var temp_link = Y.one('#temp-username');
174
var temp_spinner = Y.one('#temp-name-spinner');
175
temp_link.removeChild(temp_spinner);
176
var anim = Y.lazr.anim.green_flash({ node: temp_link });
177
anim.on('end', function() {
178
add_user_name_link(subscription);
179
var temp_parent = temp_link.get('parentNode');
180
temp_parent.removeChild(temp_link);
184
failure: error_handler.getFailureHandler()
187
person: Y.lp.client.get_absolute_uri(
188
subscription.get('person').get('escaped_uri')),
189
suppress_notify: false
192
lp_client.named_post(bug_repr.self_link, 'subscribe', config);
196
function load_subscriber_ids() {
197
function on_success(transactionid, response, args) {
199
subscriber_ids = Y.JSON.parse(response.responseText);
201
// Fire a custom event to trigger the setting-up of the
202
// subscription handlers.
203
namespace.portlet.fire('bugs:portletsubscriberidsloaded');
205
// Fire an event to signal failure. This ensures that the
206
// subscribers-from-dupes still get loaded into the portlet.
207
namespace.portlet.fire('bugs:portletsubscriberidsfailed');
211
function on_failure() {
212
// Fire an event to signal failure. This ensures that the
213
// subscribers-from-dupes still get loaded into the portlet.
214
namespace.portlet.fire('bugs:portletsubscriberidsfailed');
217
var config = {on: {success: on_success,
218
failure: on_failure}};
220
'#subscribers-ids-link').getAttribute('href');
225
* Set click handlers for unsubscribe remove icons.
227
* @method setup_unsubscribe_icon_handlers
228
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
230
function setup_unsubscribe_icon_handlers() {
231
var subscription = new Y.lp.bugs.subscriber.Subscription({
232
link: Y.one('.menu-link-subscription'),
233
spinner: Y.one('#sub-unsub-spinner'),
234
subscriber: new Y.lp.bugs.subscriber.Subscriber({
236
subscriber_ids: subscriber_ids
240
Y.on('click', function(e) {
242
unsubscribe_user_via_icon(e.target, subscription);
248
* We can have at most one advanced subscription overlay shown,
249
* so we keep it globally to be able to clean-up before constructing
252
var subscription_overlay;
255
* Cleans-up the existing subscription overlay (if any).
257
function remove_subscription_overlay() {
258
if (Y.Lang.isValue(subscription_overlay)) {
259
subscription_overlay.get('boundingBox').remove();
261
subscription_overlay = undefined;
265
* Creates and shows a new subscription overlay for the given subscription.
267
function create_new_subscription_overlay(subscription, header_text) {
268
remove_subscription_overlay();
269
subscription_overlay = setup_advanced_subscription_overlay(
270
subscription, header_text);
271
load_and_show_advanced_subscription_overlay(
272
subscription, subscription_overlay);
276
* Set up the handlers for the mute / unmute link.
278
function setup_mute_link_handlers() {
279
if (LP.links.me === undefined) {
282
var link = Y.one('.menu-link-mute_subscription');
283
if (Y.Lang.isNull(link)) {
286
link.addClass('js-action');
287
setup_client_and_bug();
288
link.on('click', _get_toggle_mute(link));
291
// This is a helper, factored out for reusability by setup_mute_link_handlers
293
function _get_toggle_mute(link) {
294
var parent_node = link.get('parentNode');
295
var spinner = Y.one('#mute-unmute-spinner');
296
var hide_spinner = function() {
297
link.set('display', 'inline');
298
spinner.set('display', 'none');
300
var handler = new Y.lp.client.ErrorHandler();
301
handler.showError = function(error_msg) {
302
Y.lp.app.errors.display_error(link, error_msg);
304
handler.clearProgressUI = hide_spinner;
305
return function (e, do_next) {
306
if (Y.Lang.isValue(e)) {
309
var is_muted = parent_node.hasClass('muted-true');
310
var spinner_text, method_name;
312
spinner_text = 'Muting...';
313
method_name = 'mute';
315
spinner_text = 'Unmuting...';
316
method_name = 'unmute';
318
spinner.set('innerHTML', spinner_text);
319
link.set('display', 'none');
320
spinner.set('display', 'block');
323
success: function(response) {
324
is_muted = ! is_muted; // We successfully toggled.
325
var subscription = get_subscribe_self_subscription();
326
subscription.enable_spinner('Updating...');
327
var subscription_link = subscription.get('link');
328
var is_subscribed = false;
329
var is_dupe_subscribed =
330
subscription.has_duplicate_subscriptions();
331
var label = subscription_labels.SUBSCRIBE;
332
update_mute_after_subscription_change(is_muted);
334
// Remove the subscriber's name from the subscriber
335
// list, if it's there.
336
Y.lp.bugs.subscribers_list.remove_user_link(
337
subscription.get('subscriber'));
339
// When unmuting, the ``response`` is the previously
340
// muted subscription, or null.
341
is_subscribed = ! Y.Lang.isNull(response);
343
subscription.set('is_direct', true);
344
add_user_name_link(subscription);
345
label = subscription_labels.EDIT;
349
Y.lazr.anim.green_flash({ node: parent_node }).run();
350
set_subscription_link_parent_class(
351
subscription_link, is_subscribed, is_dupe_subscribed);
352
subscription.disable_spinner(label);
353
if (Y.Lang.isFunction(do_next)) {
357
failure: handler.getFailureHandler()
361
lp_client.named_post(bug_repr.self_link, method_name, config);
366
* Toggle the mute state.
369
function toggle_mute(do_next) {
370
if (LP.links.me === undefined) {
373
var link = Y.one('.menu-link-mute_subscription');
374
if (Y.Lang.isNull(link)) {
377
return _get_toggle_mute(link)(null, do_next);
381
* Update the Mute link after the user's subscriptions or mutes have
384
function update_mute_after_subscription_change(is_muted) {
385
var link = Y.one('.menu-link-mute_subscription');
386
var parent_node = link.get('parentNode');
387
if (Y.Lang.isUndefined(is_muted)) {
388
is_muted = parent_node.hasClass('muted-true');
390
parent_node.removeClass('hidden');
392
parent_node.replaceClass('muted-false', 'muted-true');
393
link.set('innerHTML', "Unmute bug mail");
394
link.replaceClass('mute', 'unmute');
396
parent_node.replaceClass('muted-true', 'muted-false');
397
link.set('innerHTML', "Mute bug mail");
398
link.replaceClass('unmute', 'mute');
403
* Set up and return a Subscription object for the direct subscription
406
function get_subscribe_self_subscription() {
407
setup_client_and_bug();
408
var subscription = new Y.lp.bugs.subscriber.Subscription({
409
link: Y.one('.menu-link-subscription'),
410
spinner: Y.one('#sub-unsub-spinner'),
411
subscriber: new Y.lp.bugs.subscriber.Subscriber({
413
subscriber_ids: subscriber_ids
417
subscription.set('can_be_unsubscribed', true);
418
subscription.set('person', subscription.get('subscriber'));
419
subscription.set('is_team', false);
420
var css_name = subscription.get('person').get('css_name');
421
var direct_css_name = '#direct-' + css_name;
422
var direct_node = Y.one(direct_css_name);
423
var is_direct = direct_node !== null;
424
subscription.set('is_direct', is_direct);
425
var dupe_css_name = '#dupe-' + css_name;
426
var dupe_node = Y.one(dupe_css_name);
427
var has_dupes = dupe_node !== null;
428
subscription.set('has_dupes', has_dupes);
434
* Set up and return a Subscription object for the team subscription
437
function get_team_subscription(team_uri) {
438
setup_client_and_bug();
439
var subscription = new Y.lp.bugs.subscriber.Subscription({
440
link: Y.one('.menu-link-subscription'),
441
spinner: Y.one('#sub-unsub-spinner'),
442
subscriber: new Y.lp.bugs.subscriber.Subscriber({
444
subscriber_ids: subscriber_ids
448
subscription.set('is_direct', true);
449
subscription.set('has_dupes', false);
450
subscription.set('can_be_unsubscribed', true);
451
subscription.set('person', subscription.get('subscriber'));
452
subscription.set('is_team', true);
457
* Initialize callbacks for subscribe/unsubscribe links.
459
* @method setup_subscription_link_handlers
461
function setup_subscription_link_handlers() {
462
if (LP.links.me === undefined) {
466
var subscription = get_subscribe_self_subscription();
468
if (subscription.is_node()) {
469
subscription.get('link').on('click', function(e) {
471
subscription.set('can_be_unsubscribed', true);
472
subscription.set('person', subscription.get('subscriber'));
473
subscription.set('is_team', false);
474
var parent = e.target.get('parentNode');
475
if (namespace.use_advanced_subscriptions) {
476
create_new_subscription_overlay(
477
subscription, "Subscribe to bug");
479
// Look for the false conditions of subscription, which
480
// is_direct_subscription, etc. don't report correctly,
481
// to make sure we only use subscribe_current_user for
483
if (parent.hasClass('subscribed-false') &&
484
parent.hasClass('dup-subscribed-false')) {
485
subscribe_current_user(subscription);
488
unsubscribe_current_user(subscription);
492
subscription.get('link').addClass('js-action');
495
setup_subscribe_someone_else_handler(subscription);
496
if (namespace.use_advanced_subscriptions) {
497
setup_mute_link_handlers();
501
function load_subscribers_from_duplicates() {
506
Y.one('#subscribers-portlet-dupe-spinner').setStyle(
509
function hide_spinner() {
510
Y.one('#subscribers-portlet-dupe-spinner').setStyle(
514
function on_failure(transactionid, response, args) {
516
// Fire a custom event to signal failure, so that
517
// any remaining unsub icons can be hooked up.
518
namespace.portlet.fire('bugs:dupeportletloadfailed');
521
function on_success(transactionid, response, args) {
524
var dupe_subscribers_container = Y.one(
525
'#subscribers-from-duplicates-container');
526
dupe_subscribers_container.set(
528
dupe_subscribers_container.get('innerHTML') +
529
response.responseText);
531
// Fire a custom portlet loaded event to notify when
532
// it's safe to setup dupe subscriber link callbacks.
533
namespace.portlet.fire('bugs:dupeportletloaded');
536
var config = {on: {success: on_success,
537
failure: on_failure}};
539
'#subscribers-from-dupes-content-link').getAttribute(
540
'href').replace('bugs.', '');
545
* Add the user name to the subscriber's list.
547
* @method add_user_name_link
549
function add_user_name_link(subscription) {
550
// Be paranoid about display_name, since timeouts or other errors
551
// could mean display_name wasn't set on initialization.
552
subscription.get('person').set_display_name(function () {
553
_add_user_name_link(subscription);
557
function _add_user_name_link(subscription) {
558
var person = subscription.get('person');
559
var link_node = build_user_link_html(subscription);
560
var subscribers = Y.one('#subscribers-links');
561
if (subscription.is_current_user_subscribing()) {
562
// If this is the current user, then top post the name and be done.
563
subscribers.insertBefore(link_node, subscribers.get('firstChild'));
565
var next = get_next_subscriber_node(subscription);
567
subscribers.insertBefore(link_node, next);
569
subscribers.appendChild(link_node);
572
// Handle the case of no previous subscribers.
573
var none_subscribers = Y.one('#none-subscribers');
574
if (none_subscribers) {
575
var none_parent = none_subscribers.get('parentNode');
576
none_parent.removeChild(none_subscribers);
578
// Highlight the new addition with a green flash.
579
Y.lazr.anim.green_flash({ node: link_node }).run();
580
// Set the click handler if adding a remove icon.
581
if (subscription.can_be_unsubscribed_by_user()) {
583
Y.one('#unsubscribe-icon-' + person.get('css_name'));
584
remove_icon.on('click', function(e) {
586
unsubscribe_user_via_icon(e.target, subscription);
592
* Unsubscribe a user from this bugtask when a remove icon is clicked.
594
* @method unsubscribe_user_via_icon
595
* @param icon {Node} The remove icon that was clicked.
596
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
598
function unsubscribe_user_via_icon(icon, subscription) {
599
icon.set('src', '/@@/spinner');
600
var icon_parent = icon.get('parentNode');
602
var user_uri = get_user_uri_from_icon(icon);
603
var person = new Y.lp.bugs.subscriber.Subscriber({
605
subscriber_ids: subscriber_ids
607
subscription.set('person', person);
609
// Determine if this is a dupe.
611
var icon_parent_div = icon_parent.get('parentNode');
612
var dupe_id = 'dupe-' + person.get('css_name');
613
if (icon_parent_div.get('id') === dupe_id) {
619
var error_handler = new Y.lp.client.ErrorHandler();
620
error_handler.clearProgressUI = function () {
621
icon.set('src', '/@@/remove');
622
// Grab the icon again to reset to click handler.
623
var unsubscribe_icon = Y.one(
624
'#unsubscribe-icon-' + person.get('css_name'));
625
unsubscribe_icon.on('click', function(e) {
627
unsubscribe_user_via_icon(e.target, subscription);
631
error_handler.showError = function (error_msg) {
632
var flash_node = Y.one('.' + person.get('css_name'));
633
Y.lp.app.errors.display_error(flash_node, error_msg);
637
var subscription_link = subscription.get('link');
640
success: function(client) {
641
var num_person_links = Y.all(
642
'.' + person.get('css_name')).size();
643
Y.lp.bugs.subscribers_list.remove_user_link(person, is_dupe);
644
var has_direct, has_dupes;
645
if (num_person_links === 1 &&
646
subscription.is_current_user_subscribing()) {
647
// Current user has been completely unsubscribed.
648
subscription.disable_spinner(
649
subscription_labels.SUBSCRIBE);
653
// If we removed the duplicate subscription,
654
// we are left with the direct one, and vice versa.
655
has_direct = is_dupe;
656
has_dupes = !is_dupe;
658
subscription.set('is_direct', has_direct);
659
subscription.set('has_dupes', has_dupes);
660
set_subscription_link_parent_class(
661
subscription_link, has_direct, has_dupes);
664
failure: error_handler.getFailureHandler()
668
if (!subscription.is_current_user_subscribing()) {
669
config.parameters = {
670
person: Y.lp.client.get_absolute_uri(user_uri)
675
lp_client.named_post(
676
bug_repr.self_link, 'unsubscribeFromDupes', config);
678
lp_client.named_post(bug_repr.self_link, 'unsubscribe', config);
683
* Create and return a FormOverlay for advanced subscription
686
* @method setup_advanced_subscription_overlay
687
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
689
function setup_advanced_subscription_overlay(subscription, text) {
690
var header = Y.Node.create('<h2 />').set('text', text);
691
var subscription_overlay = new Y.lazr.FormOverlay({
692
headerContent: header,
694
Y.Node.create(submit_button_html),
696
Y.Node.create(cancel_button_html),
700
subscription_overlay.render('#privacy-form-container');
701
return subscription_overlay;
705
* Load the content for and display the advanced subscription overlay.
706
* The call to show() the overlay happens only when the form has been
707
* loaded. That way the overlay won't appear empty.
709
* @method load_and_show_advanced_subscription_overlay
710
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
711
* @param subscription_overlay {Object} A Y.lazr.FormOverlay to load
714
function load_and_show_advanced_subscription_overlay(subscription,
715
subscription_overlay) {
716
subscription.enable_spinner('Loading...');
717
subscription_overlay.set(
718
'form_submit_callback', function(form_data) {
719
handle_advanced_subscription_overlay(form_data);
720
subscription_overlay.hide();
723
var subscription_link_url = subscription.get(
724
'link').get('href') + '/++form++';
725
subscription_overlay.loadFormContentAndRender(subscription_link_url);
727
Y.on('contentready', function() {
728
Y.lp.bugs.bug_notification_level.setup();
729
}, '.bug-notification-level-field, .bug-subscription-basic');
731
// Show the overlay when the special event indicating that the form is
732
// ready to be displayed is received.
733
Y.on(['bugnotificationlevel:contentready', 'io:failure'], function() {
734
subscription.disable_spinner();
735
subscription_overlay.show();
740
* Subscribe the current user via the LP API.
742
* @method subscribe_current_user
743
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
745
function subscribe_current_user(subscription) {
746
subscription.enable_spinner('Subscribing...');
747
var subscription_link = subscription.get('link');
748
var subscriber = subscription.get('subscriber');
749
var bug_notification_level = subscription.get('bug_notification_level');
751
// This is always a direct subscription.
752
subscription.set('is_direct', true);
754
var error_handler = new Y.lp.client.ErrorHandler();
755
error_handler.clearProgressUI = function () {
756
subscription.disable_spinner();
758
error_handler.showError = function (error_msg) {
759
Y.lp.app.errors.display_error(subscription_link, error_msg);
764
success: function(client) {
765
if (namespace.use_advanced_subscriptions) {
766
subscription.disable_spinner(
767
subscription_labels.EDIT);
769
subscription.disable_spinner(
770
subscription_labels.UNSUBSCRIBE);
773
if (subscription.has_duplicate_subscriptions()) {
774
set_subscription_link_parent_class(
775
subscription_link, true, true);
777
set_subscription_link_parent_class(
778
subscription_link, true, false);
781
// Handle the case where the subscriber's list displays
783
var empty_subscribers = Y.one("#none-subscribers");
784
if (empty_subscribers) {
785
var parent = empty_subscribers.get('parentNode');
786
parent.removeChild(empty_subscribers);
789
add_user_name_link(subscription);
790
if (namespace.use_advanced_subscriptions) {
791
update_mute_after_subscription_change();
795
failure: error_handler.getFailureHandler()
799
person: Y.lp.client.get_absolute_uri(
800
subscriber.get('escaped_uri')),
801
suppress_notify: false,
802
level: bug_notification_level
805
lp_client.named_post(bug_repr.self_link, 'subscribe', config);
809
* Unsubscribe the current user via the LP API.
811
* @method unsubscribe_current_user
812
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
814
function unsubscribe_current_user(subscription) {
815
subscription.enable_spinner('Unsubscribing...');
816
var subscription_link = subscription.get('link');
817
var subscriber = subscription.get('subscriber');
819
var error_handler = new Y.lp.client.ErrorHandler();
820
error_handler.clearProgressUI = function () {
821
subscription.disable_spinner();
823
error_handler.showError = function (error_msg) {
824
Y.lp.app.errors.display_error(subscription_link, error_msg);
827
var subscriber_link = Y.lp.client.get_absolute_uri(
828
subscriber.get('escaped_uri'));
831
success: function(client) {
832
// We need to track if this is direct or not,
833
// before we toggle the is_direct flag.
834
var original_is_direct = subscription.get('is_direct');
836
if (subscription.is_direct_subscription() &&
837
subscription.has_duplicate_subscriptions()) {
838
// Don't change the 'Unsubscribe' text if
839
// dupe subscriptions remain.
840
subscription.disable_spinner();
841
set_subscription_link_parent_class(
842
subscription_link, false, true);
843
subscription.set('is_direct', false);
844
} else if (subscription.is_direct_subscription() &&
845
!subscription.has_duplicate_subscriptions()) {
846
// Only unsub'ing a direct subscriber here.
847
subscription.disable_spinner(
848
subscription_labels.SUBSCRIBE);
849
set_subscription_link_parent_class(
850
subscription_link, false, false);
851
subscription.set('is_direct', false);
853
// Only unsub'ing dupes here.
854
subscription.disable_spinner(
855
subscription_labels.SUBSCRIBE);
856
set_subscription_link_parent_class(
857
subscription_link, false, false);
858
subscription.set('has_dupes', false);
861
var is_dupe = !original_is_direct;
862
Y.lp.bugs.subscribers_list.remove_user_link(
863
subscriber, is_dupe);
866
failure: error_handler.getFailureHandler()
869
parameters: { person: subscriber_link }
872
// A team must be unsubcribed from both the current bug and from
873
// duplicates. This configuration handles the first and then
874
// chains to the other upon success.
877
success: function(client) {
878
lp_client.named_post(
879
bug_repr.self_link, 'unsubscribeFromDupes', config);
881
failure: error_handler.getFailureHandler()
883
parameters: { person: subscriber_link }
886
if (subscription.is_team()){
887
lp_client.named_post(bug_repr.self_link, 'unsubscribe', team_config);
890
if (subscription.is_direct_subscription()) {
891
lp_client.named_post(bug_repr.self_link, 'unsubscribe', config);
893
lp_client.named_post(
894
bug_repr.self_link, 'unsubscribeFromDupes', config);
900
* Initialize click handler for the subscribe someone else link.
902
* @method setup_subscribe_someone_else_handler
903
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
905
function setup_subscribe_someone_else_handler(subscription) {
907
header: 'Subscribe someone else',
908
step_title: 'Search',
909
picker_activator: '.menu-link-addsubscriber'
912
config.save = function(result) {
913
subscribe_someone_else(result, subscription);
915
var picker = Y.lp.app.picker.create('ValidPersonOrTeam', config);
919
* Build the HTML for a user link for the subscribers list.
921
* @method build_user_link_html
922
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
923
* @return html {String} The HTML used for creating a subscriber link.
925
function build_user_link_html(subscription) {
926
var name = subscription.get('person').get('name');
927
var css_name = subscription.get('person').get('css_name');
928
var full_name = subscription.get('person').get('full_display_name');
929
var display_name = subscription.get('person').get('display_name');
933
display_name: display_name,
937
if (subscription.is_current_user_subscribing()) {
938
terms.subscribed_by = 'themselves';
940
terms.subscribed_by = 'by ' + full_name;
943
var html = Y.Node.create('<div><a></a></div>');
944
html.addClass(terms.css_name);
946
if (subscription.has_duplicate_subscriptions()) {
947
html.set('id', 'dupe-' + terms.css_name);
949
html.set('id', 'direct-' + terms.css_name);
953
.set('href', '/~' + terms.name)
954
.set('name', terms.full_name)
955
.set('title', 'Subscribed ' + terms.subscribed_by);
958
if (subscription.is_team()) {
959
span = '<span class="sprite team"></span>';
961
span = '<span class="sprite person"></span>';
965
.appendChild(Y.Node.create(span))
966
.appendChild(document.createTextNode(terms.display_name));
968
// Add remove icon if the current user can unsubscribe the subscriber.
969
if (subscription.can_be_unsubscribed_by_user()) {
970
var icon_html = Y.Node.create(
971
'<a href="+subscribe">' +
972
'<img class="unsub-icon" src="/@@/remove" alt="Remove" /></a>');
974
.set('id', 'unsubscribe-' + terms.css_name)
975
.set('title', 'Unsubscribe ' + terms.full_name);
977
.set('id', 'unsubscribe-icon-' + terms.css_name);
978
html.appendChild(icon_html);
985
* Returns the next node in alphabetical order after the subscriber
986
* node now being added. No node is returned to append to end of list.
988
* The name can appear in one of two different lists. 1) The list of
989
* subscribers that can be unsubscribed by the current user, and
990
* 2) the list of subscribers that cannot be unsubscribed.
992
* @method get_next_subscriber_node
993
* @param subscription_link {Node} The sub/unsub link.
994
* @return {Node} The node appearing next in the subscriber list or
995
* undefined if no node is next.
997
function get_next_subscriber_node(subscription) {
998
var full_name = subscription.get('person').get('full_display_name');
999
var can_be_unsubscribed = subscription.can_be_unsubscribed_by_user();
1000
var nodes_by_name = {};
1001
var unsubscribables = [];
1002
var not_unsubscribables = [];
1004
// Use the list of subscribers pulled from the DOM to have sortable
1005
// lists of unsubscribable vs. not unsubscribable person links.
1006
var all_subscribers = Y.all('#subscribers-links div');
1007
if (all_subscribers.size() > 0) {
1008
all_subscribers.each(function(sub_link) {
1009
if (sub_link.getAttribute('id') !== 'temp-username') {
1010
// User's displayname is found via the link's "name"
1012
var sub_link_name = sub_link.one('a').getAttribute('name');
1013
nodes_by_name[sub_link_name] = sub_link;
1014
if (sub_link.one('img.unsub-icon')) {
1015
unsubscribables.push(sub_link_name);
1017
not_unsubscribables.push(sub_link_name);
1022
// Add the current subscription.
1023
if (can_be_unsubscribed) {
1024
unsubscribables.push(full_name);
1026
not_unsubscribables.push(full_name);
1028
unsubscribables.sort();
1029
not_unsubscribables.sort();
1031
// If there is no all_subscribers, then we're dealing with
1032
// the printed None, so return.
1037
if ((!unsubscribables && !not_unsubscribables) ||
1038
// If A) neither list exists, B) the user belongs in the second
1039
// list but the second list doesn't exist, or C) user belongs in the
1040
// first list and the second doesn't exist, return no node to append.
1041
(!can_be_unsubscribed && !not_unsubscribables) ||
1042
(can_be_unsubscribed && unsubscribables && !not_unsubscribables)) {
1045
// If the user belongs in the first list, and the first list
1046
// doesn't exist, but the second one does, return the first node
1047
// in the second list.
1048
can_be_unsubscribed && !unsubscribables && not_unsubscribables) {
1049
return nodes_by_name[not_unsubscribables[0]];
1050
} else if (can_be_unsubscribed) {
1051
// If the user belongs in the first list, loop the list for position.
1052
for (i=0; i<unsubscribables.length; i++) {
1053
if (unsubscribables[i] === full_name) {
1054
if (i+1 < unsubscribables.length) {
1055
return nodes_by_name[unsubscribables[i+1]];
1056
// If the current link should go at the end of the first
1057
// list and we're at the end of that list, return the
1058
// first node of the second list. Due to earlier checks
1059
// we can be sure this list exists.
1060
} else if (i+1 >= unsubscribables.length) {
1061
return nodes_by_name[not_unsubscribables[0]];
1065
} else if (!can_be_unsubscribed) {
1066
// If user belongs in the second list, loop the list for position.
1067
for (i=0; i<not_unsubscribables.length; i++) {
1068
if (not_unsubscribables[i] === full_name) {
1069
if (i+1 < not_unsubscribables.length) {
1070
return nodes_by_name[not_unsubscribables[i+1]];
1080
* Traverse the DOM of a given remove icon to find
1081
* the user's link. Returns a URI of the form "/~username".
1083
* @method get_user_uri_from_icon
1084
* @param icon {Node} The node representing a remove icon.
1085
* @return user_uri {String} The user's uri, without the hostname.
1087
function get_user_uri_from_icon(icon) {
1088
var parent_div = icon.get('parentNode').get('parentNode');
1089
// This should be parent_div.firstChild, but because of #text
1090
// and cross-browser issues, using the YUI query syntax is
1092
var user_uri = parent_div.one('a').getAttribute('href');
1094
// Strip the domain off. We just want a path.
1095
var host_start = user_uri.indexOf('//');
1096
if (host_start !== -1) {
1097
var host_end = user_uri.indexOf('/', host_start+2);
1098
return user_uri.substring(host_end, user_uri.length);
1105
* Set the class on subscription link's parentNode.
1107
* This is used to reset the class used by the
1108
* click handler to know which link was clicked.
1110
* @method set_subscription_link_parent_class
1111
* @param subscription_link {Node} The sub/unsub link.
1112
* @param subscribed {Boolean} The sub/unsub'ed flag for the class.
1113
* @param dupe_subscribed {Boolean} The sub/unsub'ed flag for dupes
1116
function set_subscription_link_parent_class(
1117
user_link, subscribed, dupe_subscribed) {
1119
var parent = user_link.get('parentNode');
1121
parent.removeClass('subscribed-false');
1122
parent.addClass('subscribed-true');
1124
parent.removeClass('subscribed-true');
1125
parent.addClass('subscribed-false');
1128
if (dupe_subscribed) {
1129
parent.removeClass('dup-subscribed-false');
1130
parent.addClass('dup-subscribed-true');
1132
parent.removeClass('dup-subscribed-true');
1133
parent.addClass('dup-subscribed-false');
1139
* Handle the advanced_subscription_overlay's form submissions.
1141
* @method handle_advanced_subscription_overlay
1142
* @param form_data {Object} The data from the submitted form.
1144
function handle_advanced_subscription_overlay(form_data) {
1146
var mute_link = Y.one('.menu-link-mute_subscription');
1147
var parent_node = mute_link.get('parentNode');
1148
var is_muted = parent_node.hasClass('muted-true');
1149
var requested_subscriber;
1150
var request_for_self = false;
1151
// XXX Danilo 20110422: this is a very lousy "special string"
1152
// that will make it break for a team/person named 'update-subscription'.
1153
// We should probably use special characters not allowed in team names
1154
// (maybe all-uppercase would work).
1155
var UPDATE_ACTION = 'update-subscription';
1157
if (form_data['field.subscription'][0] === UPDATE_ACTION) {
1158
requested_subscriber = LP.links.me;
1159
request_for_self = true;
1161
requested_subscriber = '/~' + form_data['field.subscription'][0];
1162
if (requested_subscriber === LP.links.me) {
1163
request_for_self = true;
1166
if (request_for_self) {
1167
subscription = get_subscribe_self_subscription();
1169
subscription = get_team_subscription(requested_subscriber);
1171
var link = subscription.get('link');
1172
var link_parent = link.get('parentNode');
1174
if (form_data['field.subscription'][0] === UPDATE_ACTION) {
1175
// The user is already subscribed or is muted and wants to
1176
// update their subscription.
1177
setup_client_and_bug();
1178
var person_name = subscription.get('person').get('name');
1179
var subscription_url =
1180
lp_bug_entry.get('self_link') + '/+subscription/' +
1182
var update_subscription = function(lp_subscription) {
1183
subscription.enable_spinner('Updating subscription...');
1184
lp_subscription.set(
1185
'bug_notification_level',
1186
form_data['field.bug_notification_level'][0]);
1189
success: function(e) {
1190
subscription.disable_spinner(
1191
'Edit subscription');
1192
link_parent.addClass('subscribed-true');
1193
link_parent.removeClass('subscribed-false');
1194
var anim = Y.lazr.anim.green_flash({
1199
failure: function(e) {
1200
subscription.disable_spinner(
1201
'Edit subscription');
1202
var anim = Y.lazr.anim.red_flash({
1209
lp_subscription.lp_save(save_config);
1212
toggle_mute(function(lp_subscription) {
1213
if (Y.Lang.isValue(lp_subscription)) {
1214
update_subscription(lp_subscription);
1217
'bug_notification_level',
1218
form_data['field.bug_notification_level']);
1219
subscribe_current_user(subscription);
1225
{on: {success: update_subscription}}
1229
} else if (link_parent.hasClass('subscribed-false') &&
1230
link_parent.hasClass('dup-subscribed-false') &&
1231
!is_muted && request_for_self) {
1232
// The user isn't subscribed or muted, and the request is
1233
// for himself (iow, not for a team).
1235
'bug_notification_level',
1236
form_data['field.bug_notification_level']);
1237
subscribe_current_user(subscription);
1238
} else if (is_muted && request_for_self) {
1239
// When a person has a bug muted, we show 2+ options:
1240
// a. unmute, b. unmute and subscribe, c. unsubscribe team1...
1241
// "b" is treated as 'update-subscription' case, and for any
1242
// of the teams, request_for_self won't be true.
1245
// Unsubscribe this person/team.
1246
unsubscribe_current_user(subscription);
1251
* Subscribe a person or team other than the current user.
1252
* This is a callback for the subscribe someone else picker.
1254
* @method subscribe_someone_else
1255
* @result {Object} The object representing a person returned by the API.
1257
function subscribe_someone_else(result, subscription) {
1258
var person = new Y.lp.bugs.subscriber.Subscriber({
1259
uri: result.api_uri,
1260
display_name: result.title,
1261
subscriber_ids: subscriber_ids
1263
subscription.set('person', person);
1265
var error_handler = new Y.lp.client.ErrorHandler();
1266
error_handler.showError = function(error_msg) {
1267
Y.lp.app.errors.display_error(
1268
Y.one('.menu-link-addsubscriber'), error_msg);
1271
if (subscription.is_already_subscribed()) {
1272
error_handler.showError(
1273
subscription.get('person').get('full_display_name') +
1274
' has already been subscribed');
1276
check_can_be_unsubscribed(subscription);
1281
* Check if the current user can unsubscribe the person
1284
* This must be done in JavaScript, since the subscription
1285
* hasn't completed yet, and so, can_be_unsubscribed_by_user
1288
* @method check_can_be_unsubscribed
1289
* @param subscription {Object} A Y.lp.bugs.subscriber.Subscription object.
1291
function check_can_be_unsubscribed(subscription) {
1292
var error_handler = new Y.lp.client.ErrorHandler();
1293
error_handler.showError = function (error_msg) {
1294
Y.lp.app.errors.display_error(
1295
Y.one('.menu-link-addsubscriber'), error_msg);
1300
success: function(result) {
1301
var is_team = result.get('is_team');
1302
subscription.set('is_team', is_team);
1303
var final_config = {
1305
success: function(result) {
1306
var team_member = false;
1308
for (i=0; i<result.entries.length; i++) {
1309
if (result.entries[i].get('member_link') ===
1310
Y.lp.client.get_absolute_uri(
1312
'subscriber').get('uri'))) {
1318
subscription.set('can_be_unsubscribed', true);
1319
add_temp_user_name(subscription);
1320
if (namespace.use_advanced_subscriptions) {
1321
update_mute_after_subscription_change();
1325
'can_be_unsubscribed', false);
1326
add_temp_user_name(subscription);
1330
failure: error_handler.getFailureHandler()
1335
// Get a list of members to see if current user
1336
// is a team member.
1337
var members = result.get(
1338
'members_details_collection_link');
1339
lp_client.get(members, final_config);
1341
subscription.set('can_be_unsubscribed', false);
1342
add_temp_user_name(subscription);
1346
failure: error_handler.getFailureHandler()
1349
lp_client.get(Y.lp.client.get_absolute_uri(
1350
subscription.get('person').get('escaped_uri')), config);
1354
* Add a grayed out, temporary user name when subscribing
1357
* @method add_temp_user_name
1358
* @param subscription_link {Node} The sub/unsub link.
1360
function add_temp_user_name(subscription) {
1361
// Be paranoid about display_name, since timeouts or other errors
1362
// could mean display_name wasn't set on initialization.
1363
subscription.get('person').set_display_name(function () {
1364
_add_temp_user_name(subscription);
1368
function _add_temp_user_name(subscription) {
1369
var display_name = subscription.get('person').get('display_name');
1371
if (subscription.is_team()) {
1372
img_src = '/@@/teamgray';
1374
img_src = '/@@/persongray';
1377
// The <span>...</span> below must *not* be <span/>. On FF (maybe
1378
// others, but at least on FF 3.0.11) will then not notice any
1379
// following sibling nodes, like the spinner image.
1380
var link_node = Y.Node.create([
1381
'<div id="temp-username"> ',
1382
' <img alt="" width="14" height="14" />',
1383
' <span>Other Display Name</span>',
1384
' <img id="temp-name-spinner" src="/@@/spinner" alt="" ',
1385
' style="position:absolute;right:8px" /></div>'].join(''));
1386
link_node.one('img').set('src', img_src);
1387
link_node.replaceChild(
1388
document.createTextNode(display_name),
1389
link_node.one('span'));
1391
var subscribers = Y.one('#subscribers-links');
1392
var next = get_next_subscriber_node(subscription);
1394
subscribers.insertBefore(link_node, next);
1396
// Handle the case of no subscribers.
1397
var none_subscribers = Y.one('#none-subscribers');
1398
if (none_subscribers) {
1399
var none_parent = none_subscribers.get('parentNode');
1400
none_parent.removeChild(none_subscribers);
1402
subscribers.appendChild(link_node);
1405
// Fire a custom event to know it's safe to begin
1406
// any actual subscribing work.
1407
namespace.portlet.fire('bugs:nameloaded', subscription);
1410
}, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
1411
"json-parse", "substitute", "widget-position-ext",
1412
"lazr.formoverlay", "lazr.anim", "lazr.base",
1413
"lazr.overlay", "lazr.choiceedit", "lp.app.picker",
1415
"lp.client.plugins", "lp.bugs.subscriber",
1416
"lp.bugs.subscribers_list",
1417
"lp.bugs.bug_notification_level", "lp.app.errors"]});