Skip to content

Commit a97a94a

Browse files
committed
Auto merge of #10834 - whee:gh-pages-retain-filter-state, r=xFrednet
Use URL parameters for filter states This fixes #8510 by storing Clippy Lints page filter configuration in the URL parameters. This includes: - Lint levels - Lint groups - Version filters "Filter" was already present in the URL and its behavior is retained. There is existing support for passing a `sel` query parameter; this is also retained, but I am not sure if it used in the wild. The URL parameters only get included if they are modified after loading the page. I have these changes available here in case people want to play with it: https://whee.github.io/rust-clippy/master/ An example with levels, groups, and versions set (oddly): https://whee.github.io/rust-clippy/master/#/?groups=pedantic,perf&levels=allow,warn&versions=gte:53,lte:57,eq:54 Adding a filter: https://whee.github.io/rust-clippy/master/#/manual_str_repeat?groups=pedantic,perf&levels=allow,warn&versions=gte:53,lte:57,eq:54 --- changelog: Docs: [`Clippy's lint list`] now stores filter parameters in the URL, to allow easy sharing [#10834](#10834) <!-- changelog_checked -->
2 parents c85ceea + ac279ef commit a97a94a

File tree

2 files changed

+183
-30
lines changed

2 files changed

+183
-30
lines changed

util/gh-pages/index.html

+6-3
Original file line numberDiff line numberDiff line change
@@ -501,9 +501,11 @@ <h1>Clippy Lints</h1>
501501
<div class="col-12 col-md-7 search-control">
502502
<div class="input-group">
503503
<label class="input-group-addon" id="filter-label" for="search-input">Filter:</label>
504-
<input type="text" class="form-control filter-input" placeholder="Keywords or search string" id="search-input" ng-model="search" ng-model-options="{debounce: 50}"/>
504+
<input type="text" class="form-control filter-input" placeholder="Keywords or search string" id="search-input"
505+
ng-model="search" ng-blur="updatePath()" ng-keyup="$event.keyCode == 13 && updatePath()"
506+
ng-model-options="{debounce: 50}" />
505507
<span class="input-group-btn">
506-
<button class="filter-clear btn" type="button" ng-click="search = ''">
508+
<button class="filter-clear btn" type="button" ng-click="search = ''; updatePath();">
507509
Clear
508510
</button>
509511
</span>
@@ -517,7 +519,8 @@ <h1>Clippy Lints</h1>
517519
<h2 class="panel-title">
518520
<div class="panel-title-name">
519521
<span>{{lint.id}}</span>
520-
<a href="#{{lint.id}}" class="anchor label label-default" ng-click="open[lint.id] = true; $event.stopPropagation()">&para;</a>
522+
<a href="#{{lint.id}}" class="anchor label label-default"
523+
ng-click="openLint(lint); $event.preventDefault(); $event.stopPropagation()">&para;</a>
521524
<a href="" id="clipboard-{{lint.id}}" class="anchor label label-default" ng-click="copyToClipboard(lint); $event.stopPropagation()">
522525
&#128203;
523526
</a>

util/gh-pages/script.js

+177-27
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
target.scrollIntoView();
2525
}
2626

