Преглед на файлове

Fixed #27471 -- Made admin's filter choices collapsable.

Marcelo Galigniana преди 3 години
родител
ревизия
27aa7035f5

+ 24 - 1
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;

+ 30 - 0
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));
+        });
+    });
+}

+ 1 - 0
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 %}

+ 9 - 5
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>

BIN
docs/intro/_images/admin13t.png


BIN
docs/ref/contrib/admin/_images/list_filter.png


+ 52 - 0
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")
+        )