1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #14917: Replace slim-select with tom-select (#15080)

* Experimenting

* Remove testing resources

* Replace ApiSelect with TomSelect

* Add color support

* Add clear button

* Clear cached options when searching dynamic selects

* Add support for static parameters

* Refactor TomSelect implementation

* Add dynamic parameter support

* Limit number of options to 100

* Remove redundant api_url definitions for user model

* Add support for disabled indicator

* Remove obsolete value-field attr on dynamic select widgets

* Remove obsolete fetch_trigger kwarg from dynamic model choice widgets

* Remove obsolete empty_label kwarg from dynamic model choice widgets

* Add support for API path variables

* Add support for setting a 'null' option

* Annotate depth for recursive hierarchies

* Misc cleanup

* Remove obsolete APISelect code

* Remove slim-select & just-debounce-it

* Clean up type validation

* Closes #14237: Clear child selections on change to parent selection

* Use an MD icon for the clear button

* Use an MD icon for the clear button

* Explain why noUnusedParameters is disabled
This commit is contained in:
Jeremy Stretch
2024-02-08 15:07:04 -05:00
committed by GitHub
parent 11697d19a6
commit d63e1dacbf
32 changed files with 772 additions and 1506 deletions

View File

@@ -119,10 +119,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
user = DynamicModelMultipleChoiceField(
queryset=get_user_model().objects.all(),
required=False,
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
label=_('User')
)

View File

@@ -393,10 +393,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
user_id = DynamicModelMultipleChoiceField(
queryset=get_user_model().objects.all(),
required=False,
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
label=_('User')
)
tag = TagFilterField(model)
@@ -551,8 +548,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
part_number = forms.CharField(
label=_('Part number'),
@@ -828,8 +824,7 @@ class VirtualDeviceContextFilterForm(
device = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
label=_('Device'),
fetch_trigger='open'
label=_('Device')
)
status = forms.MultipleChoiceField(
label=_('Status'),
@@ -855,8 +850,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
label=_('Manufacturer'),
fetch_trigger='open'
label=_('Manufacturer')
)
module_type_id = DynamicModelMultipleChoiceField(
queryset=ModuleType.objects.all(),
@@ -864,8 +858,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
query_params={
'manufacturer_id': '$manufacturer_id'
},
label=_('Type'),
fetch_trigger='open'
label=_('Type')
)
status = forms.MultipleChoiceField(
label=_('Status'),
@@ -1414,8 +1407,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),
required=False,
label=_('Role'),
fetch_trigger='open'
label=_('Role')
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),

View File

@@ -381,8 +381,7 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
cluster_type_id = DynamicModelMultipleChoiceField(
queryset=ClusterType.objects.all(),
required=False,
label=_('Cluster types'),
fetch_trigger='open'
label=_('Cluster types')
)
cluster_group_id = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
@@ -462,10 +461,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
created_by_id = DynamicModelMultipleChoiceField(
queryset=get_user_model().objects.all(),
required=False,
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
label=_('User')
)
assigned_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),
@@ -508,10 +504,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
user_id = DynamicModelMultipleChoiceField(
queryset=get_user_model().objects.all(),
required=False,
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
label=_('User')
)
changed_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -25,6 +25,7 @@
"query-string": "^7.1.1",
"sass": "^1.55.0",
"slim-select": "^1.27.1",
"tom-select": "^2.3.1",
"typeface-inter": "^3.18.1",
"typeface-roboto-mono": "^1.1.13"
},
@@ -225,6 +226,19 @@
"node": ">= 8"
}
},
"node_modules/@orchidjs/sifter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz",
"integrity": "sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==",
"dependencies": {
"@orchidjs/unicode-variants": "^1.0.4"
}
},
"node_modules/@orchidjs/unicode-variants": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz",
"integrity": "sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ=="
},
"node_modules/@pkgr/utils": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -3888,6 +3902,22 @@
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=",
"license": "MIT"
},
"node_modules/tom-select": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.3.1.tgz",
"integrity": "sha512-QS4vnOcB6StNGqX4sGboGXL2fkhBF2gIBB+8Hwv30FZXYPn0CyYO8kkdATRvwfCTThxiR4WcXwKJZ3cOmtI9eg==",
"dependencies": {
"@orchidjs/sifter": "^1.0.3",
"@orchidjs/unicode-variants": "^1.0.4"
},
"engines": {
"node": "*"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tom-select"
}
},
"node_modules/tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",

View File

