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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 761 additions and 1488 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(),

Binary file not shown.

Binary file not shown.

Binary file not shown.

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(),