27-
function scrollToLintByURL($scope) {
28-
var removeListener = $scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
29-
scrollToLint(window.location.hash.slice(1));
27+
function scrollToLintByURL($scope, $location) {
28+
var removeListener = $scope.$on('ngRepeatFinished', function (ngRepeatFinishedEvent) {
29+
scrollToLint($location.path().substring(1));
3030
removeListener();
3131
});
3232
}
@@ -106,10 +106,10 @@
106106
}
107107
};
108108
})
109-
.controller("lintList", function ($scope, $http, $timeout) {
109+
.controller("lintList", function ($scope, $http, $location, $timeout) {
110110
// Level filter
111111
var LEVEL_FILTERS_DEFAULT = {allow: true, warn: true, deny: true, none: true};
112-
$scope.levels = LEVEL_FILTERS_DEFAULT;
112+
$scope.levels = { ...LEVEL_FILTERS_DEFAULT };
113113
$scope.byLevels = function (lint) {
114114
return $scope.levels[lint.level];
115115
};
@@ -146,6 +146,165 @@
146146
"=": {enabled: false, minorVersion: null },
147147
};
148148

149+
// Map the versionFilters to the query parameters in a way that is easier to work with in a URL
150+
const versionFilterKeyMap = {
151+
"≥": "gte",
152+
"≤": "lte",
153+
"=": "eq"
154+
};
155+
const reverseVersionFilterKeyMap = Object.fromEntries(
156+
Object.entries(versionFilterKeyMap).map(([key, value]) => [value, key])
157+
);
158+
159+
// loadFromURLParameters retrieves filter settings from the URL parameters and assigns them
160+
// to corresponding $scope variables.
161+
function loadFromURLParameters() {
162+
// Extract parameters from URL
163+
const urlParameters = $location.search();
164+
165+
// Define a helper function that assigns URL parameters to a provided scope variable
166+
const handleParameter = (parameter, scopeVariable, defaultValues) => {
167+
if (urlParameters[parameter]) {
168+
const items = urlParameters[parameter].split(',');
169+
for (const key in scopeVariable) {
170+
if (scopeVariable.hasOwnProperty(key)) {
171+
scopeVariable[key] = items.includes(key);
172+
}
173+
}
174+
} else if (defaultValues) {
175+
for (const key in defaultValues) {
176+
if (scopeVariable.hasOwnProperty(key)) {
177+
scopeVariable[key] = defaultValues[key];
178+
}
179+
}
180+
}
181+
};
182+
183+
handleParameter('levels', $scope.levels, LEVEL_FILTERS_DEFAULT);
184+
handleParameter('groups', $scope.groups, GROUPS_FILTER_DEFAULT);
185+
186+
// Handle 'versions' parameter separately because it needs additional processing
187+
if (urlParameters.versions) {
188+
const versionFilters = urlParameters.versions.split(',');
189+
for (const versionFilter of versionFilters) {
190+
const [key, minorVersion] = versionFilter.split(':');
191+
const parsedMinorVersion = parseInt(minorVersion);
192+
193+
// Map the key from the URL parameter to its original form
194+
const originalKey = reverseVersionFilterKeyMap[key];
195+
196+
if (originalKey in $scope.versionFilters && !isNaN(parsedMinorVersion)) {
197+
$scope.versionFilters[originalKey].enabled = true;
198+
$scope.versionFilters[originalKey].minorVersion = parsedMinorVersion;
199+
}
200+
}
201+
}
202+
203+
// Load the search parameter from the URL path
204+
const searchParameter = $location.path().substring(1); // Remove the leading slash
205+
if (searchParameter) {
206+
$scope.search = searchParameter;
207+
$scope.open[searchParameter] = true;
208+
scrollToLintByURL($scope, $location);
209+
}
210+
}
211+
212+
// updateURLParameter updates the URL parameter with the given key to the given value
213+
function updateURLParameter(filterObj, urlKey, defaultValue = {}, processFilter = filter => filter) {
214+
const parameter = Object.keys(filterObj)
215+
.filter(filter => filterObj[filter])
216+
.sort()
217+
.map(processFilter)
218+
.filter(Boolean) // Filters out any falsy values, including null
219+
.join(',');
220+
221+
const defaultParameter = Object.keys(defaultValue)
222+
.filter(filter => defaultValue[filter])
223+
.sort()
224+
.map(processFilter)
225+
.filter(Boolean) // Filters out any falsy values, including null
226+
.join(',');
227+
228+
// if we ended up back at the defaults, just remove it from the URL
229+
if (parameter === defaultParameter) {
230+
$location.search(urlKey, null);
231+
} else {
232+
$location.search(urlKey, parameter || null);
233+
}
234+
}
235+
236+
// updateVersionURLParameter updates the version URL parameter with the given version filters
237+
function updateVersionURLParameter(versionFilters) {
238+
updateURLParameter(
239+
versionFilters,
240+
'versions', {},
241+
versionFilter => versionFilters[versionFilter].enabled && versionFilters[versionFilter].minorVersion != null
242+
? `${versionFilterKeyMap[versionFilter]}:${versionFilters[versionFilter].minorVersion}`
243+
: null
244+
);
245+
}
246+
247+
// updateAllURLParameters updates all the URL parameters with the current filter settings
248+
function updateAllURLParameters() {
249+
updateURLParameter($scope.levels, 'levels', LEVEL_FILTERS_DEFAULT);
250+
updateURLParameter($scope.groups, 'groups', GROUPS_FILTER_DEFAULT);
251+
updateVersionURLParameter($scope.versionFilters);
252+
}
253+
254+
// Add $watches to automatically update URL parameters when the data changes
255+
$scope.$watch('levels', function (newVal, oldVal) {
256+
if (newVal !== oldVal) {
257+
updateURLParameter(newVal, 'levels', LEVEL_FILTERS_DEFAULT);
258+
}
259+
}, true);
260+
261+
$scope.$watch('groups', function (newVal, oldVal) {
262+
if (newVal !== oldVal) {
263+
updateURLParameter(newVal, 'groups', GROUPS_FILTER_DEFAULT);
264+
}
265+
}, true);
266+
267+
$scope.$watch('versionFilters', function (newVal, oldVal) {
268+
if (newVal !== oldVal) {
269+
updateVersionURLParameter(newVal);
270+
}
271+
}, true);
272+
273+
// Watch for changes in the URL path and update the search and lint display
274+
$scope.$watch(function () { return $location.path(); }, function (newPath) {
275+
const searchParameter = newPath.substring(1);
276+
if ($scope.search !== searchParameter) {
277+
$scope.search = searchParameter;
278+
$scope.open[searchParameter] = true;
279+
scrollToLintByURL($scope, $location);
280+
}
281+
});
282+
283+
let debounceTimeout;
284+
$scope.$watch('search', function (newVal, oldVal) {
285+
if (newVal !== oldVal) {
286+
if (debounceTimeout) {
287+
$timeout.cancel(debounceTimeout);
288+
}
289+
290+
debounceTimeout = $timeout(function () {
291+
$location.path(newVal);
292+
}, 1000);
293+
}
294+
});
295+
296+
$scope.$watch(function () { return $location.search(); }, function (newParameters) {
297+
loadFromURLParameters();
298+
}, true);
299+
300+
$scope.updatePath = function () {
301+
if (debounceTimeout) {
302+
$timeout.cancel(debounceTimeout);
303+
}
304+
305+
$location.path($scope.search);
306+
}
307+
149308
$scope.selectTheme = function (theme) {
150309
setTheme(theme, true);
151310
}
@@ -169,10 +328,9 @@
169328
};
170329

171330
$scope.resetGroupsToDefault = function () {
172-
const groups = $scope.groups;
173-
for (const [key, value] of Object.entries(GROUPS_FILTER_DEFAULT)) {
174-
groups[key] = value;
175-
}
331+
$scope.groups = {
332+
...GROUPS_FILTER_DEFAULT
333+
};
176334
};
177335

178336
$scope.selectedValuesCount = function (obj) {
@@ -272,6 +430,12 @@
272430
return true;
273431
}
274432

433+
// Show details for one lint
434+
$scope.openLint = function (lint) {
435+
$scope.open[lint.id] = true;
436+
$location.path(lint.id);
437+
};
438+
275439
$scope.copyToClipboard = function (lint) {
276440
const clipboard = document.getElementById("clipboard-" + lint.id);
277441
if (clipboard) {
@@ -296,14 +460,12 @@
296460
// Get data
297461
$scope.open = {};
298462
$scope.loading = true;
463+
299464
// This will be used to jump into the source code of the version that this documentation is for.
300465
$scope.docVersion = window.location.pathname.split('/')[2] || "master";
301466

302-
if (window.location.hash.length > 1) {
303-
$scope.search = window.location.hash.slice(1);
304-
$scope.open[window.location.hash.slice(1)] = true;
305-
scrollToLintByURL($scope);
306-
}
467+
// Set up the filters from the URL parameters before we start loading the data
468+
loadFromURLParameters();
307469

308470
$http.get('./lints.json')
309471
.success(function (data) {
@@ -315,7 +477,7 @@
315477
selectGroup($scope, selectedGroup.toLowerCase());
316478
}
317479

318-
scrollToLintByURL($scope);
480+
scrollToLintByURL($scope, $location);
319481

320482
setTimeout(function () {
321483
var el = document.getElementById('filter-input');
@@ -326,18 +488,6 @@
326488
$scope.error = data;
327489
$scope.loading = false;
328490
});
329-
330-
window.addEventListener('hashchange', function () {
331-
// trigger re-render
332-
$timeout(function () {
333-
$scope.levels = LEVEL_FILTERS_DEFAULT;
334-
$scope.search = window.location.hash.slice(1);
335-
$scope.open[window.location.hash.slice(1)] = true;
336-
337-
scrollToLintByURL($scope);
338-
});
339-
return true;
340-
}, false);
341491
});
342492
})();
343493

0 commit comments

Comments
 (0)