Merge lp:~abentley/launchpad/display-cleanup into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Approved by: Deryck Hodge
Approved revision: no longer in the source branch.
Merged at revision: 14216
Proposed branch: lp:~abentley/launchpad/display-cleanup
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/navigate-batches
Diff against target: 353 lines (+186/-26)
4 files modified
lib/lp/bugs/browser/bugtask.py (+1/-0)
lib/lp/bugs/browser/tests/test_bugtask.py (+1/-0)
lib/lp/bugs/javascript/buglisting.js (+55/-7)
lib/lp/bugs/javascript/tests/test_buglisting.js (+129/-19)
To merge this branch: bzr merge lp:~abentley/launchpad/display-cleanup
Reviewer Review Type Date Requested Status
Abel Deuring (community) Approve
Deryck Hodge Pending
Review via email: mp+80621@code.launchpad.net

Commit message

[r=adeuring][bug=882795] Polish ajax bug listing navigation.

Description of the change

= Summary =
Fix bug #882795: ajax bug listing navigation needs polish

== Proposed fix ==
Replace all navigation nodes with hyperlink nodes.
Mark first/previous disabled on first batch.
Mark last/next disabled on last batch.
Update batch info (1 -> 5 of 10) on navigation.

== Pre-implementation notes ==
None

== Implementation details ==
None

== Tests ==
bin/test -t test_buglisting.html

== Demo and Q/A ==
- Go to a listing page. first & previous should be disabled.
- Click last. next & last should be disabled, and first & previous should be green. The info about the batch should be updated.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/javascript/buglisting.js
  lib/lp/bugs/templates/bugs-listing-table.pt
  lib/lp/bugs/templates/buglisting-default.pt
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt
  lib/lp/bugs/javascript/tests/test_buglisting.js

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

