Compare commits

..

1 Commits

Author SHA1 Message Date
Arthur Hanson
a9a300197a Clear Rack Face when clear Rack (#21182)
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
* #20383 clear rack face if no rack on edit

* #20383 clear rack face if no rack on edit

* review changes

* review changes
2026-01-23 12:26:27 -05:00
7 changed files with 131 additions and 14 deletions

View File

@@ -20,7 +20,9 @@ from utilities.forms.fields import (
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
) )
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK from utilities.forms.widgets import (
APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
)
from utilities.jsonschema import JSONSchemaProperty from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
@@ -592,6 +594,14 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
}, },
) )
) )
face = forms.ChoiceField(
label=_('Face'),
choices=add_blank_choice(DeviceFaceChoices),
required=False,
widget=ClearableSelect(
requires_fields=['rack']
)
)
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'), label=_('Device type'),
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),

View File

@@ -409,10 +409,60 @@ ADMIN_MENU = Menu(
MenuGroup( MenuGroup(
label=_('Authentication'), label=_('Authentication'),
items=( items=(
get_model_item('users', 'user', _('Users')), MenuItem(
get_model_item('users', 'group', _('Groups')), link='users:user_list',
get_model_item('users', 'token', _('API Tokens')), link_text=_('Users'),
get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']), staff_only=True,
permissions=['users.view_user'],
buttons=(
MenuItemButton(
link='users:user_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=['users.add_user']
),
MenuItemButton(
link='users:user_bulk_import',
title='Import',
icon_class='mdi mdi-upload',
permissions=['users.add_user']
)
)
),
MenuItem(
link='users:group_list',
link_text=_('Groups'),
staff_only=True,
permissions=['users.view_group'],
buttons=(
MenuItemButton(
link='users:group_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=['users.add_group']
),
MenuItemButton(
link='users:group_bulk_import',
title='Import',
icon_class='mdi mdi-upload',
permissions=['users.add_group']
)
)
),
MenuItem(
link='users:token_list',
link_text=_('API Tokens'),
staff_only=True,
permissions=['users.view_token'],
buttons=get_model_buttons('users', 'token')
),
MenuItem(
link='users:objectpermission_list',
link_text=_('Permissions'),
staff_only=True,
permissions=['users.view_objectpermission'],
buttons=get_model_buttons('users', 'objectpermission', actions=['add'])
),
), ),
), ),
MenuGroup( MenuGroup(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
import TomSelect from 'tom-select';
import { getElements } from '../util';
/**
* Initialize clear-field dependencies.
* When a required field is cleared, dependent fields with data-requires-fields attribute will also be cleared.
*/
export function initClearField(): void {
// Find all fields with data-requires-fields attribute
for (const field of getElements<HTMLSelectElement>('[data-requires-fields]')) {
const requiredFieldsAttr = field.getAttribute('data-requires-fields');
if (!requiredFieldsAttr) continue;
// Parse the comma-separated list of required field names
const requiredFields = requiredFieldsAttr.split(',').map(name => name.trim());
// Set up listeners for each required field
for (const requiredFieldName of requiredFields) {
const requiredField = document.querySelector<HTMLSelectElement>(
`[name="${requiredFieldName}"]`,
);
if (!requiredField) continue;
// Listen for changes on the required field
requiredField.addEventListener('change', () => {
// If required field is cleared, also clear this dependent field
if (!requiredField.value || requiredField.value === '') {
// Check if this field uses TomSelect
const tomselect = (field as HTMLSelectElement & { tomselect?: TomSelect }).tomselect;
if (tomselect) {
tomselect.clear();
} else {
// Regular select field
field.value = '';
}
}
});
}
}
}

View File

@@ -1,9 +1,10 @@
import { initClearField } from './clearField';
import { initFormElements } from './elements'; import { initFormElements } from './elements';
import { initFilterModifiers } from './filterModifiers'; import { initFilterModifiers } from './filterModifiers';
import { initSpeedSelector } from './speedSelector'; import { initSpeedSelector } from './speedSelector';
export function initForms(): void { export function initForms(): void {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) { for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
func(); func();
} }
} }

View File

@@ -5,6 +5,7 @@ from ..utils import add_blank_choice
__all__ = ( __all__ = (
'BulkEditNullBooleanSelect', 'BulkEditNullBooleanSelect',
'ClearableSelect',
'ColorSelect', 'ColorSelect',
'HTMXSelect', 'HTMXSelect',
'SelectWithPK', 'SelectWithPK',
@@ -28,6 +29,21 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
) )
class ClearableSelect(forms.Select):
"""
A Select widget that will be automatically cleared when one or more required fields are cleared.
Args:
requires_fields: A list of field names that this field depends on. When any of these fields
are cleared, this field will also be cleared automatically via JavaScript.
"""
def __init__(self, *args, requires_fields=None, **kwargs):
super().__init__(*args, **kwargs)
if requires_fields:
self.attrs['data-requires-fields'] = ','.join(requires_fields)
class ColorSelect(forms.Select): class ColorSelect(forms.Select):
""" """
Extends the built-in Select widget to colorize each <option>. Extends the built-in Select widget to colorize each <option>.