@@ -31,16 +31,16 @@
"gridstack": "^7.2.3",
"html-entities": "^2.3.3",
"htmx.org": "^1.8.0",
"just-debounce-it": "^3.1.1",
"query-string": "^7.1.1",
"sass": "^1.55.0",
"slim-select": "^1.27.1",
"tom-select": "^2.3.1",
"typeface-inter": "^3.18.1",
"typeface-roboto-mono": "^1.1.13"
},
"devDependencies": {
"@types/bootstrap": "5.2.10",
"@types/cookie": "^0.5.1",
"@types/node": "^20.11.16",
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"esbuild": "^0.13.15",

View File

@@ -1,11 +1,11 @@
import { getElements, isTruthy } from './util';
import { initButtons } from './buttons';
import { initSelect } from './select';
import { initSelects } from './select';
import { initObjectSelector } from './objectSelector';
import { initBootstrap } from './bs';
function initDepedencies(): void {
for (const init of [initButtons, initSelect, initObjectSelector, initBootstrap]) {
for (const init of [initButtons, initSelects, initObjectSelector, initBootstrap]) {
init();
}
}

View File

@@ -1,4 +1,5 @@
import '@popperjs/core';
import 'bootstrap';
import 'htmx.org';
import 'tom-select';
import './netbox';

View File

@@ -1,7 +1,7 @@
import { initForms } from './forms';
import { initBootstrap } from './bs';
import { initQuickSearch } from './search';
import { initSelect } from './select';
import { initSelects } from './select';
import { initButtons } from './buttons';
import { initColorMode } from './colorMode';
import { initMessages } from './messages';
@@ -22,7 +22,7 @@ function initDocument(): void {
initMessages,
initForms,
initQuickSearch,
initSelect,
initSelects,
initDateSelector,
initButtons,
initClipboard,

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
import { getElements } from '../../util';
import { APISelect } from './apiSelect';
export function initApiSelect(): void {
for (const select of getElements<HTMLSelectElement>('.netbox-api-select:not([data-ssid])')) {
new APISelect(select);
}
}
export type { Trigger } from './types';

View File

@@ -1,199 +0,0 @@
import type { Stringifiable } from 'query-string';
import type { Option, Optgroup } from 'slim-select/dist/data';
/**
* Map of string keys to primitive array values accepted by `query-string`. Keys are used as
* URL query parameter keys. Values correspond to query param values, enforced as an array
* for easier handling. For example, a mapping of `{ site_id: [1, 2] }` is serialized by
* `query-string` as `?site_id=1&site_id=2`. Likewise, `{ site_id: [1] }` is serialized as
* `?site_id=1`.
*/
export type QueryFilter = Map<string, Stringifiable[]>;
/**
* Tracked data for a related field. This is the value of `APISelect.filterFields`.
*/
export type FilterFieldValue = {
/**
* Key to use in the query parameter itself.
*/
queryParam: string;
/**
* Value to use in the query parameter for the related field.
*/
queryValue: Stringifiable[];
/**
* @see `DataFilterFields.includeNull`
*/
includeNull: boolean;
};
/**
* JSON data structure from `data-dynamic-params` attribute.
*/
export type DataDynamicParam = {
/**
* Name of form field to track.
*
* @example [name="tenant_group"]
*/
fieldName: string;
/**
* Query param key.
*
* @example group_id
*/
queryParam: string;
};
/**
* `queryParams` Map value.
*/
export type QueryParam = {
queryParam: string;
queryValue: Stringifiable[];
};
/**
* JSON data structure from `data-static-params` attribute.
*/
export type DataStaticParam = {
queryParam: string;
queryValue: Stringifiable | Stringifiable[];
};
/**
* JSON data passed from Django on the `data-filter-fields` attribute.
*/
export type DataFilterFields = {
/**
* Related field form name (`[name="<fieldName>"]`)
*
* @example tenant_group
*/
fieldName: string;
/**
* Key to use in the query parameter itself.
*
* @example group_id
*/
queryParam: string;
/**
* Optional default value. If set, value will be added to the query parameters prior to the
* initial API call and will be maintained until the field `fieldName` references (if one exists)
* is updated with a new value.
*
* @example 1
*/
defaultValue: Nullable<Stringifiable | Stringifiable[]>;
/**
* Include `null` on queries for the related field. For example, if `true`, `?<fieldName>=null`
* will be added to all API queries for this field.
*/
includeNull: boolean;
};
/**
* Map of string keys to primitive values. Used to track variables within URLs from the server. For
* example, `/api/$key/thing`. `PathFilter` tracks `$key` as `{ key: '' }` in the map, and when the
* value is later known, the value is set — `{ key: 'value' }`, and the URL is transformed to
* `/api/value/thing`.
*/
export type PathFilter = Map<string, Stringifiable>;
/**
* Merge or replace incoming options with current options.
*/
export type ApplyMethod = 'merge' | 'replace';
/**
* Trigger for which the select instance should fetch its data from the NetBox API.
*/
export type Trigger =
/**
* Load data when the select element is opened.
*/
| 'open'
/**
* Load data when the element is loaded.
*/
| 'load'
/**
* Load data when a parent element is uncollapsed.
*/
| 'collapse';
/**
* Strict Type Guard to determine if a deserialized value from the `data-filter-fields` attribute
* is of type `DataFilterFields`.
*
* @param value Deserialized value from `data-filter-fields` attribute.
*/
export function isDataFilterFields(value: unknown): value is DataFilterFields[] {
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item === 'object' && item !== null) {
if ('fieldName' in item && 'queryParam' in item) {
return (
typeof (item as DataFilterFields).fieldName === 'string' &&
typeof (item as DataFilterFields).queryParam === 'string'
);
}
}
}
}
return false;
}
/**
* Strict Type Guard to determine if a deserialized value from the `data-dynamic-params` attribute
* is of type `DataDynamicParam[]`.
*
* @param value Deserialized value from `data-dynamic-params` attribute.
*/
export function isDataDynamicParams(value: unknown): value is DataDynamicParam[] {
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item === 'object' && item !== null) {
if ('fieldName' in item && 'queryParam' in item) {
return (
typeof (item as DataDynamicParam).fieldName === 'string' &&
typeof (item as DataDynamicParam).queryParam === 'string'
);
}
}
}
}
return false;
}
/**
* Strict Type Guard to determine if a deserialized value from the `data-static-params` attribute
* is of type `DataStaticParam[]`.
*
* @param value Deserialized value from `data-static-params` attribute.
*/
export function isStaticParams(value: unknown): value is DataStaticParam[] {
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item === 'object' && item !== null) {
if ('queryParam' in item && 'queryValue' in item) {
return (
typeof (item as DataStaticParam).queryParam === 'string' &&
typeof (item as DataStaticParam).queryValue !== 'undefined'
);
}
}
}
}
return false;
}
/**
* Type guard to determine if a SlimSelect `dataObject` is an `Option`.
*
* @param data Option or Option Group
*/
export function isOption(data: Option | Optgroup): data is Option {
return !('options' in data);
}

