Merge lp:~danilo/launchpad/bug728370-descriptions into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 12835
Proposed branch: lp:~danilo/launchpad/bug728370-descriptions
Merge into: lp:launchpad
Prerequisite: lp:~danilo/launchpad/bug728370-direct-subs
Diff against target: 315 lines (+171/-20)
4 files modified
lib/lp/bugs/javascript/subscription.js (+57/-7)
lib/lp/bugs/javascript/tests/test_subscription.js (+80/-1)
lib/lp/bugs/templates/bug-subscription-list.pt (+20/-3)
lib/lp/registry/javascript/structural-subscription.js (+14/-9)
To merge this branch: bzr merge lp:~danilo/launchpad/bug728370-descriptions
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+57656@code.launchpad.net

Commit message

[r=allenap][bug=728370][incr] Make bug subscription descriptions nicer and collapse all the non-direct non-personal along with structural subscriptions into "other subscriptions" box. You do what?

Description of the change

= Bug 728370: prettyfication =

This branch prettyfies the listing of bug subscriptions on a single bug +subscriptions page. So far, we've only rendered all the potential descriptions one below the other, and this branch puts each one of them inside a separate box similar to the boxes structural subscriptions are in.

Follow-up work will introduce actions inside these "boxes" (such as "unsubscribe this team from duplicate bug").

== Implementation details ==

We are not touching structural subscriptions, other than putting them inside the "other subscriptions" slide-out box. This uncovered a few subtle annoyances in structural-subscription setup code which cleared the entire content div (and/or hard-coded it, which is why it worked), when it should have only gotten rid of the temporary overlay div. These are the only changes in structural-subscription.js and the tests still pass.

In the change itself, we only extend get_other_subscriptions_node to provide a clickable header that slides out the box. We also include some CSS to render other subscriptions in the same way as structural ones. Unfortunately, it's not trivial to reuse exactly the same CSS.

== Tests ==

lib/lp/bugs/javascript/tests/test_subscription.html
lib/lp/registry/javascript/tests/test_structural_subscription.html

== Demo and Q/A ==

See https://devpad.canonical.com/~danilo/screencasts/other-subscriptions.ogv

https://bugs.launchpad.dev/firefox/+bug/1/+subscriptions

To facilitate the demo:

  - set 'malone.advanced-structural-subscriptions.enabled default 1 on'
    on https://launchpad.dev/+feature-rules
  - add a few structural subscriptions to firefox using
      https://bugs.launchpad.dev/firefox/+subscriptions
  - add a few structural subscriptions for firefox package using
      https://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+subscriptions
  - make bug #2 duplicate of #1, subscribe one of your teams to #2
  - subscribe yourself to bug #1
  ...

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/javascript/subscription.js
  lib/lp/bugs/javascript/tests/test_subscription.js
  lib/lp/bugs/templates/bug-subscription-list.pt
  lib/lp/registry/javascript/structural-subscription.js

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Wow, the subscriptions stuff is looking *awesome* these days.

This branch looks good. I have a few comments, but nothing major.

[1]

+ var header_link = Y.Node.create('<a></a>')
+ .set('href', '#')

Is this needed? Ah, maybe for IE?

[2]

+ Y.Assert.areNotSame(
+ undefined,

Y.Assert.isNotUndefined() is slightly more concise.

[3]

+ var node = module._get_other_descriptions_node(info);
+ var link = node.one('#other-subscriptions-header a');
+ var list = node.one('#other-subscriptions-list');

IDs are /meant/ to be unique in the page so you could skip the first
line above and just use Y.one().

[4]

+ this.wait(function() {
[...]
+ this.wait(function() {
[...]
+ }, 500);
+ }, 500);

Are these delays long enough? How long is the animation meant to take?

[5]

- ss_module.setup_bug_subscriptions(
- {content_box: "#structural-subscription-content-box"});
               info_module.show_subscription_description(
                   {description_box: "#subscription-description-box"});
           });
+ Y.on('contentready', function() {
+ ss_module.setup_bug_subscriptions(
+ {content_box: "#other-subscriptions-list"});
+ }, '#other-subscriptions-list');

http://developer.yahoo.com/yui/3/examples/event/event-timing.html
seems to suggest that it's not safe to modify the DOM until domready
fires:

    The 'contentready' event fired for the element
    'contentContainer'. That element and all of its children are
    present in the DOM.

    The 'domready' event fired. The DOM is now safe to modify via
    script.

setup_bug_subscriptions() calls at least one function that modifies
the DOM, so I suspect this change should be reverted.

[6]

