summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcelo Galigniana <marcelogaligniana@gmail.com>2022-02-14 00:03:53 -0300
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-04-26 08:44:31 +0200
commit27aa7035f57f0db30b6632e4274e18b430906799 (patch)
tree8573068be094049e218fe2b35c8a67b123451d86
parent37602e49484a88867f40e9498f86c49c2d1c5d7c (diff)
Fixed #27471 -- Made admin's filter choices collapsable.
-rw-r--r--django/contrib/admin/static/admin/css/changelists.css25
-rw-r--r--django/contrib/admin/static/admin/js/filters.js30
-rw-r--r--django/contrib/admin/templates/admin/change_list.html1
-rw-r--r--django/contrib/admin/templates/admin/filter.html14
-rw-r--r--docs/intro/_images/admin13t.pngbin16961 -> 26713 bytes
-rw-r--r--docs/ref/contrib/admin/_images/list_filter.pngbin38081 -> 40590 bytes
-rw-r--r--tests/admin_changelist/tests.py52
7 files changed, 116 insertions, 6 deletions
diff --git a/django/contrib/admin/static/admin/css/changelists.css b/django/contrib/admin/static/admin/css/changelists.css
index 4f8b89ae7c..68ba55787b 100644
--- a/django/contrib/admin/static/admin/css/changelists.css
+++ b/django/contrib/admin/static/admin/css/changelists.css
@@ -148,12 +148,35 @@
border-bottom: none;
}
-#changelist-filter h3 {
+#changelist-filter h3,
+#changelist-filter details summary {
font-weight: 400;
padding: 0 15px;
margin-bottom: 10px;
}
+#changelist-filter details summary > * {
+ display: inline;
+}
+
+#changelist-filter details > summary {
+ list-style-type: none;
+}
+
+#changelist-filter details > summary::-webkit-details-marker {
+ display: none;
+}
+
+#changelist-filter details > summary::before {
+ content: '→';
+ font-weight: bold;
+ color: var(--link-hover-color);
+}
+
+#changelist-filter details[open] > summary::before {
+ content: '↓';
+}
+
#changelist-filter ul {
margin: 5px 0;
padding: 0 15px 15px;
diff --git a/django/contrib/admin/static/admin/js/filters.js b/django/contrib/admin/static/admin/js/filters.js
new file mode 100644
index 0000000000..ba691ac82f
--- /dev/null
+++ b/django/contrib/admin/static/admin/js/filters.js
@@ -0,0 +1,30 @@
+/**
+ * Persist changelist filters state (collapsed/expanded).
+ */
+'use strict';
+{
+ // Init filters.
+ let filters = JSON.parse(sessionStorage.getItem('django.admin.filtersState'));
+
+ if (!filters) {
+ filters = {};
+ }
+
+ Object.entries(filters).forEach(([key, value]) => {
+ const detailElement = document.querySelector(`[data-filter-title='${key}']`);
+
+ // Check if the filter is present, it could be from other view.
+ if (detailElement) {
+ value ? detailElement.setAttribute('open', '') : detailElement.removeAttribute('open');
+ }
+ });
+
+ // Save filter state when clicks.
+ const details = document.querySelectorAll('details');
+ details.forEach(detail => {
+ detail.addEventListener('toggle', event => {
+ filters[`${event.target.dataset.filterTitle}`] = detail.open;
+ sessionStorage.setItem('django.admin.filtersState', JSON.stringify(filters));
+ });
+ });
+}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 12a495504f..29747c317e 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -21,6 +21,7 @@
{% block extrahead %}
{{ block.super }}
{{ media.js }}
+<script src="{% static 'admin/js/filters.js' %}" defer></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
diff --git a/django/contrib/admin/templates/admin/filter.html b/django/contrib/admin/templates/admin/filter.html
index b5a1bbab0e..a6094ecb5a 100644
--- a/django/contrib/admin/templates/admin/filter.html
+++ b/django/contrib/admin/templates/admin/filter.html
@@ -1,8 +1,12 @@
{% load i18n %}
-<h3>{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}</h3>
-<ul>
-{% for choice in choices %}
+<details data-filter-title="{{ title }}" open>
+ <summary>
+ {% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}
+ </summary>
+ <ul>
+ {% for choice in choices %}
<li{% if choice.selected %} class="selected"{% endif %}>
<a href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a></li>
-{% endfor %}
-</ul>
+ {% endfor %}
+ </ul>
+</details>
diff --git a/docs/intro/_images/admin13t.png b/docs/intro/_images/admin13t.png
index 8827932461..3fed5f1985 100644
--- a/docs/intro/_images/admin13t.png
+++ b/docs/intro/_images/admin13t.png
Binary files differ
diff --git a/docs/ref/contrib/admin/_images/list_filter.png b/docs/ref/contrib/admin/_images/list_filter.png
index 0d912d1925..8b6a5a7a54 100644
--- a/docs/ref/contrib/admin/_images/list_filter.png
+++ b/docs/ref/contrib/admin/_images/list_filter.png
Binary files differ
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index e24654dfcb..24c24a670b 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -1810,3 +1810,55 @@ class SeleniumTests(AdminSeleniumTestCase):
)
finally:
alert.dismiss()
+
+ def test_collapse_filters(self):
+ from selenium.webdriver.common.by import By
+
+ self.admin_login(username="super", password="secret")
+ self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
+
+ # The UserAdmin has 3 field filters by default: "staff status",
+ # "superuser status", and "active".
+ details = self.selenium.find_elements(By.CSS_SELECTOR, "details")
+ # All filters are opened at first.
+ for detail in details:
+ self.assertTrue(detail.get_attribute("open"))
+ # Collapse "staff' and "superuser" filters.
+ for detail in details[:2]:
+ summary = detail.find_element(By.CSS_SELECTOR, "summary")
+ summary.click()
+ self.assertFalse(detail.get_attribute("open"))
+ # Filters are in the same state after refresh.
+ self.selenium.refresh()
+ self.assertFalse(
+ self.selenium.find_element(
+ By.CSS_SELECTOR, "[data-filter-title='staff status']"
+ ).get_attribute("open")
+ )
+ self.assertFalse(
+ self.selenium.find_element(
+ By.CSS_SELECTOR, "[data-filter-title='superuser status']"
+ ).get_attribute("open")
+ )
+ self.assertTrue(
+ self.selenium.find_element(
+ By.CSS_SELECTOR, "[data-filter-title='active']"
+ ).get_attribute("open")
+ )
+ # Collapse a filter on another view (Bands).
+ self.selenium.get(
+ self.live_server_url + reverse("admin:admin_changelist_band_changelist")
+ )
+ self.selenium.find_element(By.CSS_SELECTOR, "summary").click()
+ # Go to Users view and then, back again to Bands view.
+ self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
+ self.selenium.get(
+ self.live_server_url + reverse("admin:admin_changelist_band_changelist")
+ )
+ # The filter remains in the same state.
+ self.assertFalse(
+ self.selenium.find_element(
+ By.CSS_SELECTOR,
+ "[data-filter-title='number of members']",
+ ).get_attribute("open")
+ )