View File

@@ -1,7 +1,7 @@
import { isTruthy } from '../../util';
import { isDataDynamicParams } from './types';
import { isDataDynamicParams } from '../types';
import type { QueryParam } from './types';
import type { QueryParam } from '../types';
/**
* Extension of built-in `Map` to add convenience functions.

View File

@@ -0,0 +1,305 @@
import { RecursivePartial, TomInput, TomOption, TomSettings } from 'tom-select/dist/types/types';
import { addClasses } from 'tom-select/src/vanilla'
import queryString from 'query-string';
import TomSelect from 'tom-select';
import type { Stringifiable } from 'query-string';
import { DynamicParamsMap } from './dynamicParamsMap';
// Transitional
import { QueryFilter, PathFilter } from '../types'
import { getElement, replaceAll } from '../../util';
// Extends TomSelect to provide enhanced fetching of options via the REST API
export class DynamicTomSelect extends TomSelect {
public readonly nullOption: Nullable<TomOption> = null;
// Transitional code from APISelect
private readonly queryParams: QueryFilter = new Map();
private readonly staticParams: QueryFilter = new Map();
private readonly dynamicParams: DynamicParamsMap = new DynamicParamsMap();
private readonly pathValues: PathFilter = new Map();
/**
* Overrides
*/
constructor( input_arg: string|TomInput, user_settings: RecursivePartial<TomSettings> ) {
super(input_arg, user_settings);
// Glean the REST API endpoint URL from the <select> element
this.api_url = this.input.getAttribute('data-url') as string;
// Set the null option (if any)
const nullOption = this.input.getAttribute('data-null-option');
if (nullOption) {
let valueField = this.settings.valueField;
let labelField = this.settings.labelField;
this.nullOption = {}
this.nullOption[valueField] = 'null';
this.nullOption[labelField] = nullOption;
}
// Populate static query parameters.
this.getStaticParams();
for (const [key, value] of this.staticParams.entries()) {
this.queryParams.set(key, value);
}
// Populate dynamic query parameters
this.getDynamicParams();
for (const filter of this.dynamicParams.keys()) {
this.updateQueryParams(filter);
}
// Path values
this.getPathKeys();
for (const filter of this.pathValues.keys()) {
this.updatePathValues(filter);
}
// Add dependency event listeners.
this.addEventListeners();
}
load(value: string) {
const self = this;
const url = self.getRequestUrl(value);
// Automatically clear any cached options. (Only options included
// in the API response should be present.)
self.clearOptions();
addClasses(self.wrapper, self.settings.loadingClass);
self.loading++;
// Populate the null option (if any) if not searching
if (self.nullOption && !value) {
self.addOption(self.nullOption);
}
// Make the API request
fetch(url)
.then(response => response.json())
.then(json => {
self.loadCallback(json.results, []);
}).catch(()=>{
self.loadCallback([], []);
});
}
/**
* Custom methods
*/
// Formulate and return the complete URL for an API request, including any query parameters.
getRequestUrl(search: string): string {
let url = this.api_url;
// Create new URL query parameters based on the current state of `queryParams` and create an
// updated API query URL.
const query = {} as Dict<Stringifiable[]>;
for (const [key, value] of this.queryParams.entries()) {
query[key] = value;
}
// Replace any variables in the URL with values from `pathValues` if set.
for (const [key, value] of this.pathValues.entries()) {
for (const result of this.api_url.matchAll(new RegExp(`({{${key}}})`, 'g'))) {
if (value) {
url = replaceAll(url, result[1], value.toString());
}
}
}
// Append the search query, if any
if (search) {
query['q'] = [search];
}
// Add standard parameters
query['brief'] = [true];
query['limit'] = [this.settings.maxOptions];
return queryString.stringifyUrl({ url, query });
}
/**
* Transitional methods
*/
// Determine if this instance's options should be filtered by static values passed from the
// server. Looks for the DOM attribute `data-static-params`, the value of which is a JSON
// array of objects containing key/value pairs to add to `this.staticParams`.
private getStaticParams(): void {
const serialized = this.input.getAttribute('data-static-params');
try {
if (serialized) {
const deserialized = JSON.parse(serialized);
if (deserialized) {
for (const { queryParam, queryValue } of deserialized) {
if (Array.isArray(queryValue)) {
this.staticParams.set(queryParam, queryValue);
} else {
this.staticParams.set(queryParam, [queryValue]);
}
}
}
}
} catch (err) {
console.group(`Unable to determine static query parameters for select field '${this.name}'`);
console.warn(err);
console.groupEnd();
}
}
// Determine if this instances' options should be filtered by the value of another select
// element. Looks for the DOM attribute `data-dynamic-params`, the value of which is a JSON
// array of objects containing information about how to handle the related field.
private getDynamicParams(): void {
const serialized = this.input.getAttribute('data-dynamic-params');
try {
this.dynamicParams.addFromJson(serialized);
} catch (err) {
console.group(`Unable to determine dynamic query parameters for select field '${this.name}'`);
console.warn(err);
console.groupEnd();
}
}
// Parse the `data-url` attribute to add any variables to `pathValues` as keys with empty
// values. As those keys' corresponding form fields' values change, `pathValues` will be
// updated to reflect the new value.
private getPathKeys() {
for (const result of this.api_url.matchAll(new RegExp(`{{(.+)}}`, 'g'))) {
this.pathValues.set(result[1], '');
}
}
// Update an element's API URL based on the value of another element on which this element
// relies.
private updateQueryParams(fieldName: string): void {
// Find the element dependency.
const element = document.querySelector<HTMLSelectElement>(`[name="${fieldName}"]`);
if (element !== null) {
// Initialize the element value as an array, in case there are multiple values.
let elementValue = [] as Stringifiable[];
if (element.multiple) {
// If this is a multi-select (form filters, tags, etc.), use all selected options as the value.
elementValue = Array.from(element.options)
.filter(o => o.selected)
.map(o => o.value);
} else if (element.value !== '') {
// If this is single-select (most fields), use the element's value. This seemingly
// redundant/verbose check is mainly for performance, so we're not running the above three
// functions (`Array.from()`, `Array.filter()`, `Array.map()`) every time every select
// field's value changes.
elementValue = [element.value];
}
if (elementValue.length > 0) {
// If the field has a value, add it to the map.
this.dynamicParams.updateValue(fieldName, elementValue);
// Get the updated value.
const current = this.dynamicParams.get(fieldName);
if (typeof current !== 'undefined') {
const { queryParam, queryValue } = current;
let value = [] as Stringifiable[];
if (this.staticParams.has(queryParam)) {
// If the field is defined in `staticParams`, we should merge the dynamic value with
// the static value.
const staticValue = this.staticParams.get(queryParam);
if (typeof staticValue !== 'undefined') {
value = [...staticValue, ...queryValue];
}
} else {
// If the field is _not_ defined in `staticParams`, we should replace the current value
// with the new dynamic value.
value = queryValue;
}
if (value.length > 0) {
this.queryParams.set(queryParam, value);
} else {
this.queryParams.delete(queryParam);
}
}
} else {
// Otherwise, delete it (we don't want to send an empty query like `?site_id=`)
const queryParam = this.dynamicParams.queryParam(fieldName);
if (queryParam !== null) {
this.queryParams.delete(queryParam);
}
}
}
}
// Update `pathValues` based on the form value of another element.
private updatePathValues(id: string): void {
const key = replaceAll(id, /^id_/i, '');
const element = getElement<HTMLSelectElement>(`id_${key}`);
if (element !== null) {
// If this element's URL contains variable tags ({{), replace the tag with the dependency's
// value. For example, if the dependency is the `rack` field, and the `rack` field's value
// is `1`, this element's URL would change from `/dcim/racks/{{rack}}/` to `/dcim/racks/1/`.
const hasReplacement =
this.api_url.includes(`{{`) && Boolean(this.api_url.match(new RegExp(`({{(${id})}})`, 'g')));
if (hasReplacement) {
if (element.value) {
// If the field has a value, add it to the map.
this.pathValues.set(id, element.value);
} else {
// Otherwise, reset the value.
this.pathValues.set(id, '');
}
}
}
}
/**
* Events
*/
// Add event listeners to this element and its dependencies so that when dependencies change
//this element's options are updated.
private addEventListeners(): void {
// Create a unique iterator of all possible form fields which, when changed, should cause this
// element to update its API query.
const dependencies = new Set([...this.dynamicParams.keys(), ...this.pathValues.keys()]);
for (const dep of dependencies) {
const filterElement = document.querySelector(`[name="${dep}"]`);
if (filterElement !== null) {
// Subscribe to dependency changes.
filterElement.addEventListener('change', event => this.handleEvent(event));
}
// Subscribe to changes dispatched by this state manager.
this.input.addEventListener(`netbox.select.onload.${dep}`, event => this.handleEvent(event));
}
}
// Event handler to be dispatched any time a dependency's value changes. For example, when the
// value of `tenant_group` changes, `handleEvent` is called to get the current value of
// `tenant_group` and update the query parameters and API query URL for the `tenant` field.
private handleEvent(event: Event): void {
const target = event.target as HTMLSelectElement;
// Update the element's URL after any changes to a dependency.
this.updateQueryParams(target.name);
this.updatePathValues(target.name);
// Clear any previous selection(s) as the parent filter has changed
this.clear();
// Load new data.
this.load(this.lastValue);
}
}