(16:40:10) adeuring: abentley: I think "this.current_batch.mustache_model.bugtasks.length + 1" in line 113 is wrong. Shouldn't that be "... -1"?
(16:40:35) adeuring: (line 113 of the diff)
(16:41:09) adeuring: abentley: no, that should be "+0"...
(16:41:13) abentley: adeuring: So, the +1 is trying to compensate for start being 0-indexed.
(16:41:47) adeuring: abentley: right. But for 5 results and start = 0, we want to show "results 1-> 5"
(16:41:56) adeuring: while we get "1 to 6"
(16:42:08) adeuring: abentley: just click on any of the links ;)
(16:42:15) abentley: Hmm, so why does it look right in use?
(16:42:36) adeuring: and compare the number in "n->m of k" with the real number of results ;)
(16:42:49) abentley: adeuring: that's an estimate, though.
(16:43:26) adeuring: abentley: well, k is an estimate (at least for StormRangeFactory), but we known the number of results in the batch precisely
(16:44:07) abentley: adeuring: You're right, I'll fix it.
(16:44:15) adeuring: abentley: cool, thanks
(16:46:07) adeuring: abentley: another quirk: For the first batch, the "first" and "previous" links are gray, but still active. "unlinking" them is something for antother branch, I assume?
(16:46:30) adeuring: (same for the last batch and the last/next links)
(16:47:39) abentley: adeuring: I would prefer if the CSS was fixed so that inactive links don't show as links.
(16:48:33) adeuring: abentley: right, makes sense. So, r=me, and file a bug about the links?
(16:49:02) abentley: adeuring: Thanks. I'll file a bug.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtask.py'
2--- lib/lp/bugs/browser/bugtask.py 2011-10-28 15:09:16 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2011-10-28 15:09:17 +0000
4@@ -2495,6 +2495,7 @@
5 cache.objects['next'] = _getBatchInfo(next_batch)
6 prev_batch = batch_navigator.batch.prevBatch()
7 cache.objects['prev'] = _getBatchInfo(prev_batch)
8+ cache.objects['total'] = batch_navigator.batch.total()
9 cache.objects['order_by'] = ','.join(
10 get_sortorder_from_request(self.request))
11 cache.objects['forwards'] = batch_navigator.batch.range_forwards
12
13=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
14--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-28 15:09:16 +0000
15+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-28 15:09:17 +0000
16@@ -1424,6 +1424,7 @@
17 self.assertIs(None, cache.objects['memo'])
18 self.assertEqual(0, cache.objects['start'])
19 self.assertTrue(cache.objects['forwards'])
20+ self.assertEqual(1, cache.objects['total'])
21
22 def test_cache_has_all_batch_vars_specified(self):
23 """Cache has all the needed variables.
24
25=== modified file 'lib/lp/bugs/javascript/buglisting.js'
26--- lib/lp/bugs/javascript/buglisting.js 2011-10-28 15:09:16 +0000
27+++ lib/lp/bugs/javascript/buglisting.js 2011-10-28 15:09:17 +0000
28@@ -18,11 +18,13 @@
29 * cache is the JSONRequestCache for the batch.
30 * template is the template to use for rendering batches.
31 * target is a YUI node to update when rendering batches.
32+ * navigation_indices is a YUI NodeList of nodes to update with the current
33+ * batch info.
34 * io_provider is something providing the Y.io interface, typically used for
35 * testing. Defaults to Y.io.
36 */
37 namespace.ListingNavigator = function(current_url, cache, template, target,
38- io_provider) {
39+ navigation_indices, io_provider) {
40 var lp_client = new Y.lp.client.Launchpad();
41 this.search_params = namespace.get_query(current_url);
42 delete this.search_params.start;
43@@ -34,6 +36,14 @@
44 this.template = template;
45 this.target = target;
46 this.batches = {};
47+ this.backwards_navigation = new Y.NodeList([]);
48+ this.forwards_navigation = new Y.NodeList([]);
49+ if (!Y.Lang.isValue(navigation_indices)){
50+ navigation_indices = new Y.NodeList([]);
51+ }
52+ this.navigation_indices = navigation_indices;
53+ this.batch_info_template = '<strong>{{start}}</strong> &rarr; ' +
54+ '<strong>{{end}}</strong> of {{total}} results';
55 };
56
57
58@@ -53,18 +63,38 @@
59 nodes.addClass('js-action');
60 };
61
62+/**
63+ * Rewrite all nodes with navigation classes so that they are hyperlinks.
64+ * Content is retained.
65+ */
66+namespace.linkify_navigation = function(){
67+ Y.each(['previous', 'next', 'first', 'last'], function(class_name){
68+ Y.all('.' + class_name).each(function(node){
69+ new_node = Y.Node.create('<a href="#"></a>');
70+ new_node.addClass(class_name);
71+ new_node.setContent(node.getContent());
72+ node.replace(new_node);
73+ });
74+ });
75+};
76
77 /**
78 * Factory to return a ListingNavigator for the given page.
79 */
80 namespace.ListingNavigator.from_page = function(){
81 var target = Y.one('#client-listing');
82+ var navigation_indices = Y.all('.batch-navigation-index');
83 var navigator = new namespace.ListingNavigator(
84- window.location, LP.cache, LP.mustache_listings, target);
85+ window.location, LP.cache, LP.mustache_listings, target,
86+ navigation_indices);
87+ namespace.linkify_navigation();
88+ navigator.backwards_navigation = Y.all('.first,.previous');
89+ navigator.forwards_navigation = Y.all('.last,.next');
90 navigator.clickAction('.first', navigator.first_batch);
91 navigator.clickAction('.next', navigator.next_batch);
92 navigator.clickAction('.previous', navigator.prev_batch);
93 navigator.clickAction('.last', navigator.last_batch);
94+ navigator.render_navigation();
95 return navigator;
96 };
97
98@@ -77,13 +107,31 @@
99 *
100 * The template is always LP.mustache_listings.
101 */
102-namespace.ListingNavigator.prototype.rendertable = function(){
103+namespace.ListingNavigator.prototype.render = function(){
104 if (! Y.Lang.isValue(this.target)){
105 return;
106 }
107 var model = this.current_batch.mustache_model;
108- var txt = Mustache.to_html(this.template, model);
109- this.target.set('innerHTML', txt);
110+ var batch_info = Mustache.to_html(this.batch_info_template, {
111+ start: this.current_batch.start + 1,
112+ end: this.current_batch.start +
113+ this.current_batch.mustache_model.bugtasks.length,
114+ total: this.current_batch.total
115+ });
116+ this.target.setContent(Mustache.to_html(this.template, model));
117+ this.navigation_indices.setContent(batch_info);
118+ this.render_navigation();
119+};
120+
121+
122+/**
123+ * Enable/disable navigation links as appropriate.
124+ */
125+namespace.ListingNavigator.prototype.render_navigation = function(){
126+ this.backwards_navigation.toggleClass(
127+ 'inactive', this.current_batch.prev === null);
128+ this.forwards_navigation.toggleClass(
129+ 'inactive', this.current_batch.next === null);
130 };
131
132
133@@ -105,7 +153,7 @@
134 var key = namespace.ListingNavigator.get_batch_key(model);
135 this.batches[key] = model;
136 this.current_batch = model;
137- this.rendertable();
138+ this.render();
139 };
140
141
142@@ -139,7 +187,7 @@
143 var cached_batch = this.batches[key];
144 if (Y.Lang.isValue(cached_batch)){
145 this.current_batch = cached_batch;
146- this.rendertable();
147+ this.render();
148 }
149 else {
150 this.load_model(config);
151
152=== modified file 'lib/lp/bugs/javascript/tests/test_buglisting.js'
153--- lib/lp/bugs/javascript/tests/test_buglisting.js 2011-10-28 15:09:16 +0000
154+++ lib/lp/bugs/javascript/tests/test_buglisting.js 2011-10-28 15:09:17 +0000
155@@ -21,26 +21,132 @@
156 }));
157
158 suite.add(new Y.Test.Case({
159- name: 'rendertable',
160-
161- test_rendertable_no_client_listing: function() {
162+ name: 'render',
163+
164+ tearDown: function(){
165+ Y.one('#fixture').setContent('');
166+ },
167+
168+ test_render_no_client_listing: function() {
169 // Rendering should not error with no #client-listing.
170 var navigator = new module.ListingNavigator();
171- navigator.rendertable();
172+ navigator.render();
173 },
174- test_rendertable: function() {
175- // Rendering should work with #client-listing supplied.
176+ get_render_navigator: function() {
177 var target = Y.Node.create('<div id="client-listing"></div>');
178 var lp_cache = {
179 mustache_model: {
180- foo: 'bar'
181- }
182+ foo: 'bar',
183+ bugtasks: ['a', 'b', 'c']
184+ },
185+ next: null,
186+ prev: null,
187+ start: 5,
188+ total: 256
189 };
190 var template = "{{foo}}";
191- var navigator = new module.ListingNavigator(
192+ var navigator = new module.ListingNavigator(
193 null, lp_cache, template, target);
194- navigator.rendertable();
195- Y.Assert.areEqual('bar', navigator.target.get('innerHTML'));
196+ var index = Y.Node.create(
197+ '<div><strong>3</strong> &rarr; <strong>4</strong>' +
198+ ' of 512 results</div>');
199+ navigator.navigation_indices.push(index);
200+ navigator.backwards_navigation.push(Y.Node.create('<div></div>'));
201+ navigator.forwards_navigation.push(Y.Node.create('<div></div>'));
202+ return navigator;
203+ },
204+ test_render: function() {
205+ // Rendering should work with #client-listing supplied.
206+ var navigator = this.get_render_navigator();
207+ navigator.render();
208+ Y.Assert.areEqual('bar', navigator.target.getContent());
209+ },
210+ /**
211+ * render_navigation should disable "previous" and "first" if there is
212+ * no previous batch (i.e. we're at the beginning.)
213+ */
214+ test_render_navigation_disables_backwards_navigation_if_no_prev:
215+ function() {
216+ var navigator = this.get_render_navigator();
217+ var action = navigator.backwards_navigation.item(0);
218+ navigator.render_navigation();
219+ Y.Assert.isTrue(action.hasClass('inactive'));
220+ },
221+ /**
222+ * render_navigation should enable "previous" and "first" if there is
223+ * a previous batch (i.e. we're not at the beginning.)
224+ */
225+ test_render_navigation_enables_backwards_navigation_if_prev:
226+ function() {
227+ var navigator = this.get_render_navigator();
228+ var action = navigator.backwards_navigation.item(0);
229+ action.addClass('inactive');
230+ navigator.current_batch.prev = {
231+ start: 1, memo: 'pi'
232+ };
233+ navigator.render_navigation();
234+ Y.Assert.isFalse(action.hasClass('inactive'));
235+ },
236+ /**
237+ * render_navigation should disable "next" and "last" if there is
238+ * no next batch (i.e. we're at the end.)
239+ */
240+ test_render_navigation_disables_forwards_navigation_if_no_next:
241+ function() {
242+ var navigator = this.get_render_navigator();
243+ var action = navigator.forwards_navigation.item(0);
244+ navigator.render_navigation();
245+ Y.Assert.isTrue(action.hasClass('inactive'));
246+ },
247+ /**
248+ * render_navigation should enable "next" and "last" if there is a next
249+ * batch (i.e. we're not at the end.)
250+ */
251+ test_render_navigation_enables_forwards_navigation_if_next: function() {
252+ var navigator = this.get_render_navigator();
253+ var action = navigator.forwards_navigation.item(0);
254+ action.addClass('inactive');
255+ navigator.current_batch.next = {
256+ start: 1, memo: 'pi'
257+ };
258+ navigator.render_navigation();
259+ Y.Assert.isFalse(action.hasClass('inactive'));
260+ },
261+ /**
262+ * linkify_navigation should convert previous, next, first last into
263+ * hyperlinks, while retaining the original content.
264+ */
265+ test_linkify_navigation: function(){
266+ Y.one('#fixture').setContent(
267+ '<span class="previous">PreVious</span>' +
268+ '<span class="next">NeXt</span>' +
269+ '<span class="first">FiRST</span>' +
270+ '<span class="last">lAst</span>');
271+ module.linkify_navigation();
272+ function checkNav(selector, content){
273+ var node = Y.one(selector);
274+ Y.Assert.areEqual('a', node.get('tagName').toLowerCase());
275+ Y.Assert.areEqual(content, node.getContent());
276+ Y.Assert.areEqual('#', node.get('href').substr(-1, 1));
277+ }
278+ checkNav('.previous', 'PreVious');
279+ checkNav('.next', 'NeXt');
280+ checkNav('.first', 'FiRST');
281+ checkNav('.last', 'lAst');
282+ },
283+ /**
284+ * Render should update the navigation_indices with the result info.
285+ */
286+ test_render_navigation_indices: function(){
287+ var navigator = this.get_render_navigator();
288+ index = navigator.navigation_indices.item(0);
289+ Y.Assert.areEqual(
290+ '<strong>3</strong> \u2192 <strong>4</strong> of 512 results',
291+ index.getContent());
292+ navigator.render();
293+ Y.Assert.areEqual(
294+ '<strong>6</strong> \u2192 <strong>8</strong> of 256 results',
295+ index.getContent());
296 }
297 }));
298
299@@ -58,26 +164,29 @@
300 web_link: 'http://foo/bar'
301 },
302 mustache_model: {
303- foo: 'bar'
304+ foo: 'bar',
305+ bugtasks: []
306 }
307 };
308 var target = Y.Node.create('<div id="client-listing"></div>');
309 var navigator = new module.ListingNavigator(
310 "http://yahoo.com?start=5&memo=6&direction=backwards",
311 lp_cache, "<ol>" + "{{#item}}<li>{{name}}</li>{{/item}}</ol>",
312- target, mock_io);
313+ target, null, mock_io);
314 navigator.first_batch('intensity');
315- Y.Assert.areEqual('', navigator.target.get('innerHTML'));
316+ Y.Assert.areEqual('', navigator.target.getContent());
317 mock_io.last_request.successJSON({
318 context: {
319 resource_type_link: 'http://foo_type',
320 web_link: 'http://foo/bar'
321 },
322 mustache_model:
323- {item: [
324+ {
325+ item: [
326 {name: 'first'},
327- {name: 'second'}
328- ]},
329+ {name: 'second'}],
330+ bugtasks: []
331+ },
332 order_by: 'intensity',
333 start: 0,
334 forwards: true,
335@@ -91,7 +200,7 @@
336 var navigator = this.get_intensity_listing();
337 var mock_io = navigator.io_provider;
338 Y.Assert.areEqual('<ol><li>first</li><li>second</li></ol>',
339- navigator.target.get('innerHTML'));
340+ navigator.target.getContent());
341 Y.Assert.areEqual('/bar/+bugs/++model++?orderby=intensity&start=0',
342 mock_io.last_request.url);
343 },
344@@ -247,7 +356,8 @@
345 order_by: 'foo',
346 last_start: 23
347 };
348- return new module.ListingNavigator(url, lp_cache, null, null, mock_io);
349+ return new module.ListingNavigator(url, lp_cache, null, null, null,
350+ mock_io);
351 };
352
353 suite.add(new Y.Test.Case({