+ content_node = content_node.one('#overlay-container');

Like before, and in a few other places, you can just use Y.one() here,
not that it matters much.

Becauase IDs are, afaik, meant to be unique in a DOM tree, I dislike
them; they're like the global variables of the DOM, and I try to avoid
using them, especially in widgets. Do you have any opinions either
way, or are you aware of anyone else's?

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :
Download full text (4.3 KiB)

У чет, 14. 04 2011. у 13:22 +0000, Gavin Panella пише:
> Review: Approve
> Wow, the subscriptions stuff is looking *awesome* these days.

Thanks. It's all the yellow-goo over it :)
Though, we are building on the sound foundation you guys did
previously :)

> This branch looks good. I have a few comments, but nothing major.
>
>
> [1]
>
> + var header_link = Y.Node.create('<a></a>')
> + .set('href', '#')
>
> Is this needed? Ah, maybe for IE?

Not needed at all, other than the mouse pointer doesn't turn into a
"click me" pointer. Basically, the <a> tag is considered an anchor
instead of a link. I am thinking of keeping it, any better suggestions?

>
> [2]
>
> + Y.Assert.areNotSame(
> + undefined,
>
> Y.Assert.isNotUndefined() is slightly more concise.

Ah, thanks. Fixed.

>
> [3]
>
> + var node = module._get_other_descriptions_node(info);
> + var link = node.one('#other-subscriptions-header a');
> + var list = node.one('#other-subscriptions-list');
>
> IDs are /meant/ to be unique in the page so you could skip the first
> line above and just use Y.one().

Except that the stuff is not added to the page DOM yet. The method
returns a node which other methods are in charge of adding to the page:
and since this is trying to be more of a unit test, I wouldn't want to
add it.

> [4]
>
> + this.wait(function() {
> [...]
> + this.wait(function() {
> [...]
> + }, 500);
> + }, 500);
>
> Are these delays long enough? How long is the animation meant to take?

They consistently worked for me (and anything lower consistently failed
for me). It's taking whatever the default is for animation. I'll check
what it is and use that + some small leverage.

>
> [5]
>
> - ss_module.setup_bug_subscriptions(
> - {content_box: "#structural-subscription-content-box"});
> info_module.show_subscription_description(
> {description_box: "#subscription-description-box"});
> });
> + Y.on('contentready', function() {
> + ss_module.setup_bug_subscriptions(
> + {content_box: "#other-subscriptions-list"});
> + }, '#other-subscriptions-list');
>
> http://developer.yahoo.com/yui/3/examples/event/event-timing.html
> seems to suggest that it's not safe to modify the DOM until domready
> fires:
>
> The 'contentready' event fired for the element
> 'contentContainer'. That element and all of its children are
> present in the DOM.
>
> The 'domready' event fired. The DOM is now safe to modify via
> script.
>
> setup_bug_subscriptions() calls at least one function that modifies
> the DOM, so I suspect this change should be reverted.

Well, it's put inside 'contentready' for #other-subscriptions-list,
which is constructed in show_subscription_description, which is called
in 'domready' event. '#other-subscriptions-list' is *not* present in
the DOM before show_subscription_description() runs.

