mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
* 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>
This commit is contained in:
parent
c81869c795
commit
8d39181842
@ -110,6 +110,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
(_('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(
|
type_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -164,6 +164,7 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'region_id', 'group_id')
|
||||||
status = forms.MultipleChoiceField(
|
status = forms.MultipleChoiceField(
|
||||||
label=_('Status'),
|
label=_('Status'),
|
||||||
choices=SiteStatusChoices,
|
choices=SiteStatusChoices,
|
||||||
@ -247,6 +248,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||||
(_('Weight'), ('weight', 'max_weight', 'weight_unit')),
|
(_('Weight'), ('weight', 'max_weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -419,6 +421,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
)),
|
)),
|
||||||
(_('Weight'), ('weight', 'weight_unit')),
|
(_('Weight'), ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -543,6 +546,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
)),
|
)),
|
||||||
(_('Weight'), ('weight', 'weight_unit')),
|
(_('Weight'), ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -619,6 +623,7 @@ class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
|
|
||||||
class PlatformFilterForm(NetBoxModelFilterSetForm):
|
class PlatformFilterForm(NetBoxModelFilterSetForm):
|
||||||
model = Platform
|
model = Platform
|
||||||
|
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -653,6 +658,7 @@ class DeviceFilterForm(
|
|||||||
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
|
'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(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -996,6 +1002,7 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||||||
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
|
||||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1227,6 +1234,7 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
|||||||
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
|
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
|
||||||
(_('Connection'), ('cabled', 'connected', 'occupied')),
|
(_('Connection'), ('cabled', 'connected', 'occupied')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'device_id')
|
||||||
vdc_id = DynamicModelMultipleChoiceField(
|
vdc_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=VirtualDeviceContext.objects.all(),
|
queryset=VirtualDeviceContext.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -300,6 +300,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||||
(_('Device/VM'), ('device_id', 'virtual_machine_id')),
|
(_('Device/VM'), ('device_id', 'virtual_machine_id')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
||||||
parent = forms.CharField(
|
parent = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
@ -452,6 +453,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
(_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
|
(_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
|
||||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'site_id')
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -145,12 +145,16 @@ class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMi
|
|||||||
model: The model class associated with the form
|
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
|
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.
|
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(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Search')
|
label=_('Search')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
selector_fields = ('filter_id', 'q')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -264,6 +264,11 @@ export class APISelect {
|
|||||||
switch (this.trigger) {
|
switch (this.trigger) {
|
||||||
case 'collapse':
|
case 'collapse':
|
||||||
if (collapse !== null) {
|
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
|
// If this element is part of a collapsible element, only load the data when the
|
||||||
// collapsible element is shown.
|
// collapsible element is shown.
|
||||||
// See: https://getbootstrap.com/docs/5.0/components/collapse/#events
|
// See: https://getbootstrap.com/docs/5.0/components/collapse/#events
|
||||||
|
@ -10,18 +10,18 @@
|
|||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for field in form.visible_fields %}
|
{% 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 }}">
|
<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 }}
|
{{ field.label }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
<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" />
|
<input type="hidden" name="_search" value="true" />
|
||||||
<div class="tab-content p-1">
|
<div class="tab-content p-1">
|
||||||
{% for field in form.visible_fields %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
|
@ -44,6 +44,7 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||||||
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
|
||||||
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'group_id')
|
||||||
type_id = DynamicModelMultipleChoiceField(
|
type_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=ClusterType.objects.all(),
|
queryset=ClusterType.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -186,6 +187,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
|
|||||||
(_('Virtual Machine'), ('cluster_id', 'virtual_machine_id')),
|
(_('Virtual Machine'), ('cluster_id', 'virtual_machine_id')),
|
||||||
(_('Attributes'), ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')),
|
(_('Attributes'), ('enabled', 'mac_address', 'vrf_id', 'l2vpn_id')),
|
||||||
)
|
)
|
||||||
|
selector_fields = ('filter_id', 'q', 'virtual_machine_id')
|
||||||
cluster_id = DynamicModelMultipleChoiceField(
|
cluster_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
Loading…
Reference in New Issue
Block a user