فهرست منبع

Fixes #12751 - Usability improvements for object selector (#14387)

* Usability improvements for object selector:
* Adds preselected filters
* Applies the filter on selection instead of requiring the search button to be pushed

* Declare selector_fields on base form class

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
kkthxbye 2 سال پیش
والد
کامیت
8d39181842

+ 1 - 0
netbox/circuits/forms/filtersets.py

@@ -110,6 +110,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
         (_('Tenant'), ('tenant_group_id', 'tenant_id')),
         (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
+    selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'provider_id', 'provider_network_id')
     type_id = DynamicModelMultipleChoiceField(
         queryset=CircuitType.objects.all(),
         required=False,

+ 8 - 0
netbox/dcim/forms/filtersets.py

@@ -164,6 +164,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
         (_('Tenant'), ('tenant_group_id', 'tenant_id')),
         (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
+    selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
     status = forms.MultipleChoiceField(
         label=_('Status'),
         choices=SiteStatusChoices,
@@ -247,6 +248,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
         (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
         (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
     )
+    selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -419,6 +421,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
         )),
         (_('Weight'), ('weight', 'weight_unit')),
     )
+    selector_fields = ('filter_id', 'q', 'manufacturer_id')
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
@@ -543,6 +546,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
         )),
         (_('Weight'), ('weight', 'weight_unit')),
     )
+    selector_fields = ('filter_id', 'q', 'manufacturer_id')
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
@@ -619,6 +623,7 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
 
 class PlatformFilterForm(NetBoxModelFilterSetForm):
     model = Platform
+    selector_fields = ('filter_id', 'q', 'manufacturer_id')
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
@@ -653,6 +658,7 @@ class DeviceFilterForm(
             'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
         ))
     )
+    selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -996,6 +1002,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
         (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
         (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
+    selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -1227,6 +1234,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
         (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
         (_('Connection'), ('cabled', 'connected', 'occupied')),
     )
+    selector_fields = ('filter_id', 'q', 'device_id')
     vdc_id = DynamicModelMultipleChoiceField(
         queryset=VirtualDeviceContext.objects.all(),
         required=False,

+ 2 - 0
netbox/ipam/forms/filtersets.py

@@ -300,6 +300,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         (_('Tenant'), ('tenant_group_id', 'tenant_id')),
         (_('Device/VM'), ('device_id', 'virtual_machine_id')),
     )
+    selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
     parent = forms.CharField(
         required=False,
         widget=forms.TextInput(
@@ -452,6 +453,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         (_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
         (_('Tenant'), ('tenant_group_id', 'tenant_id')),
     )
+    selector_fields = ('filter_id', 'q', 'site_id')
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,

+ 4 - 0
netbox/netbox/forms/base.py

@@ -145,12 +145,16 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMi
         model: The model class associated with the form
         fieldsets: An iterable of two-tuples which define a heading and field set to display per section of
             the rendered form (optional). If not defined, the all fields will be rendered as a single section.
+        selector_fields: An iterable of names of fields to display by default when rendering the form as
+            a selector widget
     """
     q = forms.CharField(
         required=False,
         label=_('Search')
     )
 
+    selector_fields = ('filter_id', 'q')
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
netbox/project-static/dist/netbox.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 5 - 0
netbox/project-static/src/select/api/apiSelect.ts

@@ -264,6 +264,11 @@ export class APISelect {
     switch (this.trigger) {
       case 'collapse':
         if (collapse !== null) {
+          // If the element is collapsible but already shown, load the data immediately.
+          if (collapse.classList.contains('show')) {
+            Promise.all([this.loadData()]);
+          }
+
           // If this element is part of a collapsible element, only load the data when the
           // collapsible element is shown.
           // See: https://getbootstrap.com/docs/5.0/components/collapse/#events

+ 3 - 3
netbox/templates/htmx/object_selector.html

@@ -10,18 +10,18 @@
     <div class="list-group list-group-flush">
       {% for field in form.visible_fields %}
         <a href="#" class="list-group-item list-group-item-action px-0 py-1" data-bs-toggle="collapse" data-bs-target="#checkmark{{ forloop.counter }}, #selector{{ forloop.counter }}">
-          <span id="checkmark{{ forloop.counter }}" class="collapse{% if forloop.counter < 3 %} show{% endif %}"><i class="mdi mdi-check-bold"></i></span>
+          <span id="checkmark{{ forloop.counter }}" class="collapse{% if forloop.counter < 3 or field.name in form.selector_fields %} show{% endif %}"><i class="mdi mdi-check-bold"></i></span>
           {{ field.label }}
         </a>
       {% endfor %}
     </div>
   </div>
   <div class="col-9">
-    <form hx-get="{% url 'htmx_object_selector' %}?_model={{ model|meta:"label_lower" }}" hx-target="#selector_results" hx-trigger="load, submit, keyup from:#id_q delay:500ms">
+    <form hx-get="{% url 'htmx_object_selector' %}?_model={{ model|meta:"label_lower" }}" hx-target="#selector_results" hx-trigger="load, submit, change, keyup from:#id_q delay:500ms">
       <input type="hidden" name="_search" value="true" />
       <div class="tab-content p-1">
         {% for field in form.visible_fields %}
-          <div class="collapse{% if forloop.counter < 3 %} show{% endif %}" id="selector{{ forloop.counter }}">{% render_field field %}</div>
+          <div class="collapse{% if field.name in form.selector_fields %} show{% endif %}" id="selector{{ forloop.counter }}">{% render_field field %}</div>
         {% endfor %}
       </div>
       <div class="text-end">

+ 2 - 0
netbox/virtualization/forms/filtersets.py

@@ -44,6 +44,7 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
         (_('Tenant'), ('tenant_group_id', 'tenant_id')),
         (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
     )
+    selector_fields = ('filter_id', 'q', 'group_id')
     type_id = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         required=False,
@@ -186,6 +187,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
         (_('Virtual Machine'), ('cluster_id', 'virtual_machine_id')),
         (_('Attributes'), ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')),
     )
+    selector_fields = ('filter_id', 'q', 'virtual_machine_id')
     cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
         required=False,

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است