View File

@@ -1,82 +0,0 @@
import SlimSelect from 'slim-select';
import { readableColor } from 'color2k';
import { getElements } from '../util';
import type { Option } from 'slim-select/dist/data';
/**
* Determine if the option has a valid value (i.e., is not the placeholder).
*/
function canChangeColor(option: Option | HTMLOptionElement): boolean {
return typeof option.value === 'string' && option.value !== '';
}
/**
* Style the container element based on the selected option value.
*/
function styleContainer(
instance: InstanceType<typeof SlimSelect>,
option: Option | HTMLOptionElement,
): void {
if (instance.slim.singleSelected !== null) {
if (canChangeColor(option)) {
// Get the background color from the selected option's value.
const bg = `#${option.value}`;
// Determine an accessible foreground color based on the background color.
const fg = readableColor(bg);
// Set the container's style attributes.
instance.slim.singleSelected.container.style.backgroundColor = bg;
instance.slim.singleSelected.container.style.color = fg;
} else {
// If the color cannot be set (i.e., the placeholder), remove any inline styles.
instance.slim.singleSelected.container.removeAttribute('style');
}
}
}
/**
* Initialize color selection widget. Dynamically change the style of the select container to match
* the selected option.
*/
export function initColorSelect(): void {
for (const select of getElements<HTMLSelectElement>(
'select.netbox-color-select:not([data-ssid])',
)) {
for (const option of select.options) {
if (canChangeColor(option)) {
// Get the background color from the option's value.
const bg = `#${option.value}`;
// Determine an accessible foreground color based on the background color.
const fg = readableColor(bg);
// Set the option's style attributes.
option.style.backgroundColor = bg;
option.style.color = fg;
}
}
const instance = new SlimSelect({
select,
allowDeselect: true,
// Inherit the calculated color on the deselect icon.
deselectLabel: `<i class="mdi mdi-close-circle" style="color: currentColor;"></i>`,
});
// Style the select container to match any pre-selectd options.
for (const option of instance.data.data) {
if ('selected' in option && option.selected) {
styleContainer(instance, option);
break;
}
}
// Don't inherit the select element's classes.
for (const className of select.classList) {
instance.slim.container.classList.remove(className);
}
// Change the SlimSelect container's style based on the selected option.
instance.onChange = option => styleContainer(instance, option);
}
}

