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
1=== modified file 'lib/lp/bugs/javascript/subscription.js'
2--- lib/lp/bugs/javascript/subscription.js 2011-04-13 15:47:48 +0000
3+++ lib/lp/bugs/javascript/subscription.js 2011-04-14 15:39:15 +0000
4@@ -100,8 +100,7 @@
5
6 /* These are the owner variations. */
7 var _OWNER = (
8- "the owner of the {pillar_type} " +
9- "{pillar}, which has no bug supervisor.");
10+ "the owner of {pillar}, which has no bug supervisor.");
11 /* These are the actual strings to use. */
12 Y.mix(reasons, {
13 YOU_OWNER: _BECAUSE_YOU_ARE + _OWNER,
14@@ -584,22 +583,73 @@
15 */
16 function get_other_descriptions_node(info, extra_data) {
17 var subs = gather_nondirect_subscriptions(info);
18- if (subs.length > 0) {
19- var other_node = Y.Node.create('<div></div>')
20+ if (subs.length > 0 || has_structural_subscriptions()) {
21+ var node = Y.Node.create('<div></div>')
22 .set('id', 'other-subscriptions');
23+ var header = Y.Node.create('<div></div>')
24+ .set('id', 'other-subscriptions-header');
25+ var header_link = Y.Node.create('<a></a>')
26+ .set('href', '#')
27+ .set('text', 'Other subscriptions');
28+ header.appendChild(header_link);
29+ node.appendChild(header);
30+ var list = Y.Node.create('<div></div>')
31+ .set('id', 'other-subscriptions-list');
32+ node.appendChild(list);
33+
34+ setup_slider(list, header_link);
35
36 for (var index in subs) {
37- other_node.appendChild(
38+ list.appendChild(
39 get_single_description_node(subs[index], extra_data));
40 }
41
42- return other_node;
43+ return node;
44 } else {
45 return undefined;
46 }
47 }
48 namespace._get_other_descriptions_node = get_other_descriptions_node;
49
50+/**
51+ * Are there any structural subscriptions that need to be rendered.
52+ */
53+function has_structural_subscriptions() {
54+ return (LP.cache.subscription_info &&
55+ LP.cache.subscription_info.length > 0);
56+}
57+
58+/**
59+ * Sets up a slider that slides the `body` in and out when `header`
60+ * is clicked.
61+ */
62+function setup_slider(body, header) {
63+ // Hide the widget body contents.
64+ body.addClass('lazr-closed');
65+ body.setStyle('display', 'none');
66+
67+ // Ensure that the widget header uses the correct sprite icon
68+ // and gets the styling for javascript actions applied.
69+ header.addClass('sprite');
70+ header.addClass('treeCollapsed');
71+ header.addClass('js-action');
72+
73+ var slide;
74+ function toggle_body_visibility(e) {
75+ e.halt();
76+ if (!slide) {
77+ slide = Y.lazr.effects.slide_out(body);
78+ header.replaceClass('treeCollapsed', 'treeExpanded');
79+ } else {
80+ slide.set('reverse', !slide.get('reverse'));
81+ header.toggleClass('treeExpanded');
82+ header.toggleClass('treeCollapsed');
83+ }
84+ slide.stop();
85+ slide.run();
86+ }
87+ header.on('click', toggle_body_visibility);
88+}
89
90 /**
91 * Add descriptions for all non-structural subscriptions to the page.
92@@ -632,5 +682,5 @@
93 namespace.show_subscription_description = show_subscription_description
94
95 }, '0.1', {requires: [
96- 'dom', 'node', 'substitute'
97+ 'dom', 'event', 'node', 'substitute', 'lazr.effects',
98 ]});
99
100=== modified file 'lib/lp/bugs/javascript/tests/test_subscription.js'
101--- lib/lp/bugs/javascript/tests/test_subscription.js 2011-04-13 14:19:19 +0000
102+++ lib/lp/bugs/javascript/tests/test_subscription.js 2011-04-14 15:39:15 +0000
103@@ -1149,6 +1149,7 @@
104 as_owner: _constructCategory(),
105 count: 1
106 };
107+ window.LP = { cache: {} };
108 Y.Assert.areSame(
109 undefined,
110 module._get_other_descriptions_node(info));
111@@ -1191,6 +1192,84 @@
112 2, node.all('.subscription-description').size());
113 },
114
115+ test_no_direct_has_structural_subscriptions: function() {
116+ // With no non-personal subscriptions, and a structural
117+ // subscription, the node is still constructed because
118+ // structural subscriptions go there as well.
119+ var info = {
120+ direct: _constructCategory([{ bug: {} }]),
121+ from_duplicate: _constructCategory(),
122+ as_assignee: _constructCategory(),
123+ as_owner: _constructCategory(),
124+ count: 1
125+ };
126+ window.LP = { cache: { subscription_info: ['1'] } };
127+ Y.Assert.isNotUndefined(
128+ module._get_other_descriptions_node(info));
129+ },
130+
131+ test_header: function() {
132+ // There is a subscription on the duplicate bug.
133+ var info = {
134+ direct: _constructCategory(),
135+ from_duplicate: _constructCategory([{ bug: {id: 1} }]),
136+ as_assignee: _constructCategory(),
137+ as_owner: _constructCategory(),
138+ count: 1
139+ };
140+
141+ // A returned node contains the 'other-subscriptions-header'
142+ // div with the link.
143+ var node = module._get_other_descriptions_node(info);
144+ var header = node.one('#other-subscriptions-header');
145+ Y.Assert.isNotUndefined(header);
146+ var link = header.one('a');
147+ Y.Assert.areEqual('Other subscriptions', link.get('text'));
148+ },
149+
150+ test_header_slideout: function() {
151+ // Clicking on the header slides-out the box, and
152+ // clicking it again slides it back in.
153+ var info = {
154+ direct: _constructCategory(),
155+ from_duplicate: _constructCategory([{ bug: {id: 1} }]),
156+ as_assignee: _constructCategory(),
157+ as_owner: _constructCategory(),
158+ count: 1
159+ };
160+
161+ // A returned node contains the 'other-subscriptions-header'
162+ // div with the link.
163+ var node = module._get_other_descriptions_node(info);
164+ var link = node.one('#other-subscriptions-header a');
165+ var list = node.one('#other-subscriptions-list');
166+
167+ // Initially, the list is hidden.
168+ Y.Assert.isTrue(link.hasClass('treeCollapsed'));
169+ Y.Assert.isTrue(list.hasClass('lazr-closed'));
170+ Y.Assert.areEqual('none', list.getStyle('display'));
171+
172+ // Clicking the link slides out the list of other subscriptions.
173+ Y.Event.simulate(Y.Node.getDOMNode(link), 'click');
174+ this.wait(function() {
175+ Y.Assert.isFalse(link.hasClass('treeCollapsed'));
176+ Y.Assert.isTrue(link.hasClass('treeExpanded'));
177+ Y.Assert.isFalse(list.hasClass('lazr-closed'));
178+ Y.Assert.areNotEqual('none', list.getStyle('display'));
179+
180+ // Clicking it again, slides it back in.
181+ // It has to be nested inside 'wait' because we need
182+ // to wait for the first click to "finish".
183+ Y.Event.simulate(Y.Node.getDOMNode(link), 'click');
184+
185+ this.wait(function() {
186+ Y.Assert.isTrue(link.hasClass('treeCollapsed'));
187+ Y.Assert.isFalse(link.hasClass('treeExpanded'));
188+ Y.Assert.isTrue(list.hasClass('lazr-closed'));
189+ }, 500);
190+ }, 500);
191+ },
192+
193 }));
194
195 /**
196@@ -1225,7 +1304,7 @@
197 bug_id: 1,
198 count: 0
199 };
200- window.LP = {};
201+ window.LP = { cache: {} };
202 module.show_subscription_description(this.config);
203 this.wait(function() {
204 Y.Assert.areEqual(
205
206=== modified file 'lib/lp/bugs/templates/bug-subscription-list.pt'
207--- lib/lp/bugs/templates/bug-subscription-list.pt 2011-04-13 14:24:36 +0000
208+++ lib/lp/bugs/templates/bug-subscription-list.pt 2011-04-14 15:39:15 +0000
209@@ -20,14 +20,31 @@
210 var ss_module = Y.lp.registry.structural_subscription;
211 var info_module = Y.lp.bugs.subscription;
212 Y.on('domready', function() {
213- ss_module.setup_bug_subscriptions(
214- {content_box: "#structural-subscription-content-box"});
215 info_module.show_subscription_description(
216 {description_box: "#subscription-description-box"});
217 });
218+ Y.on('contentready', function() {
219+ // This happens only after show_subscription_description()
220+ // is called in 'domready' handler, because that's what
221+ // inserts '#other-subscriptions-list' into DOM.
222+ ss_module.setup_bug_subscriptions(
223+ {content_box: "#other-subscriptions-list"});
224+ }, '#other-subscriptions-list');
225 });
226 </script>
227-
228+ <style>
229+
230+ div#other-subscriptions-header {
231+ margin-top: 1em;
232+ }
233+
234+ div.subscription-description {
235+ margin: 0px;
236+ margin-top: 2em;
237+ padding: 1em;
238+ border: 1px solid #ddd;
239+ }
240+ </style>
241 </tal:head-epilogue>
242 </head>
243 <body>
244
245=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
246--- lib/lp/registry/javascript/structural-subscription.js 2011-04-12 00:14:41 +0000
247+++ lib/lp/registry/javascript/structural-subscription.js 2011-04-14 15:39:15 +0000
248@@ -362,6 +362,8 @@
249 * Reset the overlay form to initial values.
250 */
251 function clear_overlay(content_node, no_recipient_picker) {
252+ content_node = content_node.one('#overlay-container');
253+
254 if (no_recipient_picker) {
255 set_recipient_label(content_node, undefined);
256 } else {
257@@ -693,6 +695,12 @@
258 var content_node = Y.one(content_box_id);
259 if (!Y.Lang.isValue(content_node)) {
260 Y.error("Node not found: " + content_box_id);
261+ } else {
262+ var container_node = content_node.one('#overlay-container');
263+ if (Y.Lang.isValue(container_node)) {
264+ container_node.remove();
265+ container_node.destroy();
266+ }
267 }
268 var container = Y.Node.create(
269 '<div id="overlay-container"><dl>' +
270@@ -1032,16 +1040,14 @@
271 * Show an overlay for editing a subscription.
272 */
273 function show_edit_overlay(config, subscription, filter_info, filter_id) {
274- Y.one(config.content_box).empty();
275- var content_node = Y.one(config.content_box),
276- overlay_id = setup_overlay(config.content_box, true),
277- submit_button = Y.Node.create(
278+ var content_node = Y.one(config.content_box);
279+ var overlay_id = setup_overlay(config.content_box, true);
280+ clear_overlay(content_node, true);
281+ var submit_button = Y.Node.create(
282 '<button type="submit" name="field.actions.create" ' +
283 'value="Save Changes" class="lazr-pos lazr-btn" ' +
284 '>OK</button>');
285
286- clear_overlay(content_node, true);
287-
288 var context = {
289 filter_info: filter_info,
290 filter_id: filter_id
291@@ -1157,7 +1163,7 @@
292 * Attach activation (click) handlers to all of the edit links on the page.
293 */
294 function wire_up_edit_links(config) {
295- var listing = Y.one('#subscription-listing');
296+ var listing = Y.one(config.content_box);
297 var subscription_info = LP.cache.subscription_info;
298 var filter_id = 0;
299 var i;
300@@ -1261,7 +1267,7 @@
301 function fill_in_bug_subscriptions(config) {
302 validate_config(config);
303
304- var listing = Y.one('#subscription-listing');
305+ var listing = Y.one(config.content_box);
306 var subscription_info = LP.cache.subscription_info;
307 var top_node = Y.Node.create(
308 '<div class="yui-g"><div id="structural-subscriptions"></div></div>');
309@@ -1542,7 +1548,6 @@
310 */
311 function show_add_overlay(config) {
312 var content_node = Y.one(config.content_box);
313- content_node.empty();
314 var overlay_id = setup_overlay(config.content_box);
315 clear_overlay(content_node, false);
316