>
> [6]
>
> + content_node = content_node.one('#overlay-container');
>
> Like before, and in a few other places, you can just use Y.one() here,
...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/javascript/subscription.js'
--- lib/lp/bugs/javascript/subscription.js 2011-04-13 15:47:48 +0000
+++ lib/lp/bugs/javascript/subscription.js 2011-04-14 15:39:15 +0000
@@ -100,8 +100,7 @@
100100
101/* These are the owner variations. */101/* These are the owner variations. */
102var _OWNER = (102var _OWNER = (
103 "the owner of the {pillar_type} " +103 "the owner of {pillar}, which has no bug supervisor.");
104 "{pillar}, which has no bug supervisor.");
105/* These are the actual strings to use. */104/* These are the actual strings to use. */
106Y.mix(reasons, {105Y.mix(reasons, {
107 YOU_OWNER: _BECAUSE_YOU_ARE + _OWNER,106 YOU_OWNER: _BECAUSE_YOU_ARE + _OWNER,
@@ -584,22 +583,73 @@
584 */583 */
585function get_other_descriptions_node(info, extra_data) {584function get_other_descriptions_node(info, extra_data) {
586 var subs = gather_nondirect_subscriptions(info);585 var subs = gather_nondirect_subscriptions(info);
587 if (subs.length > 0) {586 if (subs.length > 0 || has_structural_subscriptions()) {
588 var other_node = Y.Node.create('<div></div>')587 var node = Y.Node.create('<div></div>')
589 .set('id', 'other-subscriptions');588 .set('id', 'other-subscriptions');
589 var header = Y.Node.create('<div></div>')
590 .set('id', 'other-subscriptions-header');
591 var header_link = Y.Node.create('<a></a>')
592 .set('href', '#')
593 .set('text', 'Other subscriptions');
594 header.appendChild(header_link);
595 node.appendChild(header);
596 var list = Y.Node.create('<div></div>')
597 .set('id', 'other-subscriptions-list');
598 node.appendChild(list);
599
600 setup_slider(list, header_link);
590601
591 for (var index in subs) {602 for (var index in subs) {
592 other_node.appendChild(603 list.appendChild(
593 get_single_description_node(subs[index], extra_data));604 get_single_description_node(subs[index], extra_data));
594 }605 }
595606
596 return other_node;607 return node;
597 } else {608 } else {
598 return undefined;609 return undefined;
599 }610 }
600}611}
601namespace._get_other_descriptions_node = get_other_descriptions_node;612namespace._get_other_descriptions_node = get_other_descriptions_node;
602613
614/**
615 * Are there any structural subscriptions that need to be rendered.
616 */
617function has_structural_subscriptions() {
618 return (LP.cache.subscription_info &&
619 LP.cache.subscription_info.length > 0);
620}
621
622/**
623 * Sets up a slider that slides the `body` in and out when `header`
624 * is clicked.
625 */
626function setup_slider(body, header) {
627 // Hide the widget body contents.
628 body.addClass('lazr-closed');
629 body.setStyle('display', 'none');
630
631 // Ensure that the widget header uses the correct sprite icon
632 // and gets the styling for javascript actions applied.
633 header.addClass('sprite');
634 header.addClass('treeCollapsed');
635 header.addClass('js-action');
636
637 var slide;
638 function toggle_body_visibility(e) {
639 e.halt();
640 if (!slide) {
641 slide = Y.lazr.effects.slide_out(body);
642 header.replaceClass('treeCollapsed', 'treeExpanded');
643 } else {
644 slide.set('reverse', !slide.get('reverse'));
645 header.toggleClass('treeExpanded');
646 header.toggleClass('treeCollapsed');
647 }
648 slide.stop();
649 slide.run();
650 }
651 header.on('click', toggle_body_visibility);
652}
603653
604/**654/**
605 * Add descriptions for all non-structural subscriptions to the page.655 * Add descriptions for all non-structural subscriptions to the page.
@@ -632,5 +682,5 @@
632namespace.show_subscription_description = show_subscription_description682namespace.show_subscription_description = show_subscription_description
633683
634}, '0.1', {requires: [684}, '0.1', {requires: [
635 'dom', 'node', 'substitute'685 'dom', 'event', 'node', 'substitute', 'lazr.effects',
636]});686]});
637687
=== modified file 'lib/lp/bugs/javascript/tests/test_subscription.js'
--- lib/lp/bugs/javascript/tests/test_subscription.js 2011-04-13 14:19:19 +0000
+++ lib/lp/bugs/javascript/tests/test_subscription.js 2011-04-14 15:39:15 +0000
@@ -1149,6 +1149,7 @@
1149 as_owner: _constructCategory(),1149 as_owner: _constructCategory(),
1150 count: 11150 count: 1
1151 };1151 };
1152 window.LP = { cache: {} };
1152 Y.Assert.areSame(1153 Y.Assert.areSame(
1153 undefined,1154 undefined,
1154 module._get_other_descriptions_node(info));1155 module._get_other_descriptions_node(info));
@@ -1191,6 +1192,84 @@
1191 2, node.all('.subscription-description').size());1192 2, node.all('.subscription-description').size());
1192 },1193 },
11931194
1195 test_no_direct_has_structural_subscriptions: function() {
1196 // With no non-personal subscriptions, and a structural
1197 // subscription, the node is still constructed because
1198 // structural subscriptions go there as well.
1199 var info = {
1200 direct: _constructCategory([{ bug: {} }]),
1201 from_duplicate: _constructCategory(),
1202 as_assignee: _constructCategory(),
1203 as_owner: _constructCategory(),
1204 count: 1
1205 };
1206 window.LP = { cache: { subscription_info: ['1'] } };
1207 Y.Assert.isNotUndefined(
1208 module._get_other_descriptions_node(info));
1209 },
1210
1211 test_header: function() {
1212 // There is a subscription on the duplicate bug.
1213 var info = {
1214 direct: _constructCategory(),
1215 from_duplicate: _constructCategory([{ bug: {id: 1} }]),
1216 as_assignee: _constructCategory(),
1217 as_owner: _constructCategory(),
1218 count: 1
1219 };
1220
1221 // A returned node contains the 'other-subscriptions-header'
1222 // div with the link.
1223 var node = module._get_other_descriptions_node(info);
1224 var header = node.one('#other-subscriptions-header');
1225 Y.Assert.isNotUndefined(header);
1226 var link = header.one('a');
1227 Y.Assert.areEqual('Other subscriptions', link.get('text'));
1228 },
1229
1230 test_header_slideout: function() {
1231 // Clicking on the header slides-out the box, and
1232 // clicking it again slides it back in.
1233 var info = {
1234 direct: _constructCategory(),
1235 from_duplicate: _constructCategory([{ bug: {id: 1} }]),
1236 as_assignee: _constructCategory(),
1237 as_owner: _constructCategory(),
1238 count: 1
1239 };
1240
1241 // A returned node contains the 'other-subscriptions-header'
1242 // div with the link.
1243 var node = module._get_other_descriptions_node(info);
1244 var link = node.one('#other-subscriptions-header a');
1245 var list = node.one('#other-subscriptions-list');
1246
1247 // Initially, the list is hidden.
1248 Y.Assert.isTrue(link.hasClass('treeCollapsed'));
1249 Y.Assert.isTrue(list.hasClass('lazr-closed'));
1250 Y.Assert.areEqual('none', list.getStyle('display'));
1251
1252 // Clicking the link slides out the list of other subscriptions.
1253 Y.Event.simulate(Y.Node.getDOMNode(link), 'click');
1254 this.wait(function() {
1255 Y.Assert.isFalse(link.hasClass('treeCollapsed'));
1256 Y.Assert.isTrue(link.hasClass('treeExpanded'));
1257 Y.Assert.isFalse(list.hasClass('lazr-closed'));
1258 Y.Assert.areNotEqual('none', list.getStyle('display'));
1259
1260 // Clicking it again, slides it back in.
1261 // It has to be nested inside 'wait' because we need
1262 // to wait for the first click to "finish".
1263 Y.Event.simulate(Y.Node.getDOMNode(link), 'click');
1264
1265 this.wait(function() {
1266 Y.Assert.isTrue(link.hasClass('treeCollapsed'));
1267 Y.Assert.isFalse(link.hasClass('treeExpanded'));
1268 Y.Assert.isTrue(list.hasClass('lazr-closed'));
1269 }, 500);
1270 }, 500);
1271 },
1272
1194}));1273}));
11951274
1196/**1275/**
@@ -1225,7 +1304,7 @@
1225 bug_id: 1,1304 bug_id: 1,
1226 count: 01305 count: 0
1227 };1306 };
1228 window.LP = {};1307 window.LP = { cache: {} };
1229 module.show_subscription_description(this.config);1308 module.show_subscription_description(this.config);
1230 this.wait(function() {1309 this.wait(function() {
1231 Y.Assert.areEqual(1310 Y.Assert.areEqual(
12321311
=== modified file 'lib/lp/bugs/templates/bug-subscription-list.pt'
--- lib/lp/bugs/templates/bug-subscription-list.pt 2011-04-13 14:24:36 +0000
+++ lib/lp/bugs/templates/bug-subscription-list.pt 2011-04-14 15:39:15 +0000
@@ -20,14 +20,31 @@
20 var ss_module = Y.lp.registry.structural_subscription;20 var ss_module = Y.lp.registry.structural_subscription;
21 var info_module = Y.lp.bugs.subscription;21 var info_module = Y.lp.bugs.subscription;
22 Y.on('domready', function() {22 Y.on('domready', function() {
23 ss_module.setup_bug_subscriptions(
24 {content_box: "#structural-subscription-content-box"});
25 info_module.show_subscription_description(23 info_module.show_subscription_description(
26 {description_box: "#subscription-description-box"});24 {description_box: "#subscription-description-box"});
27 });25 });
26 Y.on('contentready', function() {
27 // This happens only after show_subscription_description()
28 // is called in 'domready' handler, because that's what
29 // inserts '#other-subscriptions-list' into DOM.
30 ss_module.setup_bug_subscriptions(
31 {content_box: "#other-subscriptions-list"});
32 }, '#other-subscriptions-list');
28 });33 });
29 </script>34 </script>
3035 <style>
36
37 div#other-subscriptions-header {
38 margin-top: 1em;
39 }
40
41 div.subscription-description {
42 margin: 0px;
43 margin-top: 2em;
44 padding: 1em;
45 border: 1px solid #ddd;
46 }
47 </style>
31 </tal:head-epilogue>48 </tal:head-epilogue>
32</head>49</head>
33<body>50<body>
3451
=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js 2011-04-12 00:14:41 +0000
+++ lib/lp/registry/javascript/structural-subscription.js 2011-04-14 15:39:15 +0000
@@ -362,6 +362,8 @@
362 * Reset the overlay form to initial values.362 * Reset the overlay form to initial values.
363 */363 */
364function clear_overlay(content_node, no_recipient_picker) {364function clear_overlay(content_node, no_recipient_picker) {
365 content_node = content_node.one('#overlay-container');
366
365 if (no_recipient_picker) {367 if (no_recipient_picker) {
366 set_recipient_label(content_node, undefined);368 set_recipient_label(content_node, undefined);
367 } else {369 } else {
@@ -693,6 +695,12 @@
693 var content_node = Y.one(content_box_id);695 var content_node = Y.one(content_box_id);
694 if (!Y.Lang.isValue(content_node)) {696 if (!Y.Lang.isValue(content_node)) {
695 Y.error("Node not found: " + content_box_id);697 Y.error("Node not found: " + content_box_id);
698 } else {
699 var container_node = content_node.one('#overlay-container');
700 if (Y.Lang.isValue(container_node)) {
701 container_node.remove();
702 container_node.destroy();
703 }
696 }704 }
697 var container = Y.Node.create(705 var container = Y.Node.create(
698 '<div id="overlay-container"><dl>' +706 '<div id="overlay-container"><dl>' +
@@ -1032,16 +1040,14 @@
1032 * Show an overlay for editing a subscription.1040 * Show an overlay for editing a subscription.
1033 */1041 */
1034function show_edit_overlay(config, subscription, filter_info, filter_id) {1042function show_edit_overlay(config, subscription, filter_info, filter_id) {
1035 Y.one(config.content_box).empty();1043 var content_node = Y.one(config.content_box);
1036 var content_node = Y.one(config.content_box),1044 var overlay_id = setup_overlay(config.content_box, true);
1037 overlay_id = setup_overlay(config.content_box, true),1045 clear_overlay(content_node, true);
1038 submit_button = Y.Node.create(1046 var submit_button = Y.Node.create(
1039 '<button type="submit" name="field.actions.create" ' +1047 '<button type="submit" name="field.actions.create" ' +
1040 'value="Save Changes" class="lazr-pos lazr-btn" ' +1048 'value="Save Changes" class="lazr-pos lazr-btn" ' +
1041 '>OK</button>');1049 '>OK</button>');
10421050
1043 clear_overlay(content_node, true);
1044
1045 var context = {1051 var context = {
1046 filter_info: filter_info,1052 filter_info: filter_info,
1047 filter_id: filter_id1053 filter_id: filter_id
@@ -1157,7 +1163,7 @@
1157 * Attach activation (click) handlers to all of the edit links on the page.1163 * Attach activation (click) handlers to all of the edit links on the page.
1158 */1164 */
1159function wire_up_edit_links(config) {1165function wire_up_edit_links(config) {
1160 var listing = Y.one('#subscription-listing');1166 var listing = Y.one(config.content_box);
1161 var subscription_info = LP.cache.subscription_info;1167 var subscription_info = LP.cache.subscription_info;
1162 var filter_id = 0;1168 var filter_id = 0;
1163 var i;1169 var i;
@@ -1261,7 +1267,7 @@
1261function fill_in_bug_subscriptions(config) {1267function fill_in_bug_subscriptions(config) {
1262 validate_config(config);1268 validate_config(config);
12631269
1264 var listing = Y.one('#subscription-listing');1270 var listing = Y.one(config.content_box);
1265 var subscription_info = LP.cache.subscription_info;1271 var subscription_info = LP.cache.subscription_info;
1266 var top_node = Y.Node.create(1272 var top_node = Y.Node.create(
1267 '<div class="yui-g"><div id="structural-subscriptions"></div></div>');1273 '<div class="yui-g"><div id="structural-subscriptions"></div></div>');
@@ -1542,7 +1548,6 @@
1542 */1548 */
1543function show_add_overlay(config) {1549function show_add_overlay(config) {
1544 var content_node = Y.one(config.content_box);1550 var content_node = Y.one(config.content_box);
1545 content_node.empty();
1546 var overlay_id = setup_overlay(config.content_box);1551 var overlay_id = setup_overlay(config.content_box);
1547 clear_overlay(content_node, false);1552 clear_overlay(content_node, false);
15481553