mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-18 05:28:16 -06:00
Add dynamic parameter support
This commit is contained in:
parent
04c6083ff6
commit
edc9aaf53d
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
76
netbox/project-static/src/select/classes/dynamicParamsMap.ts
Normal file
76
netbox/project-static/src/select/classes/dynamicParamsMap.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { isTruthy } from '../../util';
|
||||
import { isDataDynamicParams } from '../../select_old/api/types';
|
||||
|
||||
import type { QueryParam } from '../../select_old/api/types';
|
||||
|
||||
/**
|
||||
* Extension of built-in `Map` to add convenience functions.
|
||||
*/
|
||||
export class DynamicParamsMap extends Map<string, QueryParam> {
|
||||
/**
|
||||
* Get the query parameter key based on field name.
|
||||
*
|
||||
* @param fieldName Related field name.
|
||||
* @returns `queryParam` key.
|
||||
*/
|
||||
public queryParam(fieldName: string): Nullable<QueryParam['queryParam']> {
|
||||
const value = this.get(fieldName);
|
||||
if (typeof value !== 'undefined') {
|
||||
return value.queryParam;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query parameter value based on field name.
|
||||
*
|
||||
* @param fieldName Related field name.
|
||||
* @returns `queryValue` value, or an empty array if there is no corresponding Map entry.
|
||||
*/
|
||||
public queryValue(fieldName: string): QueryParam['queryValue'] {
|
||||
const value = this.get(fieldName);
|
||||
if (typeof value !== 'undefined') {
|
||||
return value.queryValue;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the value of a field when the value changes.
|
||||
*
|
||||
* @param fieldName Related field name.
|
||||
* @param queryValue New value.
|
||||
* @returns `true` if the update was successful, `false` if there was no corresponding Map entry.
|
||||
*/
|
||||
public updateValue(fieldName: string, queryValue: QueryParam['queryValue']): boolean {
|
||||
const current = this.get(fieldName);
|
||||
if (isTruthy(current)) {
|
||||
const { queryParam } = current;
|
||||
this.set(fieldName, { queryParam, queryValue });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the underlying map based on the JSON passed in the `data-dynamic-params` attribute.
|
||||
*
|
||||
* @param json Raw JSON string from `data-dynamic-params` attribute.
|
||||
*/
|
||||
public addFromJson(json: string | null | undefined): void {
|
||||
if (isTruthy(json)) {
|
||||
const deserialized = JSON.parse(json);
|
||||
// Ensure the value is the data structure we expect.
|
||||
if (isDataDynamicParams(deserialized)) {
|
||||
for (const { queryParam, fieldName } of deserialized) {
|
||||
// Populate the underlying map with the initial data.
|
||||
this.set(fieldName, { queryParam, queryValue: [] });
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Data from 'data-dynamic-params' attribute is improperly formatted: '${json}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ 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 } from '../../select_old/api/types'
|
||||
@ -16,6 +17,7 @@ export class DynamicTomSelect extends TomSelect {
|
||||
*/
|
||||
private readonly queryParams: QueryFilter = new Map();
|
||||
private readonly staticParams: QueryFilter = new Map();
|
||||
private readonly dynamicParams: DynamicParamsMap = new DynamicParamsMap();
|
||||
|
||||
/**
|
||||
* Overrides
|
||||
@ -32,6 +34,15 @@ export class DynamicTomSelect extends TomSelect {
|
||||
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);
|
||||
}
|
||||
|
||||
// Add dependency event listeners.
|
||||
this.addEventListeners();
|
||||
}
|
||||
|
||||
load(value: string) {
|
||||
@ -40,9 +51,7 @@ export class DynamicTomSelect extends TomSelect {
|
||||
|
||||
// Automatically clear any cached options. (Only options included
|
||||
// in the API response should be present.)
|
||||
if (value) {
|
||||
self.clearOptions();
|
||||
}
|
||||
|
||||
addClasses(self.wrapper, self.settings.loadingClass);
|
||||
self.loading++;
|
||||
@ -114,4 +123,114 @@ export class DynamicTomSelect extends TomSelect {
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()]);
|
||||
const dependencies = this.dynamicParams.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);
|
||||
// this.updateQueryUrl();
|
||||
|
||||
// Load new data.
|
||||
this.load(this.lastValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user