Fixed #32204 -- Added quick filter to admin's navigation sidebar.
This commit is contained in:
parent
7248afe12f
commit
d915dd1c58
|
@ -118,3 +118,25 @@
|
|||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-filter {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 5px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--darkened-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#nav-filter:focus {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#nav-filter.no-results {
|
||||
background: var(--message-error-bg);
|
||||
}
|
||||
|
||||
#nav-sidebar table {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -36,4 +36,58 @@
|
|||
main.classList.toggle('shifted');
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarQuickFilter() {
|
||||
const options = [];
|
||||
const navSidebar = document.getElementById('nav-sidebar');
|
||||
if (!navSidebar) {
|
||||
return;
|
||||
}
|
||||
navSidebar.querySelectorAll('th[scope=row] a').forEach((container) => {
|
||||
options.push({title: container.innerHTML, node: container});
|
||||
});
|
||||
|
||||
function checkValue(event) {
|
||||
let filterValue = event.target.value;
|
||||
if (filterValue) {
|
||||
filterValue = filterValue.toLowerCase();
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
filterValue = '';
|
||||
event.target.value = ''; // clear input
|
||||
}
|
||||
let matches = false;
|
||||
for (const o of options) {
|
||||
let displayValue = '';
|
||||
if (filterValue) {
|
||||
if (o.title.toLowerCase().indexOf(filterValue) === -1) {
|
||||
displayValue = 'none';
|
||||
} else {
|
||||
matches = true;
|
||||
}
|
||||
}
|
||||
// show/hide parent <TR>
|
||||
o.node.parentNode.parentNode.style.display = displayValue;
|
||||
}
|
||||
if (!filterValue || matches) {
|
||||
event.target.classList.remove('no-results');
|
||||
} else {
|
||||
event.target.classList.add('no-results');
|
||||
}
|
||||
localStorage.setItem('django.admin.navSidebarFilterValue', filterValue);
|
||||
}
|
||||
|
||||
const nav = document.getElementById('nav-filter');
|
||||
nav.addEventListener('change', checkValue, false);
|
||||
nav.addEventListener('input', checkValue, false);
|
||||
nav.addEventListener('keyup', checkValue, false);
|
||||
|
||||
const storedValue = localStorage.getItem('django.admin.navSidebarFilterValue');
|
||||
if (storedValue) {
|
||||
nav.value = storedValue;
|
||||
checkValue({target: nav, key: ''});
|
||||
}
|
||||
}
|
||||
window.initSidebarQuickFilter = initSidebarQuickFilter;
|
||||
initSidebarQuickFilter();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{% load i18n %}
|
||||
<button class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar" aria-label="{% translate 'Toggle navigation' %}"></button>
|
||||
<nav class="sticky" id="nav-sidebar">
|
||||
<input type="search" id="nav-filter"
|
||||
placeholder="{% translate 'Start typing to filter...' %}"
|
||||
aria-label="{% translate 'Filter navigation items' %}">
|
||||
{% include 'admin/app_list.html' with app_list=available_apps show_changelinks=False %}
|
||||
</nav>
|
||||
|
|
|
@ -70,6 +70,8 @@ Minor features
|
|||
* The new :meth:`.ModelAdmin.get_formset_kwargs` method allows customizing the
|
||||
keyword arguments passed to the constructor of a formset.
|
||||
|
||||
* The navigation sidebar now has a quick filter toolbar.
|
||||
|
||||
:mod:`django.contrib.admindocs`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/* global QUnit */
|
||||
'use strict';
|
||||
|
||||
QUnit.module('admin.sidebar: filter', {
|
||||
beforeEach: function() {
|
||||
const $ = django.jQuery;
|
||||
$('#qunit-fixture').append($('#nav-sidebar-filter').text());
|
||||
this.navSidebar = $('#nav-sidebar');
|
||||
this.navFilter = $('#nav-filter');
|
||||
initSidebarQuickFilter();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('filter by a model name', function(assert) {
|
||||
assert.equal(this.navSidebar.find('th[scope=row] a').length, 2);
|
||||
|
||||
this.navFilter.val('us'); // Matches 'users'.
|
||||
this.navFilter[0].dispatchEvent(new Event('change'));
|
||||
assert.equal(this.navSidebar.find('tr[class^="model-"]:visible').length, 1);
|
||||
|
||||
this.navFilter.val('nonexistent');
|
||||
this.navFilter[0].dispatchEvent(new Event('change'));
|
||||
assert.equal(this.navSidebar.find('tr[class^="model-"]:visible').length, 0);
|
||||
});
|
|
@ -83,6 +83,33 @@
|
|||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="nav-sidebar-filter">
|
||||
<nav class="sticky" id="nav-sidebar">
|
||||
<input type="search" id="nav-filter"
|
||||
placeholder="Start typing to filter..."
|
||||
aria-label="Filter navigation items">
|
||||
<div class="app-auth module current-app">
|
||||
<table>
|
||||
<caption>
|
||||
<a href="/admin/auth/" class="section"
|
||||
title="Models in the Authentication and Authorization application">
|
||||
Authentication and Authorization
|
||||
</a>
|
||||
</caption>
|
||||
<tbody>
|
||||
<tr class="model-group">
|
||||
<th scope="row"><a href="/admin/auth/group/">Groups</a></th>
|
||||
<td><a href="/admin/auth/group/add/" class="addlink">Add</a></td>
|
||||
</tr>
|
||||
<tr class="model-user current-model">
|
||||
<th scope="row"><a href="/admin/auth/user/" aria-current="page">Users</a></th>
|
||||
<td><a href="/admin/auth/user/add/" class="addlink">Add</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</nav>
|
||||
</script>
|
||||
|
||||
<script src="../node_modules/qunit/qunit/qunit.js"></script>
|
||||
|
||||
|
@ -94,6 +121,9 @@
|
|||
<script src='../django/contrib/admin/static/admin/js/core.js' data-cover></script>
|
||||
<script src='./admin/core.test.js'></script>
|
||||
|
||||
<script src='../django/contrib/admin/static/admin/js/nav_sidebar.js' data-cover></script>
|
||||
<script src='./admin/navigation.test.js'></script>
|
||||
|
||||
<script src='../django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js' data-cover></script>
|
||||
|
||||
<script src='./admin/DateTimeShortcuts.test.js'></script>
|
||||
|
|
|
@ -143,3 +143,16 @@ class SeleniumTests(AdminSeleniumTestCase):
|
|||
self.selenium.get(self.live_server_url + reverse('test_with_sidebar:auth_user_changelist'))
|
||||
main_element = self.selenium.find_element_by_css_selector('#main')
|
||||
self.assertIn('shifted', main_element.get_attribute('class').split())
|
||||
|
||||
def test_sidebar_filter_persists(self):
|
||||
self.selenium.get(
|
||||
self.live_server_url +
|
||||
reverse('test_with_sidebar:auth_user_changelist')
|
||||
)
|
||||
filter_value_script = (
|
||||
"return localStorage.getItem('django.admin.navSidebarFilterValue')"
|
||||
)
|
||||
self.assertIsNone(self.selenium.execute_script(filter_value_script))
|
||||
filter_input = self.selenium.find_element_by_css_selector('#nav-filter')
|
||||
filter_input.send_keys('users')
|
||||
self.assertEqual(self.selenium.execute_script(filter_value_script), 'users')
|
||||
|
|
|
@ -3142,7 +3142,7 @@ class AdminViewListEditable(TestCase):
|
|||
# CSRF field = 1
|
||||
# field to track 'select all' across paginated views = 1
|
||||
# 6 + 4 + 4 + 1 + 2 + 1 + 1 = 19 inputs
|
||||
self.assertContains(response, "<input", count=19)
|
||||
self.assertContains(response, "<input", count=20)
|
||||
# 1 select per object = 3 selects
|
||||
self.assertContains(response, "<select", count=4)
|
||||
|
||||
|
@ -4980,7 +4980,7 @@ class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
|
|||
self.assertNotContains(response, 'name="posted"')
|
||||
# 3 fields + 2 submit buttons + 5 inline management form fields, + 2
|
||||
# hidden fields for inlines + 1 field for the inline + 2 empty form
|
||||
self.assertContains(response, "<input", count=15)
|
||||
self.assertContains(response, "<input", count=16)
|
||||
self.assertContains(response, formats.localize(datetime.date.today()))
|
||||
self.assertContains(response, "<label>Awesomeness level:</label>")
|
||||
self.assertContains(response, "Very awesome.")
|
||||
|
|
Loading…
Reference in New Issue