View File

@@ -0,0 +1,9 @@
export const config = {
plugins: {
// Provides the "clear" button on the widget
clear_button: {
html: (data: Dict) =>
`<i class="mdi mdi-close-circle ${data.className}" title="${data.title}"></i>`,
},
},
};

View File

@@ -0,0 +1,51 @@
import { TomOption } from 'tom-select/src/types';
import { escape_html } from 'tom-select/src/utils';
import { DynamicTomSelect } from './classes/dynamicTomSelect';
import { config } from './config';
import { getElements } from '../util';
const VALUE_FIELD = 'id';
const LABEL_FIELD = 'display';
const MAX_OPTIONS = 100;
// Render the HTML for a dropdown option
function renderOption(data: TomOption, escape: typeof escape_html) {
// If the option has a `_depth` property, indent its label
if (typeof data._depth === 'number' && data._depth > 0) {
return `<div>${'─'.repeat(data._depth)} ${escape(data[LABEL_FIELD])}</div>`;
}
return `<div>${escape(data[LABEL_FIELD])}</div>`;
}
// Initialize <select> elements which are populated via a REST API call
export function initDynamicSelects(): void {
for (const select of getElements<HTMLSelectElement>('select.api-select')) {
new DynamicTomSelect(select, {
...config,
valueField: VALUE_FIELD,
labelField: LABEL_FIELD,
maxOptions: MAX_OPTIONS,
// Disable local search (search is performed on the backend)
searchField: [],
// Reference the disabled-indicator attr on the <select> element to determine
// the name of the attribute which indicates whether an option should be disabled
disabledField: select.getAttribute('disabled-indicator') || undefined,
// Load options from API immediately on focus
preload: 'focus',
// Define custom rendering functions
render: {
option: renderOption,
},
// By default, load() will be called only if query.length > 0
shouldLoad: function (): boolean {
return true;
},
});
}
}

View File

