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

#7123: Handle empty_option on API Select

This commit is contained in:
thatmattlove
2021-09-01 17:02:43 -07:00
parent 774dff07ee
commit ddff193786
4 changed files with 34 additions and 25 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -22,13 +22,6 @@ import type { Stringifiable } from 'query-string';
import type { Option } from 'slim-select/dist/data'; import type { Option } from 'slim-select/dist/data';
import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types'; import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types';
// Empty placeholder option.
const PLACEHOLDER = {
value: '',
text: '',
placeholder: true,
} as Option;
// Attributes which if truthy should render the option disabled. // Attributes which if truthy should render the option disabled.
const DISABLED_ATTRIBUTES = ['occupied'] as string[]; const DISABLED_ATTRIBUTES = ['occupied'] as string[];
@ -52,6 +45,8 @@ export class APISelect {
*/ */
public readonly placeholder: string; public readonly placeholder: string;
public readonly emptyOption: Nullable<Option> = null;
/** /**
* Event that will initiate the API call to NetBox to load option data. By default, the trigger * Event that will initiate the API call to NetBox to load option data. By default, the trigger
* is `'load'`, so data will be fetched when the element renders on the page. * is `'load'`, so data will be fetched when the element renders on the page.
@ -147,7 +142,7 @@ export class APISelect {
/** /**
* This instance's available options. * This instance's available options.
*/ */
private _options: Option[] = [PLACEHOLDER]; private _options: Option[] = [];
/** /**
* Array of options values which should be considered disabled or static. * Array of options values which should be considered disabled or static.
@ -168,6 +163,14 @@ export class APISelect {
this.preSorted = true; this.preSorted = true;
} }
const emptyOption = base.getAttribute('data-empty-option');
if (isTruthy(emptyOption)) {
this.emptyOption = {
text: emptyOption,
value: '',
};
}
if (hasUrl(base)) { if (hasUrl(base)) {
const url = base.getAttribute('data-url') as string; const url = base.getAttribute('data-url') as string;
this.url = url; this.url = url;
@ -285,17 +288,16 @@ export class APISelect {
// Get the placeholder index (note: if there is no placeholder, the index will be `-1`). // Get the placeholder index (note: if there is no placeholder, the index will be `-1`).
const placeholderIdx = deduplicated.findIndex(o => o.value === ''); const placeholderIdx = deduplicated.findIndex(o => o.value === '');
if (hasPlaceholder && placeholderIdx < 0) { if (hasPlaceholder && placeholderIdx < 0 && this.emptyOption !== null) {
// If there is a placeholder but it is not the first element (due to sorting or other merge // If there is a placeholder but it is not the first element (due to sorting or other merge
// issues), remove it from the options array and place it in front. // issues), remove it from the options array and place it in front.
deduplicated.splice(placeholderIdx); deduplicated.splice(placeholderIdx);
deduplicated = [PLACEHOLDER, ...deduplicated]; deduplicated = [this.emptyOption, ...deduplicated];
} }
if (!hasPlaceholder) { if (!hasPlaceholder && this.emptyOption !== null) {
// If there is no placeholder, add one to the front of the array. // If there is no placeholder, add one to the front of the array.
deduplicated = [PLACEHOLDER, ...deduplicated]; deduplicated = [this.emptyOption, ...deduplicated];
} }
this._options = deduplicated; this._options = deduplicated;
this.slim.setData(deduplicated); this.slim.setData(deduplicated);
} }
@ -304,7 +306,11 @@ export class APISelect {
* Remove all options and reset back to the generic placeholder. * Remove all options and reset back to the generic placeholder.
*/ */
private resetOptions(): void { private resetOptions(): void {
this.options = [PLACEHOLDER]; if (this.emptyOption !== null) {
this.options = [this.emptyOption];
} else {
this.options = [];
}
} }
/** /**
@ -534,7 +540,7 @@ export class APISelect {
*/ */
private async getOptions(action: ApplyMethod = 'merge'): Promise<void> { private async getOptions(action: ApplyMethod = 'merge'): Promise<void> {
if (this.queryUrl.includes(`{{`)) { if (this.queryUrl.includes(`{{`)) {
this.options = [PLACEHOLDER]; this.resetOptions();
return; return;
} }
await this.fetchOptions(this.queryUrl, action); await this.fetchOptions(this.queryUrl, action);

View File

@ -376,7 +376,7 @@ class DynamicModelChoiceMixin:
widget = widgets.APISelect widget = widgets.APISelect
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None, def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None,
*args, **kwargs): empty_label=None, *args, **kwargs):
self.query_params = query_params or {} self.query_params = query_params or {}
self.initial_params = initial_params or {} self.initial_params = initial_params or {}
self.null_option = null_option self.null_option = null_option
@ -386,11 +386,14 @@ class DynamicModelChoiceMixin:
# to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
# by widget_attrs() # by widget_attrs()
self.to_field_name = kwargs.get('to_field_name') self.to_field_name = kwargs.get('to_field_name')
self.empty_option = empty_label or ""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def widget_attrs(self, widget): def widget_attrs(self, widget):
attrs = {} attrs = {
'data-empty-option': self.empty_option
}
# Set value-field attribute if the field specifies to_field_name # Set value-field attribute if the field specifies to_field_name
if self.to_field_name: if self.to_field_name: