Skip to content

Commit 06e8f88

Browse files
author
Mike Taylor
committed
Issue #399 - Read params from the URL and sync with model
1 parent 519e0c2 commit 06e8f88

File tree

1 file changed

+161
-39
lines changed

1 file changed

+161
-39
lines changed

webcompat/static/js/lib/issue-list.js

Lines changed: 161 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ issueList.DropdownView = Backbone.View.extend({
1818
this.closeDropdown();
1919
}
2020
}, this));
21+
22+
issueList.events.on('dropdown:update', _.bind(this.selectDropdownOption, this));
2123
},
2224
template: _.template($('#dropdown-tmpl').html()),
2325
render: function() {
@@ -32,21 +34,39 @@ issueList.DropdownView = Backbone.View.extend({
3234
this.$el.removeClass('is-active');
3335
},
3436
selectDropdownOption: function(e) {
35-
var option = $(e.target);
36-
var params = option.data('params');
37-
option.addClass('is-active')
38-
.siblings().removeClass('is-active');
37+
var option;
38+
var params;
3939

40-
this.updateDropdownTitle(option);
41-
42-
// persist value of selection to be used on subsequent page loads
43-
if ('localStorage' in window) {
44-
window.localStorage.setItem("params", params);
40+
if (typeof e === 'string') {
41+
// we received a dropdown:update event and want to update the dropdown, but
42+
// not broadcast any events (so we don't need "params")
43+
// $= because some of these will just be the beginning part (mentioned, creator)
44+
option = $('[data-params$="' + e + '"]');
45+
this.manuallyUpdateDropdownTitle(option, e);
46+
} else if (typeof e.type === 'string') {
47+
// we're dealing with a user click event.
48+
option = $(e.target);
49+
params = option.data('params');
50+
51+
// fire an event so other views can react to dropdown changes
52+
wcEvents.trigger('dropdown:change', params, {update: true});
53+
this.updateDropdownTitle(option);
54+
e.preventDefault();
4555
}
4656

47-
// fire an event so other views can react to dropdown changes
48-
wcEvents.trigger('dropdown:change', params);
49-
e.preventDefault();
57+
option.addClass('is-active')
58+
.siblings().removeClass('is-active');
59+
},
60+
manuallyUpdateDropdownTitle: function(optionElm, e) {
61+
// make sure we're only updating the title if we're operating
62+
// on the correct model.
63+
var modelOpts = this.model.get('dropdownOptions');
64+
if (_.find(modelOpts, function(opt) {
65+
return opt.params === e;
66+
}) !== undefined) {
67+
this.model.set('dropdownTitle', optionElm.text());
68+
this.render();
69+
}
5070
},
5171
updateDropdownTitle: function(optionElm) {
5272
this.model.set('dropdownTitle', optionElm.text());
@@ -59,6 +79,7 @@ issueList.FilterView = Backbone.View.extend({
5979
events: {
6080
'click .js-filter-button': 'toggleFilter'
6181
},
82+
_filterRegex: /(new|needsdiagnosis|contactready|sitewait|closed)=1/ig,
6283
_isLoggedIn: $('body').data('username'),
6384
_userName: $('body').data('username'),
6485
initialize: function() {
@@ -99,12 +120,27 @@ issueList.FilterView = Backbone.View.extend({
99120
this.dropdown.setElement(this.$el.find('.js-dropdown-wrapper')).render();
100121
return this;
101122
},
123+
addFilterToModel: function(filter) {
124+
issueList.events.trigger('filter:add-to-model', filter);
125+
},
102126
clearFilter: function() {
103127
var btns = $('[data-filter]');
104128
btns.removeClass('is-active');
129+
130+
this.removeFiltersFromModel();
131+
132+
if (history.pushState) {
133+
// remove filter from URL
134+
history.pushState({}, '', location.search.replace(this._filterRegex, ''));
135+
}
136+
},
137+
removeFiltersFromModel: function() {
138+
// Sends a message to remove filter params from the model
139+
issueList.events.trigger('filter:remove-from-model');
105140
},
106141
toggleFilter: function(e) {
107142
var btn;
143+
var filterParam;
108144
// Stringy e comes from triggered filter:activate event
109145
if (typeof e === 'string') {
110146
btn = $('[data-filter=' + e + ']');
@@ -119,9 +155,18 @@ issueList.FilterView = Backbone.View.extend({
119155
// Clear the search field
120156
issueList.events.trigger('search:clear');
121157

158+
// Remove existing filters from model and URL
159+
this.removeFiltersFromModel();
160+
if (history.pushState) {
161+
history.pushState({}, '', location.search.replace(this._filterRegex, ''));
162+
}
163+
122164
if (btn.hasClass('is-active')) {
165+
filterParam = btn.data('filter') + "=1";
123166
this.updateResults(btn.data('filter'));
167+
this.addFilterToModel(filterParam);
124168
} else {
169+
this.removeFiltersFromModel();
125170
this.updateResults();
126171
}
127172
},
@@ -195,7 +240,6 @@ issueList.SortingView = Backbone.View.extend({
195240
events: {},
196241
initialize: function() {
197242
this.paginationModel = new Backbone.Model({
198-
// TODO(miket): persist selected page limit to survive page loads
199243
dropdownTitle: 'Show 50',
200244
dropdownOptions: [
201245
{title: 'Show 25', params: 'per_page=25'},
@@ -259,36 +303,48 @@ issueList.IssueView = Backbone.View.extend({
259303
events: {
260304
'click .js-issue-label': 'labelSearch',
261305
},
306+
_filterRegex: /&*(new|needsdiagnosis|contactready|sitewait|closed)=1&*/i,
262307
_isLoggedIn: $('body').data('username'),
263308
_loadingIndicator: $('.js-loader'),
264-
_pageLimit: null,
265309
initialize: function() {
266310
this.issues = new issueList.IssueCollection();
267-
// check to see if we should pre-filter results
268-
// otherwise load default (unfiltered "all")
269-
this.loadIssues();
270311

271312
// set up event listeners.
272313
issueList.events.on('issues:update', _.bind(this.updateIssues, this));
314+
issueList.events.on('filter:add-to-model', _.bind(this.updateModelParams, this));
315+
issueList.events.on('filter:remove-from-model', _.bind(this.removeAllFiltersFromModel, this));
273316
issueList.events.on('paginate:next', _.bind(this.requestNextPage, this));
274317
issueList.events.on('paginate:previous', _.bind(this.requestPreviousPage, this));
275318
wcEvents.on('dropdown:change', _.bind(this.updateModelParams, this));
319+
320+
this.loadIssues();
276321
},
277322
template: _.template($('#issuelist-issue-tmpl').html()),
278323
loadIssues: function() {
279-
// First checks URL params, e.g., /?new=1 and activates the new filter,
280-
// or loads default unsorted/unfiltered issues
324+
// Attemps to load model state from URL params, if present,
325+
// otherwise grab model defaults and load issues
326+
327+
// popstate should load this method.
281328
var category;
282-
var filterRegex = /\?(new|needsdiagnosis|contactready|sitewait|closed)=1/;
283-
if (category = window.location.search.match(filterRegex)) {
284-
// If there was a match, load the relevant results and fire an event
285-
// to notify the button to activate.
286-
this.updateIssues(category[1]);
287-
_.delay(function() {
288-
issueList.events.trigger('filter:activate', category[1]);
289-
}, 0);
329+
// get params excluding the leading ?
330+
var urlParams = location.search.slice(1);
331+
332+
if (location.search.length !== 0) {
333+
// There are some params in the URL
334+
if (category = window.location.search.match(this._filterRegex)) {
335+
// If there was a filter match, fire an event which loads results
336+
// and notifies the button to activate.
337+
this.updateModelParams(urlParams);
338+
_.delay(function() {
339+
issueList.events.trigger('filter:activate', category[1]);
340+
}, 0);
341+
} else {
342+
this.updateModelParams(urlParams);
343+
this.fetchAndRenderIssues();
344+
}
290345
} else {
291-
// Otherwise, load default issues.
346+
// There are no params in the URL, load the defaults
347+
this.updateURLParams();
292348
this.fetchAndRenderIssues();
293349
}
294350
},
@@ -318,9 +374,6 @@ issueList.IssueView = Backbone.View.extend({
318374
wcEvents.trigger('flash:error', {message: message, timeout: timeout});
319375
});
320376
},
321-
getPageLimit: function() {
322-
return this._pageLimit;
323-
},
324377
render: function(issues) {
325378
this.$el.html(this.template({
326379
issues: issues.toJSON()
@@ -366,6 +419,16 @@ issueList.IssueView = Backbone.View.extend({
366419
issueList.events.trigger('issues:update', {query: labelFilter});
367420
e.preventDefault();
368421
},
422+
removeAllFiltersFromModel: function() {
423+
// We can't have more than one filter at once for the issues model,
424+
// and there's no meaningful notion of contactready=0, so remove them all.
425+
var filters = ['new', 'needsdiagnosis', 'contactready',
426+
'sitewait', 'closed', 'needscontact', 'q'];
427+
_.forEach(filters, function(filter) {
428+
delete this.issues.params[filter];
429+
}, this);
430+
431+
},
369432
requestNextPage: function() {
370433
var nextPage;
371434
if (nextPage = this.issues.getNextPage()) {
@@ -387,7 +450,7 @@ issueList.IssueView = Backbone.View.extend({
387450
// note: until GitHub fixes a bug where requesting issues filtered by labels
388451
// doesn't return pagination via Link, we get those results via the Search API.
389452
var searchCategories = ['new', 'contactready', 'needsdiagnosis', 'sitewait'];
390-
var params = $.extend(this.issues.params, this.getPageLimit());
453+
var params = this.issues.params;
391454

392455
// note: if query is the empty string, it will load all issues from the
393456
// '/api/issues' endpoint (which I think we want).
@@ -401,26 +464,85 @@ issueList.IssueView = Backbone.View.extend({
401464
} else {
402465
this.issues.setURLState('/api/issues', params);
403466
}
467+
this.updateURLParams();
404468
this.fetchAndRenderIssues();
405469
},
406-
updateModelParams: function(params) {
470+
updateModelParams: function(params, options) {
471+
// TODO: profile how many times this gets called and optimize.
407472
// convert params string to an array,
408473
// splitting on & in case of multiple params
409-
var paramsArray = params.split('&');
474+
// params are merged into issues model
475+
// call _.uniq() on it to ignore duplicate values
476+
var paramsArray = _.uniq(params.split('&'));
410477

411-
// paramsArray is an array of param 'key=value' string pairs,
478+
// paramsArray is an array of param 'key=value' string pairs
412479
_.forEach(paramsArray, _.bind(function(param) {
413480
var kvArray = param.split('=');
414481
var key = kvArray[0];
415482
var value = kvArray[1];
416483
this.issues.params[key] = value;
484+
}, this));
417485

418-
if (key === 'per_page') {
419-
this._pageLimit = value;
420-
}
486+
//broadcast to each of the dropdowns that they need to update
487+
var pageDropdown;
488+
if ('per_page' in this.issues.params) {
489+
pageDropdown = 'per_page=' + this.issues.params.per_page;
490+
_.delay(function(){
491+
issueList.events.trigger('dropdown:update', pageDropdown);
492+
}, 0);
493+
}
494+
495+
var sortDropdown;
496+
// all the sort options begin with sort, and end with direction.
497+
if ('sort' in this.issues.params) {
498+
sortDropdown = 'sort=' + this.issues.params.sort + '&direction=' + this.issues.params.direction;
499+
_.delay(function(){
500+
issueList.events.trigger('dropdown:update', sortDropdown);
501+
}, 0);
502+
}
503+
504+
// make sure we prevent more than one mutually-exclusive state param
505+
// in the model, because that's weird. the "last" param will win.
506+
var currentStateParamName;
507+
var stateParamsSet = ['state', 'creator', 'mentioned'];
508+
var stateParam = _.find(paramsArray, function(paramString) {
509+
return _.find(stateParamsSet, function(stateParam) {
510+
if (paramString.indexOf(stateParam) === 0) {
511+
return currentStateParamName = stateParam;
512+
}
513+
});
514+
});
515+
516+
// delete the non-current state params from the stateParamsSet
517+
var toDelete = _.without(stateParamsSet, currentStateParamName);
518+
_.forEach(toDelete, _.bind(function(param) {
519+
delete this.issues.params[param];
421520
}, this));
422521

423-
this.fetchAndRenderIssues();
522+
var stateDropdown;
523+
if (currentStateParamName in this.issues.params) {
524+
stateDropdown = stateParam;
525+
_.delay(function(){
526+
issueList.events.trigger('dropdown:update', stateDropdown);
527+
}, 0);
528+
}
529+
530+
this.updateURLParams();
531+
// only re-request issues if explicitly asked to
532+
if (options && options.update === true) {
533+
this.fetchAndRenderIssues();
534+
}
535+
},
536+
updateURLParams: function() {
537+
// push params from the model back to the URL so it can be used for bookmarks,
538+
// link sharing, etc.
539+
// an optional category can be passed in to be added.
540+
// TODO: figure out next and prev buttons.
541+
var serializedParams = $.param(this.issues.params);
542+
543+
if (history.pushState) {
544+
history.pushState({}, '', '?' + serializedParams);
545+
}
424546
}
425547
});
426548

0 commit comments

Comments
 (0)