import queryString from 'query-string'; import { readableColor } from 'color2k'; import SlimSelect from 'slim-select'; import { createToast } from '../bs'; import { hasUrl, hasExclusions } from './util'; import { isTruthy, hasError, getElement, getApiData, isApiError, getElements, findFirstAdjacent, } from '../util'; import type { Option } from 'slim-select/dist/data'; type QueryFilter = Map; // Various one-off patterns to replace in query param keys. const REPLACE_PATTERNS = [ // Don't query `termination_a_device=1`, but rather `device=1`. [new RegExp(/termination_(a|b)_(.+)/g), '$2_id'], // A tenant's group relationship field is `group`, but the field name is `tenant_group`. [new RegExp(/tenant_(group)/g), '$1_id'], // Append `_id` to any fields [new RegExp(/^([A-Za-z0-9]+)(_id)?$/g), '$1_id'], ] as [RegExp, string][]; // Empty placeholder option. const PLACEHOLDER = { value: '', text: '', placeholder: true, } as Option; // Attributes which if truthy should render the option disabled. const DISABLED_ATTRIBUTES = ['occupied'] as string[]; /** * Manage a single API-backed select element's state. Each API select element is likely controlled * or dynamically updated by one or more other API select (or static select) elements' values. */ class APISelect { /** * Base `` element, or * by existence of specific fields such as `_depth`. */ private preSorted: boolean = false; /** * This instance's available options. */ private _options: Option[] = [PLACEHOLDER]; /** * Array of options values which should be considered disabled or static. */ private disabledOptions: Array = []; /** * Array of properties which if truthy on an API object should be considered disabled. */ private disabledAttributes: Array = DISABLED_ATTRIBUTES; constructor(base: HTMLSelectElement) { // Initialize readonly properties. this.base = base; this.name = base.name; if (base.getAttribute('pre-sorted') !== null) { this.preSorted = true; } if (hasUrl(base)) { const url = base.getAttribute('data-url') as string; this.url = url; this.queryUrl = url; } this.loadEvent = new Event(`netbox.select.onload.${base.name}`); this.placeholder = this.getPlaceholder(); this.disabledOptions = this.getDisabledOptions(); this.disabledAttributes = this.getDisabledAttributes(); this.slim = new SlimSelect({ select: this.base, allowDeselect: true, deselectLabel: ``, placeholder: this.placeholder, onChange: () => this.handleSlimChange(), }); // Initialize API query properties. this.getFilteredBy(); this.getPathKeys(); for (const filter of this.filterParams.keys()) { this.updateQueryParams(filter); } for (const filter of this.pathValues.keys()) { this.updatePathValues(filter); } this.queryParams.set('brief', true); this.queryParams.set('limit', 0); this.updateQueryUrl(); // Initialize element styling. this.resetClasses(); this.setSlimStyles(); // Initialize controlling elements. this.initResetButton(); // Add dependency event listeners. this.addEventListeners(); // Determine if this element is part of collapsible element. const collapse = this.base.closest('.content-container .collapse'); if (collapse !== null) { // 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 collapse.addEventListener('show.bs.collapse', () => this.loadData()); collapse.addEventListener('hide.bs.collapse', () => this.resetOptions()); } else { // Otherwise, load the data on render. Promise.all([this.loadData()]); } } /** * This instance's available options. */ public get options(): Option[] { return this._options; } /** * Sort incoming options by label and apply the new options to both the SlimSelect instance and * this manager's state. If the `preSorted` attribute exists on the base `