@@ -1,9 +1,8 @@
import { initApiSelect } from './api';
import { initColorSelect } from './color';
import { initStaticSelect } from './static';
import { initColorSelects, initStaticSelects } from './static';
import { initDynamicSelects } from './dynamic';
export function initSelect(): void {
for (const func of [initApiSelect, initColorSelect, initStaticSelect]) {
func();
}
export function initSelects(): void {
initStaticSelects();
initDynamicSelects();
initColorSelects();
}

View File

@@ -1,27 +1,30 @@
import SlimSelect from 'slim-select';
import { TomOption } from 'tom-select/src/types';
import TomSelect from 'tom-select';
import { escape_html } from 'tom-select/src/utils';
import { config } from './config';
import { getElements } from '../util';
export function initStaticSelect(): void {
for (const select of getElements<HTMLSelectElement>('.netbox-static-select:not([data-ssid])')) {
if (select !== null) {
const label = document.querySelector(`label[for="${select.id}"]`) as HTMLLabelElement;
let placeholder;
if (label !== null) {
placeholder = `Select ${label.innerText.trim()}`;
}
const instance = new SlimSelect({
select,
allowDeselect: true,
deselectLabel: `<i class="mdi mdi-close-circle"></i>`,
placeholder,
});
// Don't copy classes from select element to SlimSelect instance.
for (const className of select.classList) {
instance.slim.container.classList.remove(className);
}
}
// Initialize <select> elements with statically-defined options
export function initStaticSelects(): void {
for (const select of getElements<HTMLSelectElement>(
'select:not(.api-select):not(.color-select)',
)) {
new TomSelect(select, {
...config,
});
}
}
// Initialize color selection fields
export function initColorSelects(): void {
for (const select of getElements<HTMLSelectElement>('select.color-select')) {
new TomSelect(select, {
...config,
render: {
option: function (item: TomOption, escape: typeof escape_html) {
return `<div style="background-color: #${escape(item.value)}">${escape(item.text)}</div>`;
},
},
});
}
}

View File

@@ -0,0 +1,66 @@
import type { Stringifiable } from 'query-string';
/**
* Map of string keys to primitive array values accepted by `query-string`. Keys are used as
* URL query parameter keys. Values correspond to query param values, enforced as an array
* for easier handling. For example, a mapping of `{ site_id: [1, 2] }` is serialized by
* `query-string` as `?site_id=1&site_id=2`. Likewise, `{ site_id: [1] }` is serialized as
* `?site_id=1`.
*/
export type QueryFilter = Map<string, Stringifiable[]>;
/**
* JSON data structure from `data-dynamic-params` attribute.
*/
export type DataDynamicParam = {
/**
* Name of form field to track.
*
* @example [name="tenant_group"]
*/
fieldName: string;
/**
* Query param key.
*
* @example group_id
*/
queryParam: string;
};
/**
* `queryParams` Map value.
*/
export type QueryParam = {
queryParam: string;
queryValue: Stringifiable[];
};
/**
* Map of string keys to primitive values. Used to track variables within URLs from the server. For
* example, `/api/$key/thing`. `PathFilter` tracks `$key` as `{ key: '' }` in the map, and when the
* value is later known, the value is set — `{ key: 'value' }`, and the URL is transformed to
* `/api/value/thing`.
*/
export type PathFilter = Map<string, Stringifiable>;
/**
* Strict Type Guard to determine if a deserialized value from the `data-dynamic-params` attribute
* is of type `DataDynamicParam[]`.
*
* @param value Deserialized value from `data-dynamic-params` attribute.
*/
export function isDataDynamicParams(value: unknown): value is DataDynamicParam[] {
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item === 'object' && item !== null) {
if ('fieldName' in item && 'queryParam' in item) {
return (
typeof (item as DataDynamicParam).fieldName === 'string' &&
typeof (item as DataDynamicParam).queryParam === 'string'
);
}
}
}
}
return false;
}

View File

@@ -1,26 +0,0 @@
import type { Trigger } from './api';
/**
* Determine if an element has the `data-url` attribute set.
*/
export function hasUrl(el: HTMLSelectElement): el is HTMLSelectElement & { 'data-url': string } {
const value = el.getAttribute('data-url');
return typeof value === 'string' && value !== '';
}
/**
* Determine if an element has the `data-query-param-exclude` attribute set.
*/
export function hasExclusions(
el: HTMLSelectElement,
): el is HTMLSelectElement & { 'data-query-param-exclude': string } {
const exclude = el.getAttribute('data-query-param-exclude');
return typeof exclude === 'string' && exclude !== '';
}
/**
* Determine if a trigger value is valid.
*/
export function isTrigger(value: unknown): value is Trigger {
return typeof value === 'string' && ['load', 'open', 'collapse'].includes(value);
}

View File

@@ -1,7 +1,8 @@
@import 'variables';
// Tabler
// Tabler & vendors
@import '../node_modules/@tabler/core/src/scss/_core.scss';
@import '../node_modules/@tabler/core/src/scss/vendor/tom-select';
// Overrides of external libraries
@import 'overrides/slim-select';

View File

@@ -3,7 +3,8 @@
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"noUnusedParameters": true,
// tom-select v2.3.1 raises several TS6133 errors with noUnusedParameters
"noUnusedParameters": false,
"esModuleInterop": true,
"isolatedModules": true,
"noUnusedLocals": true,

View File

