mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Clean up TypeScript file structure, fix missing VLAN tag visibility logic
This commit is contained in:
28
netbox/project-static/src/forms/actions.ts
Normal file
28
netbox/project-static/src/forms/actions.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { getElements, findFirstAdjacent, isTruthy } from '../util';
|
||||
|
||||
/**
|
||||
* Handle bulk add/edit/rename form actions.
|
||||
*
|
||||
* @param event Click Event
|
||||
*/
|
||||
function handleFormActionClick(event: Event): void {
|
||||
event.preventDefault();
|
||||
const element = event.currentTarget as HTMLElement;
|
||||
if (element !== null) {
|
||||
const form = findFirstAdjacent<HTMLFormElement>(element, 'form');
|
||||
const href = element.getAttribute('href');
|
||||
if (form !== null && isTruthy(href)) {
|
||||
form.setAttribute('action', href);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize bulk form action links.
|
||||
*/
|
||||
export function initFormActions(): void {
|
||||
for (const element of getElements<HTMLAnchorElement>('a.formaction')) {
|
||||
element.addEventListener('click', handleFormActionClick);
|
||||
}
|
||||
}
|
57
netbox/project-static/src/forms/elements.ts
Normal file
57
netbox/project-static/src/forms/elements.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { getElements, scrollTo } from '../util';
|
||||
|
||||
function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
||||
// Track the names of each invalid field.
|
||||
const invalids = new Set<string>();
|
||||
|
||||
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
|
||||
if (!element.validity.valid) {
|
||||
invalids.add(element.name);
|
||||
|
||||
// If the field is invalid, but contains the .is-valid class, remove it.
|
||||
if (element.classList.contains('is-valid')) {
|
||||
element.classList.remove('is-valid');
|
||||
}
|
||||
// If the field is invalid, but doesn't contain the .is-invalid class, add it.
|
||||
if (!element.classList.contains('is-invalid')) {
|
||||
element.classList.add('is-invalid');
|
||||
}
|
||||
} else {
|
||||
// If the field is valid, but contains the .is-invalid class, remove it.
|
||||
if (element.classList.contains('is-invalid')) {
|
||||
element.classList.remove('is-invalid');
|
||||
}
|
||||
// If the field is valid, but doesn't contain the .is-valid class, add it.
|
||||
if (!element.classList.contains('is-valid')) {
|
||||
element.classList.add('is-valid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalids.size !== 0) {
|
||||
// If there are invalid fields, pick the first field and scroll to it.
|
||||
const firstInvalid = form.elements.namedItem(Array.from(invalids)[0]) as Element;
|
||||
scrollTo(firstInvalid);
|
||||
|
||||
// If the form has invalid fields, don't submit it.
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an event listener to each form's submitter (button[type=submit]). When called, the
|
||||
* callback checks the validity of each form field and adds the appropriate Bootstrap CSS class
|
||||
* based on the field's validity.
|
||||
*/
|
||||
export function initFormElements(): void {
|
||||
for (const form of getElements('form')) {
|
||||
// Find each of the form's submitters. Most object edit forms have a "Create" and
|
||||
// a "Create & Add", so we need to add a listener to both.
|
||||
const submitters = form.querySelectorAll<HTMLButtonElement>('button[type=submit]');
|
||||
|
||||
for (const submitter of submitters) {
|
||||
// Add the event listener to each submitter.
|
||||
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
|
||||
}
|
||||
}
|
||||
}
|
17
netbox/project-static/src/forms/index.ts
Normal file
17
netbox/project-static/src/forms/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { initFormActions } from './actions';
|
||||
import { initFormElements } from './elements';
|
||||
import { initSpeedSelector } from './speedSelector';
|
||||
import { initScopeSelector } from './scopeSelector';
|
||||
import { initVlanTags } from './vlanTags';
|
||||
|
||||
export function initForms(): void {
|
||||
for (const func of [
|
||||
initFormActions,
|
||||
initFormElements,
|
||||
initSpeedSelector,
|
||||
initScopeSelector,
|
||||
initVlanTags,
|
||||
]) {
|
||||
func();
|
||||
}
|
||||
}
|
109
netbox/project-static/src/forms/scopeSelector.ts
Normal file
109
netbox/project-static/src/forms/scopeSelector.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { getElements } from '../util';
|
||||
|
||||
type ShowHideMap = {
|
||||
default: { hide: string[]; show: string[] };
|
||||
[k: string]: { hide: string[]; show: string[] };
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of scope names to arrays of object types whose fields should be hidden or shown when
|
||||
* the scope type (key) is selected.
|
||||
*
|
||||
* For example, if `region` is the scope type, the fields with IDs listed in
|
||||
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
|
||||
* showHideMap.region.show should be shown.
|
||||
*/
|
||||
const showHideMap: ShowHideMap = {
|
||||
region: {
|
||||
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region'],
|
||||
},
|
||||
'site group': {
|
||||
hide: ['id_region', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_sitegroup'],
|
||||
},
|
||||
site: {
|
||||
hide: ['id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site'],
|
||||
},
|
||||
location: {
|
||||
hide: ['id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location'],
|
||||
},
|
||||
rack: {
|
||||
hide: ['id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||
},
|
||||
'cluster group': {
|
||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_cluster'],
|
||||
show: ['id_clustergroup'],
|
||||
},
|
||||
cluster: {
|
||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||
show: ['id_clustergroup', 'id_cluster'],
|
||||
},
|
||||
default: {
|
||||
hide: [
|
||||
'id_region',
|
||||
'id_sitegroup',
|
||||
'id_site',
|
||||
'id_location',
|
||||
'id_rack',
|
||||
'id_clustergroup',
|
||||
'id_cluster',
|
||||
],
|
||||
show: [],
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Toggle visibility of a given element's parent.
|
||||
* @param query CSS Query.
|
||||
* @param action Show or Hide the Parent.
|
||||
*/
|
||||
function toggleParentVisibility(query: string, action: 'show' | 'hide') {
|
||||
for (const element of getElements(query)) {
|
||||
if (action === 'show') {
|
||||
element.parentElement?.classList.remove('d-none', 'invisible');
|
||||
} else {
|
||||
element.parentElement?.classList.add('d-none', 'invisible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle changes to the Scope Type field.
|
||||
*/
|
||||
function handleScopeChange(event: Event) {
|
||||
const element = event.currentTarget as HTMLSelectElement;
|
||||
// Scope type's innerText looks something like `DCIM > region`.
|
||||
const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
|
||||
|
||||
for (const [scope, fields] of Object.entries(showHideMap)) {
|
||||
// If the scope type ends with the specified scope, toggle its field visibility according to
|
||||
// the show/hide values.
|
||||
if (scopeType.endsWith(scope)) {
|
||||
for (const field of fields.hide) {
|
||||
toggleParentVisibility(`#${field}`, 'hide');
|
||||
}
|
||||
for (const field of fields.show) {
|
||||
toggleParentVisibility(`#${field}`, 'show');
|
||||
}
|
||||
// Stop on first match.
|
||||
break;
|
||||
} else {
|
||||
// Otherwise, hide all fields.
|
||||
for (const field of showHideMap.default.hide) {
|
||||
toggleParentVisibility(`#${field}`, 'hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize scope type select event listeners.
|
||||
*/
|
||||
export function initScopeSelector(): void {
|
||||
for (const element of getElements<HTMLSelectElement>('#id_scope_type')) {
|
||||
element.addEventListener('change', handleScopeChange);
|
||||
}
|
||||
}
|
24
netbox/project-static/src/forms/speedSelector.ts
Normal file
24
netbox/project-static/src/forms/speedSelector.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { getElements } from '../util';
|
||||
|
||||
/**
|
||||
* Set the value of the number input field based on the selection of the dropdown.
|
||||
*/
|
||||
export function initSpeedSelector(): void {
|
||||
for (const element of getElements<HTMLAnchorElement>('a.set_speed')) {
|
||||
if (element !== null) {
|
||||
function handleClick(event: Event) {
|
||||
// Don't reload the page (due to href="#").
|
||||
event.preventDefault();
|
||||
// Get the value of the `data` attribute on the dropdown option.
|
||||
const value = element.getAttribute('data');
|
||||
// Find the input element referenced by the dropdown element.
|
||||
const input = document.getElementById(element.target) as Nullable<HTMLInputElement>;
|
||||
if (input !== null && value !== null) {
|
||||
// Set the value of the input field to the `data` attribute's value.
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
element.addEventListener('click', handleClick);
|
||||
}
|
||||
}
|
||||
}
|
116
netbox/project-static/src/forms/vlanTags.ts
Normal file
116
netbox/project-static/src/forms/vlanTags.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { all, getElement, resetSelect, toggleVisibility } from '../util';
|
||||
|
||||
/**
|
||||
* Get a select element's containing `.row` element.
|
||||
*
|
||||
* @param element Select element.
|
||||
* @returns Containing row element.
|
||||
*/
|
||||
function fieldContainer(element: Nullable<HTMLSelectElement>): Nullable<HTMLElement> {
|
||||
const container = element?.parentElement?.parentElement ?? null;
|
||||
if (container !== null && container.classList.contains('row')) {
|
||||
return container;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle element visibility when the mode field does not have a value.
|
||||
*/
|
||||
function handleModeNone(): void {
|
||||
const elements = [
|
||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||
];
|
||||
|
||||
if (all(elements)) {
|
||||
const [taggedVlans, untaggedVlan] = elements;
|
||||
resetSelect(untaggedVlan);
|
||||
resetSelect(taggedVlans);
|
||||
for (const element of elements) {
|
||||
toggleVisibility(fieldContainer(element), 'hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle element visibility when the mode field's value is Access.
|
||||
*/
|
||||
function handleModeAccess(): void {
|
||||
const elements = [
|
||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||
];
|
||||
if (all(elements)) {
|
||||
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
||||
resetSelect(taggedVlans);
|
||||
toggleVisibility(fieldContainer(vlanGroup), 'show');
|
||||
toggleVisibility(fieldContainer(untaggedVlan), 'show');
|
||||
toggleVisibility(fieldContainer(taggedVlans), 'hide');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle element visibility when the mode field's value is Tagged.
|
||||
*/
|
||||
function handleModeTagged(): void {
|
||||
const elements = [
|
||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||
];
|
||||
if (all(elements)) {
|
||||
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
||||
toggleVisibility(fieldContainer(taggedVlans), 'show');
|
||||
toggleVisibility(fieldContainer(vlanGroup), 'show');
|
||||
toggleVisibility(fieldContainer(untaggedVlan), 'show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle element visibility when the mode field's value is Tagged (All).
|
||||
*/
|
||||
function handleModeTaggedAll(): void {
|
||||
const elements = [
|
||||
getElement<HTMLSelectElement>('id_tagged_vlans'),
|
||||
getElement<HTMLSelectElement>('id_untagged_vlan'),
|
||||
getElement<HTMLSelectElement>('id_vlan_group'),
|
||||
];
|
||||
if (all(elements)) {
|
||||
const [taggedVlans, untaggedVlan, vlanGroup] = elements;
|
||||
resetSelect(taggedVlans);
|
||||
toggleVisibility(fieldContainer(vlanGroup), 'show');
|
||||
toggleVisibility(fieldContainer(untaggedVlan), 'show');
|
||||
toggleVisibility(fieldContainer(taggedVlans), 'hide');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset field visibility when the mode field's value changes.
|
||||
*/
|
||||
function handleModeChange(element: HTMLSelectElement): void {
|
||||
switch (element.value) {
|
||||
case 'access':
|
||||
handleModeAccess();
|
||||
break;
|
||||
case 'tagged':
|
||||
handleModeTagged();
|
||||
break;
|
||||
case 'tagged-all':
|
||||
handleModeTaggedAll();
|
||||
break;
|
||||
case '':
|
||||
handleModeNone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function initVlanTags(): void {
|
||||
const element = getElement<HTMLSelectElement>('id_mode');
|
||||
if (element !== null) {
|
||||
element.addEventListener('change', () => handleModeChange(element));
|
||||
handleModeChange(element);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user