@@ -2,6 +2,11 @@
# yarn lockfile v1
"@esbuild/linux-loong64@0.14.54":
version "0.14.54"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
"@eslint/eslintrc@^1.3.2":
version "1.3.2"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz"
@@ -67,7 +72,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -80,6 +85,18 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@orchidjs/sifter@^1.0.3":
version "1.0.3"
resolved "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz"
integrity sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==
dependencies:
"@orchidjs/unicode-variants" "^1.0.4"
"@orchidjs/unicode-variants@^1.0.4":
version "1.0.4"
resolved "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz"
integrity sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ==
"@pkgr/utils@^2.3.1":
version "2.3.1"
resolved "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz"
@@ -92,16 +109,11 @@
tiny-glob "^0.2.9"
tslib "^2.4.0"
"@popperjs/core@^2.11.8":
"@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
version "2.11.8"
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@popperjs/core@^2.9.2":
version "2.11.6"
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
"@tabler/core@1.0.0-beta20":
version "1.0.0-beta20"
resolved "https://registry.npmjs.org/@tabler/core/-/core-1.0.0-beta20.tgz"
@@ -138,6 +150,13 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/node@^20.11.16":
version "20.11.16"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.16.tgz#4411f79411514eb8e2926f036c86c9f0e4ec6708"
integrity sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==
dependencies:
undici-types "~5.26.4"
"@typescript-eslint/eslint-plugin@^5.39.0":
version "5.39.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz"
@@ -152,7 +171,7 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.39.0":
"@typescript-eslint/parser@^5.39.0":
version "5.39.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.39.0.tgz"
integrity sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==
@@ -223,7 +242,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.8.0:
acorn@^8.8.0:
version "8.8.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
@@ -577,6 +596,71 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
esbuild-android-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
esbuild-android-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44"
integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==
esbuild-android-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
esbuild-darwin-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72"
integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==
esbuild-darwin-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
esbuild-darwin-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a"
integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==
esbuild-darwin-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
esbuild-freebsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85"
integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==
esbuild-freebsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
esbuild-freebsd-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52"
integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==
esbuild-freebsd-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
esbuild-linux-32@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69"
integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==
esbuild-linux-32@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
esbuild-linux-64@0.13.15:
version "0.13.15"
resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz"
@@ -587,6 +671,76 @@ esbuild-linux-64@0.14.54:
resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz"
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
esbuild-linux-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1"
integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==
esbuild-linux-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
esbuild-linux-arm@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe"
integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==
esbuild-linux-arm@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
esbuild-linux-mips64le@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7"
integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==
esbuild-linux-mips64le@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
esbuild-linux-ppc64le@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2"
integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==
esbuild-linux-ppc64le@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
esbuild-linux-riscv64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
esbuild-linux-s390x@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
esbuild-netbsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038"
integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==
esbuild-netbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
esbuild-openbsd-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7"
integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==
esbuild-openbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
esbuild-sass-plugin@^2.3.3:
version "2.3.3"
resolved "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.3.3.tgz"
@@ -596,6 +750,46 @@ esbuild-sass-plugin@^2.3.3:
resolve "^1.22.1"
sass "^1.49.0"
esbuild-sunos-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4"
integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==
esbuild-sunos-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
esbuild-windows-32@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7"
integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==
esbuild-windows-32@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
esbuild-windows-64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294"
integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==
esbuild-windows-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
esbuild-windows-arm64@0.13.15:
version "0.13.15"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3"
integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==
esbuild-windows-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
esbuild@^0.13.15:
version "0.13.15"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz"
@@ -689,7 +883,7 @@ eslint-module-utils@^2.7.3:
dependencies:
debug "^3.2.7"
eslint-plugin-import@*, eslint-plugin-import@^2.26.0:
eslint-plugin-import@^2.26.0:
version "2.26.0"
resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz"
integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==
@@ -748,7 +942,7 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", eslint@^8.24.0, eslint@>=5, eslint@>=7.0.0, eslint@>=7.28.0:
eslint@^8.24.0:
version "8.24.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz"
integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==
@@ -909,7 +1103,7 @@ flat-cache@^3.0.4:
flatted "^3.1.0"
rimraf "^3.0.2"
flatpickr@^4.6.13, flatpickr@4.6.13:
flatpickr@4.6.13:
version "4.6.13"
resolved "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz"
integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==
@@ -924,6 +1118,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
@@ -1089,7 +1288,7 @@ graphql-language-service@^5.0.6:
nullthrows "^1.0.0"
vscode-languageserver-types "^3.15.1"
"graphql@^15.5.0 || ^16.0.0", "graphql@>= v14.5.0 <= 15.5.0", graphql@>=0.10.0:
"graphql@>= v14.5.0 <= 15.5.0":
version "15.5.0"
resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz"
integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==
@@ -1121,12 +1320,7 @@ has-property-descriptors@^1.0.0:
dependencies:
get-intrinsic "^1.1.1"
has-symbols@^1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has-symbols@^1.0.2:
has-symbols@^1.0.1, has-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
@@ -1268,14 +1462,7 @@ is-extglob@^2.1.1:
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
is-glob@^4.0.0:
version "4.0.1"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz"
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
dependencies:
is-extglob "^2.1.1"
is-glob@^4.0.1:
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz"
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
@@ -1289,13 +1476,6 @@ is-glob@^4.0.3:
dependencies:
is-extglob "^2.1.1"
is-glob@~4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz"
integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
dependencies:
is-extglob "^2.1.1"
is-negative-zero@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz"
@@ -1417,11 +1597,6 @@ json5@^1.0.1:
dependencies:
minimist "^1.2.0"
just-debounce-it@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-3.1.1.tgz"
integrity sha512-oPsuRyWp99LJaQ4KXC3A42tQNqkRTcPy0A8BCkRZ5cPCgsx81upB2KUrmHZvDUNhnCDKe7MshfTuWFQB9iXwDg==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
@@ -1521,11 +1696,6 @@ minimist@^1.2.6:
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
ms@^2.1.1:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@@ -1536,22 +1706,16 @@ ms@2.1.2:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@^2.1.1:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
"netbox-graphiql@file:/home/jstretch/projects/netbox/netbox/project-static/netbox-graphiql":
version "0.1.0"
resolved "file:netbox-graphiql"
dependencies:
graphiql "1.8.9"
graphql ">= v14.5.0 <= 15.5.0"
react "17.0.2"
react-dom "17.0.2"
subscriptions-transport-ws "0.9.18"
whatwg-fetch "3.6.2"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
@@ -1697,7 +1861,7 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^2.7.1, prettier@>=2.0.0:
prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
@@ -1722,7 +1886,7 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", react-dom@17.0.2:
react-dom@17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
@@ -1731,7 +1895,7 @@ queue-microtask@^1.2.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
"react@^16.8.0 || ^17.0.0 || ^18.0.0", react@17.0.2:
react@17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
@@ -1878,11 +2042,6 @@ slash@^4.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz"
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
slim-select@^1.27.1:
version "1.27.1"
resolved "https://registry.npmjs.org/slim-select/-/slim-select-1.27.1.tgz"
integrity sha512-LvJ02cKKk6/jSHIcQv7dZwkQSXHLCVQR3v3lo8RJUssUUcmKPkpBmTpQ8au8KSMkxwca9+yeg+dO0iHAaVr5Aw==
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.2"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
@@ -2004,6 +2163,14 @@ toggle-selection@^1.0.6:
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
tom-select@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/tom-select/-/tom-select-2.3.1.tgz"
integrity sha512-QS4vnOcB6StNGqX4sGboGXL2fkhBF2gIBB+8Hwv30FZXYPn0CyYO8kkdATRvwfCTThxiR4WcXwKJZ3cOmtI9eg==
dependencies:
"@orchidjs/sifter" "^1.0.3"
"@orchidjs/unicode-variants" "^1.0.4"
tsconfig-paths@^3.14.1:
version "3.14.1"
resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz"
@@ -2053,7 +2220,7 @@ typeface-roboto-mono@^1.1.13:
resolved "https://registry.npmjs.org/typeface-roboto-mono/-/typeface-roboto-mono-1.1.13.tgz"
integrity sha512-pnzDc70b7ywJHin/BUFL7HZX8DyOTBLT2qxlJ92eH1UJOFcENIBXa9IZrxsJX/gEKjbEDKhW5vz/TKRBNk/ufQ==
"typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@~4.8.4:
typescript@~4.8.4:
version "4.8.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
@@ -2073,6 +2240,11 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"

View File

@@ -1,4 +1,4 @@
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} class="{% if 'size' in widget.attrs %}form-select form-select-sm{% else %}netbox-static-select{% endif %}{% if 'class' in widget.attrs %} {{ widget.attrs.class }}{% endif %}">{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} class="form-select {% if 'class' in widget.attrs %} {{ widget.attrs.class }}{% endif %}">{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %}

View File

@@ -186,8 +186,7 @@ class UserForm(forms.ModelForm):
object_permissions = DynamicModelMultipleChoiceField(
required=False,
label=_('Permissions'),
queryset=ObjectPermission.objects.all(),
to_field_name='pk',
queryset=ObjectPermission.objects.all()
)
fieldsets = (
@@ -244,8 +243,7 @@ class GroupForm(forms.ModelForm):
object_permissions = DynamicModelMultipleChoiceField(
required=False,
label=_('Permissions'),
queryset=ObjectPermission.objects.all(),
to_field_name='pk',
queryset=ObjectPermission.objects.all()
)
fieldsets = (

View File

@@ -64,8 +64,6 @@ class DynamicModelChoiceMixin:
null_option: The string used to represent a null selection (if any)
disabled_indicator: The name of the field which, if populated, will disable selection of the
choice (optional)
fetch_trigger: The event type which will cause the select element to
fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional)
selector: Include an advanced object selection widget to assist the user in identifying the desired object
"""
filter = django_filters.ModelChoiceFilter
@@ -79,8 +77,6 @@ class DynamicModelChoiceMixin:
initial_params=None,
null_option=None,
disabled_indicator=None,
fetch_trigger=None,
empty_label=None,
selector=False,
**kwargs
):
@@ -89,24 +85,12 @@ class DynamicModelChoiceMixin:
self.initial_params = initial_params or {}
self.null_option = null_option
self.disabled_indicator = disabled_indicator
self.fetch_trigger = fetch_trigger
self.selector = selector
# to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
# by widget_attrs()
self.to_field_name = kwargs.get('to_field_name')
self.empty_option = empty_label or ""
super().__init__(queryset, **kwargs)
def widget_attrs(self, widget):
attrs = {
'data-empty-option': self.empty_option
}
# Set value-field attribute if the field specifies to_field_name
if self.to_field_name:
attrs['value-field'] = self.to_field_name
attrs = {}
# Set the string used to represent a null option
if self.null_option is not None:
@@ -116,10 +100,6 @@ class DynamicModelChoiceMixin:
if self.disabled_indicator is not None:
attrs['disabled-indicator'] = self.disabled_indicator
# Set the fetch trigger, if any.
if self.fetch_trigger is not None:
attrs['data-fetch-trigger'] = self.fetch_trigger
# Attach any static query parameters
if (len(self.query_params) > 0):
widget.add_query_params(self.query_params)

View File

@@ -24,7 +24,7 @@ class APISelect(forms.Select):
def __init__(self, api_url=None, full=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-api-select'
self.attrs['class'] = 'api-select'
self.dynamic_params: Dict[str, List[str]] = {}
self.static_params: Dict[str, List[str]] = {}
@@ -153,8 +153,4 @@ class APISelect(forms.Select):
class APISelectMultiple(APISelect, forms.SelectMultiple):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['data-multiple'] = 1
pass

View File

@@ -25,7 +25,6 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
('2', 'Yes'),
('3', 'No'),
)
self.attrs['class'] = 'netbox-static-select'
class ColorSelect(forms.Select):
@@ -37,7 +36,7 @@ class ColorSelect(forms.Select):
def __init__(self, *args, **kwargs):
kwargs['choices'] = add_blank_choice(ColorChoices)
super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-color-select'
self.attrs['class'] = 'color-select'
class HTMXSelect(forms.Select):

View File

@@ -423,8 +423,7 @@ class L2VPNTerminationForm(NetBoxModelForm):
queryset=L2VPN.objects.all(),
required=True,
query_params={},
label=_('L2VPN'),
fetch_trigger='open'
label=_('L2VPN')
)
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),