From bed47a4f59101676e2f7ca8bcb0d4d79129219cf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Feb 2024 15:38:10 -0500 Subject: [PATCH] Remove obsolete APISelect code --- netbox/project-static/dist/netbox.js | Bin 374389 -> 374389 bytes netbox/project-static/dist/netbox.js.map | Bin 339689 -> 339681 bytes .../src/select/classes/dynamicParamsMap.ts | 4 +- .../src/select/classes/dynamicTomSelect.ts | 2 +- netbox/project-static/src/select/types.ts | 66 ++ .../src/select_old/api/apiSelect.ts | 1002 ----------------- .../src/select_old/api/dynamicParams.ts | 76 -- .../src/select_old/api/index.ts | 10 - .../src/select_old/api/types.ts | 199 ---- netbox/project-static/src/select_old/color.ts | 82 -- netbox/project-static/src/select_old/index.ts | 9 - .../project-static/src/select_old/static.ts | 27 - netbox/project-static/src/select_old/util.ts | 26 - 13 files changed, 69 insertions(+), 1434 deletions(-) create mode 100644 netbox/project-static/src/select/types.ts delete mode 100644 netbox/project-static/src/select_old/api/apiSelect.ts delete mode 100644 netbox/project-static/src/select_old/api/dynamicParams.ts delete mode 100644 netbox/project-static/src/select_old/api/index.ts delete mode 100644 netbox/project-static/src/select_old/api/types.ts delete mode 100644 netbox/project-static/src/select_old/color.ts delete mode 100644 netbox/project-static/src/select_old/index.ts delete mode 100644 netbox/project-static/src/select_old/static.ts delete mode 100644 netbox/project-static/src/select_old/util.ts diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index d8a0e00098c899bf4aa8855789ac8cad325b4ff6..37ee33cc7c8f750035f6c5e0032abcc16c502723 100644 GIT binary patch delta 6999 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 Q>Styv^5@r2sy}1@3-~%Y*8l(j delta 6999 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 Sb$b+#Y&0T&ezQmUnf$+q2srNm diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 702a0af12e36e286fe698d4cc657d0761de97276..f65842504da818652727004918b070bfedf5794a 100644 GIT binary patch delta 58 zcmaEPSLESckqtEpZ2BdY1*yf8trb+7*D18GQ(y#QCLm_szD|MVHy=~F%k8nawo{?KKLFK+FWh%-d@eSXB9$ioK>U)@ND9Wa+it J+kj=-KLBnV6hZ(1 diff --git a/netbox/project-static/src/select/classes/dynamicParamsMap.ts b/netbox/project-static/src/select/classes/dynamicParamsMap.ts index c47535c9f..cadf37e55 100644 --- a/netbox/project-static/src/select/classes/dynamicParamsMap.ts +++ b/netbox/project-static/src/select/classes/dynamicParamsMap.ts @@ -1,7 +1,7 @@ import { isTruthy } from '../../util'; -import { isDataDynamicParams } from '../../select_old/api/types'; +import { isDataDynamicParams } from '../types'; -import type { QueryParam } from '../../select_old/api/types'; +import type { QueryParam } from '../types'; /** * Extension of built-in `Map` to add convenience functions. diff --git a/netbox/project-static/src/select/classes/dynamicTomSelect.ts b/netbox/project-static/src/select/classes/dynamicTomSelect.ts index 13b483dfe..b650b4a86 100644 --- a/netbox/project-static/src/select/classes/dynamicTomSelect.ts +++ b/netbox/project-static/src/select/classes/dynamicTomSelect.ts @@ -6,7 +6,7 @@ import type { Stringifiable } from 'query-string'; import { DynamicParamsMap } from './dynamicParamsMap'; // Transitional -import { QueryFilter, PathFilter } from '../../select_old/api/types' +import { QueryFilter, PathFilter } from '../types' import { getElement, replaceAll } from '../../util'; diff --git a/netbox/project-static/src/select/types.ts b/netbox/project-static/src/select/types.ts new file mode 100644 index 000000000..3ebd65dab --- /dev/null +++ b/netbox/project-static/src/select/types.ts @@ -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; + +/** + * 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; + +/** + * 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; +} diff --git a/netbox/project-static/src/select_old/api/apiSelect.ts b/netbox/project-static/src/select_old/api/apiSelect.ts deleted file mode 100644 index 279340c12..000000000 --- a/netbox/project-static/src/select_old/api/apiSelect.ts +++ /dev/null @@ -1,1002 +0,0 @@ -import { readableColor } from 'color2k'; -import debounce from 'just-debounce-it'; -import { encode } from 'html-entities'; -import queryString from 'query-string'; -import SlimSelect from 'slim-select'; -import { createToast } from '../../bs'; -import { hasUrl, hasExclusions, isTrigger } from '../util'; -import { DynamicParamsMap } from './dynamicParams'; -import { isStaticParams, isOption } from './types'; -import { - hasMore, - hasError, - isTruthy, - getApiData, - getElement, - isApiError, - replaceAll, - createElement, - uniqueByProperty, - findFirstAdjacent, -} from '../../util'; - -import type { Stringifiable } from 'query-string'; -import type { Option } from 'slim-select/dist/data'; -import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types'; - -// Empty placeholder option. -const EMPTY_PLACEHOLDER = { - value: '', - text: '', - placeholder: true, -} as Option; - -// Attributes which if truthy should render the option disabled. -const DISABLED_ATTRIBUTES = ['occupied'] as string[]; - -/** - * Manage a single API-backed select element's state. Each API select element is likely controlled - * or dynamically updated by one or more other API select (or static select) elements' values. - */ -export class APISelect { - /** - * Base `` element. - */ - private readonly allowRefresh: boolean = true; - - /** - * Event to be dispatched when dependent fields' values change. - */ - private readonly loadEvent: InstanceType; - - /** - * Event to be dispatched when the scroll position of this element's optinos list is at the - * bottom. - */ - private readonly bottomEvent: InstanceType; - - /** - * SlimSelect instance for this element. - */ - private readonly slim: InstanceType; - - /** - * Post-parsed URL query parameters for API queries. - */ - private readonly queryParams: QueryFilter = new Map(); - - /** - * API query parameters that should be applied to API queries for this field. This will be - * updated as other dependent fields' values change. This is a mapping of: - * - * Form Field Names → Object containing: - * - Query parameter key name - * - Query value - * - * This is different from `queryParams` in that it tracks all _possible_ related fields and their - * values, even if they are empty. Further, the keys in `queryParams` correspond to the actual - * query parameter keys, which are not necessarily the same as the form field names, depending on - * the model. For example, `tenant_group` would be the field name, but `group_id` would be the - * query parameter. - */ - private readonly dynamicParams: DynamicParamsMap = new DynamicParamsMap(); - - /** - * API query parameters that are already known by the server and should not change. - */ - private readonly staticParams: QueryFilter = new Map(); - - /** - * Mapping of URL template key/value pairs. If this element's URL contains Django template tags - * (e.g., `{{key}}`), `key` will be added to `pathValue` and the `id_key` form element will be - * tracked for changes. When the `id_key` element's value changes, the new value will be added - * to this map. For example, if the template key is `rack`, and the `id_rack` field's value is - * `1`, `pathValues` would be updated to reflect a `"rack" => 1` mapping. When the query URL is - * updated, the URL would change from `/dcim/racks/{{rack}}/` to `/dcim/racks/1/`. - */ - private readonly pathValues: PathFilter = new Map(); - - /** - * Original API query URL passed via the `data-href` attribute from the server. This is kept so - * that the URL can be reconstructed as form values change. - */ - private readonly url: string = ''; - - /** - * API query URL. This will be updated dynamically to include any query parameters in `queryParameters`. - */ - private queryUrl: string = ''; - - /** - * Scroll position of options is at the bottom of the list, or not. Used to determine if - * additional options should be fetched from the API. - */ - private atBottom: boolean = false; - - /** - * API URL for additional options, if applicable. `null` indicates no options remain. - */ - private more: Nullable = null; - - /** - * Array of options values which should be considered disabled or static. - */ - private disabledOptions: Array = []; - - /** - * Array of properties which if truthy on an API object should be considered disabled. - */ - private disabledAttributes: Array = DISABLED_ATTRIBUTES; - - constructor(base: HTMLSelectElement) { - // Initialize readonly properties. - this.base = base; - this.name = base.name; - - if (hasUrl(base)) { - const url = base.getAttribute('data-url') as string; - this.url = url; - this.queryUrl = url; - } - - this.loadEvent = new Event(`netbox.select.onload.${base.name}`); - this.bottomEvent = new Event(`netbox.select.atbottom.${base.name}`); - - this.placeholder = this.getPlaceholder(); - this.disabledOptions = this.getDisabledOptions(); - this.disabledAttributes = this.getDisabledAttributes(); - - const emptyOption = base.getAttribute('data-empty-option'); - if (isTruthy(emptyOption)) { - this.emptyOption = { - text: emptyOption, - value: '', - }; - } else { - this.emptyOption = EMPTY_PLACEHOLDER; - } - - const nullOption = base.getAttribute('data-null-option'); - if (isTruthy(nullOption)) { - this.nullOption = { - text: nullOption, - value: 'null', - }; - } - - this.slim = new SlimSelect({ - select: this.base, - allowDeselect: true, - deselectLabel: ``, - placeholder: this.placeholder, - searchPlaceholder: 'Filter', - onChange: () => this.handleSlimChange(), - }); - - // Don't close on select if multiple select - if (this.base.multiple) { - this.slim.config.closeOnSelect = false; - } - - // Initialize API query properties. - this.getStaticParams(); - this.getDynamicParams(); - this.getPathKeys(); - - // Populate static query parameters. - for (const [key, value] of this.staticParams.entries()) { - this.queryParams.set(key, value); - } - - // Populate dynamic query parameters with any form values that are already known. - for (const filter of this.dynamicParams.keys()) { - this.updateQueryParams(filter); - } - - // Populate dynamic path values with any form values that are already known. - for (const filter of this.pathValues.keys()) { - this.updatePathValues(filter); - } - - this.queryParams.set('brief', [true]); - this.updateQueryUrl(); - - // Initialize element styling. - this.resetClasses(); - this.setSlimStyles(); - - // Initialize controlling elements. - this.initResetButton(); - - // Add the refresh button to the search element. - this.initRefreshButton(); - - // Add dependency event listeners. - this.addEventListeners(); - - // Determine if the fetch trigger has been set. - const triggerAttr = this.base.getAttribute('data-fetch-trigger'); - - // Determine if this element is part of collapsible element. - const collapse = this.base.closest('.content-container .collapse'); - - if (isTrigger(triggerAttr)) { - this.trigger = triggerAttr; - } else if (collapse !== null) { - this.trigger = 'collapse'; - } else { - this.trigger = 'open'; - } - - switch (this.trigger) { - case 'collapse': - if (collapse !== null) { - // If the element is collapsible but already shown, load the data immediately. - if (collapse.classList.contains('show')) { - Promise.all([this.loadData()]); - } - - // If this element is part of a collapsible element, only load the data when the - // collapsible element is shown. - // See: https://getbootstrap.com/docs/5.0/components/collapse/#events - collapse.addEventListener('show.bs.collapse', () => this.loadData()); - collapse.addEventListener('hide.bs.collapse', () => this.resetOptions()); - } - break; - case 'open': - // If the trigger is 'open', only load API data when the select element is opened. - this.slim.beforeOpen = () => this.loadData(); - break; - case 'load': - // Otherwise, load the data immediately. - Promise.all([this.loadData()]); - break; - } - } - - /** - * This instance's available options. - */ - private get options(): Option[] { - return this.slim.data.data.filter(isOption); - } - - /** - * Apply new options to both the SlimSelect instance and this manager's state. - */ - private set options(optionsIn: Option[]) { - let newOptions = optionsIn; - // Ensure null option is present, if it exists. - if (this.nullOption !== null) { - newOptions = [this.nullOption, ...newOptions]; - } - // Deduplicate options each time they're set. - const deduplicated = uniqueByProperty(newOptions, 'value'); - // Determine if the new options have a placeholder. - const hasPlaceholder = typeof deduplicated.find(o => o.value === '') !== 'undefined'; - // Get the placeholder index (note: if there is no placeholder, the index will be `-1`). - const placeholderIdx = deduplicated.findIndex(o => o.value === ''); - - if (hasPlaceholder && placeholderIdx >= 0) { - // If there is an existing placeholder, replace it. - deduplicated[placeholderIdx] = this.emptyOption; - } else { - // If there is not a placeholder, add one to the front. - deduplicated.unshift(this.emptyOption); - } - this.slim.setData(deduplicated); - } - - /** - * Remove all options and reset back to the generic placeholder. - */ - private resetOptions(): void { - this.options = [this.emptyOption]; - } - - /** - * Add or remove a class to the SlimSelect element to match Bootstrap .form-select:disabled styles. - */ - public disable(): void { - if (this.slim.slim.singleSelected !== null) { - if (!this.slim.slim.singleSelected.container.hasAttribute('disabled')) { - this.slim.slim.singleSelected.container.setAttribute('disabled', ''); - } - } else if (this.slim.slim.multiSelected !== null) { - if (!this.slim.slim.multiSelected.container.hasAttribute('disabled')) { - this.slim.slim.multiSelected.container.setAttribute('disabled', ''); - } - } - this.slim.disable(); - } - - /** - * Add or remove a class to the SlimSelect element to match Bootstrap .form-select:disabled styles. - */ - public enable(): void { - if (this.slim.slim.singleSelected !== null) { - if (this.slim.slim.singleSelected.container.hasAttribute('disabled')) { - this.slim.slim.singleSelected.container.removeAttribute('disabled'); - } - } else if (this.slim.slim.multiSelected !== null) { - if (this.slim.slim.multiSelected.container.hasAttribute('disabled')) { - this.slim.slim.multiSelected.container.removeAttribute('disabled'); - } - } - this.slim.enable(); - } - - /** - * 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 debounced function to fetch options based on the search input value. - const fetcher = debounce((event: Event) => this.handleSearch(event), 300, false); - - // Query the API when the input value changes or a value is pasted. - this.slim.slim.search.input.addEventListener('keyup', event => { - // Only search when necessary keys are pressed. - if (!event.key.match(/^(Arrow|Enter|Tab).*/)) { - return fetcher(event); - } - }); - this.slim.slim.search.input.addEventListener('paste', event => fetcher(event)); - - // Watch every scroll event to determine if the scroll position is at bottom. - this.slim.slim.list.addEventListener('scroll', () => this.handleScroll()); - - // When the scroll position is at bottom, fetch additional options. - this.base.addEventListener(`netbox.select.atbottom.${this.name}`, () => - this.fetchOptions(this.more, 'merge'), - ); - - // When the base select element is disabled or enabled, properly disable/enable this instance. - this.base.addEventListener(`netbox.select.disabled.${this.name}`, event => - this.handleDisableEnable(event), - ); - - // 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.filterParams.keys(), ...this.pathValues.keys()]); - 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.base.addEventListener(`netbox.select.onload.${dep}`, event => this.handleEvent(event)); - } - } - - /** - * Load this element's options from the NetBox API. - */ - private async loadData(): Promise { - try { - this.disable(); - await this.getOptions('replace'); - } catch (err) { - console.error(err); - } finally { - this.setOptionStyles(); - this.enable(); - this.base.dispatchEvent(this.loadEvent); - } - } - - /** - * Get all options from the native select element that are already selected and do not contain - * placeholder values. - */ - private getPreselectedOptions(): HTMLOptionElement[] { - return Array.from(this.base.options) - .filter(option => option.selected) - .filter(option => { - if (option.value === '---------' || option.innerText === '---------') return false; - return true; - }); - } - - /** - * Process a valid API response and add results to this instance's options. - * - * @param data Valid API response (not an error). - */ - private async processOptions( - data: APIAnswer, - action: ApplyMethod = 'merge', - ): Promise { - // Get all already-selected options. - const preSelected = this.getPreselectedOptions(); - - // Get the values of all already-selected options. - const selectedValues = preSelected.map(option => option.getAttribute('value')).filter(isTruthy); - - // Build SlimSelect options from all already-selected options. - const preSelectedOptions = preSelected.map(option => ({ - value: option.value, - text: encode(option.innerText), - selected: true, - disabled: false, - })) as Option[]; - - let options = [] as Option[]; - - for (const result of data.results) { - let text = encode(result.display); - - if (typeof result._depth === 'number' && result._depth > 0) { - // If the object has a `_depth` property, indent its display text. - text = `${'─'.repeat(result._depth)} ${text}`; - } - const data = {} as Record; - const value = result.id.toString(); - let style, selected, disabled; - - // Set any primitive k/v pairs as data attributes on each option. - for (const [k, v] of Object.entries(result)) { - if (!['id', 'slug'].includes(k) && ['string', 'number', 'boolean'].includes(typeof v)) { - const key = replaceAll(k, '_', '-'); - data[key] = String(v); - } - // Set option to disabled if the result contains a matching key and is truthy. - if (this.disabledAttributes.some(key => key.toLowerCase() === k.toLowerCase())) { - if (typeof v === 'string' && v.toLowerCase() !== 'false') { - disabled = true; - } else if (typeof v === 'boolean' && v === true) { - disabled = true; - } else if (typeof v === 'number' && v > 0) { - disabled = true; - } - } - } - - // Set option to disabled if it is contained within the disabled array. - if (selectedValues.some(option => this.disabledOptions.includes(option))) { - disabled = true; - } - - // Set pre-selected options. - if (selectedValues.includes(value)) { - selected = true; - // If an option is selected, it can't be disabled. Otherwise, it won't be submitted with - // the rest of the form, resulting in that field's value being deleting from the object. - disabled = false; - } - - const option = { - value, - text, - data, - style, - selected, - disabled, - } as Option; - options = [...options, option]; - } - - switch (action) { - case 'merge': - this.options = [...this.options, ...options]; - break; - case 'replace': - this.options = [...preSelectedOptions, ...options]; - break; - } - - if (hasMore(data)) { - // If the `next` property in the API response is a URL, there are more options on the server - // side to be fetched. - this.more = data.next; - } else { - // If the `next` property in the API response is `null`, there are no more options on the - // server, and no additional fetching needs to occur. - this.more = null; - } - } - - /** - * Fetch options from the given API URL and add them to the instance. - * - * @param url API URL - */ - private async fetchOptions(url: Nullable, action: ApplyMethod = 'merge'): Promise { - if (typeof url === 'string') { - const data = await getApiData(url); - - if (hasError(data)) { - if (isApiError(data)) { - return this.handleError(data.exception, data.error); - } - return this.handleError(`Error Fetching Options for field '${this.name}'`, data.error); - } - await this.processOptions(data, action); - } - } - - /** - * Query the NetBox API for this element's options. - */ - private async getOptions(action: ApplyMethod = 'merge'): Promise { - if (this.queryUrl.includes(`{{`)) { - this.resetOptions(); - return; - } - await this.fetchOptions(this.queryUrl, action); - } - - /** - * Query the API for a specific search pattern and add the results to the available options. - */ - private async handleSearch(event: Event) { - const { value: q } = event.target as HTMLInputElement; - const url = queryString.stringifyUrl({ url: this.queryUrl, query: { q } }); - if (!url.includes(`{{`)) { - await this.fetchOptions(url, 'merge'); - this.slim.data.search(q); - this.slim.render(); - } - return; - } - - /** - * Determine if the user has scrolled to the bottom of the options list. If so, try to load - * additional paginated options. - */ - private handleScroll(): void { - // Floor scrollTop as chrome can return fractions on some zoom levels. - const atBottom = - Math.floor(this.slim.slim.list.scrollTop) + this.slim.slim.list.offsetHeight === - this.slim.slim.list.scrollHeight; - - if (this.atBottom && !atBottom) { - this.atBottom = false; - this.base.dispatchEvent(this.bottomEvent); - } else if (!this.atBottom && atBottom) { - this.atBottom = true; - this.base.dispatchEvent(this.bottomEvent); - } - } - - /** - * 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. - Promise.all([this.loadData()]); - } - - /** - * Event handler to be dispatched when the base select element is disabled or enabled. When that - * occurs, run the instance's `disable()` or `enable()` methods to synchronize UI state with - * desired action. - * - * @param event Dispatched event matching pattern `netbox.select.disabled.` - */ - private handleDisableEnable(event: Event): void { - const target = event.target as HTMLSelectElement; - - if (target.disabled === true) { - this.disable(); - } else if (target.disabled === false) { - this.enable(); - } - } - - /** - * When the API returns an error, show it to the user and reset this element's available options. - * - * @param title Error title - * @param message Error message - */ - private handleError(title: string, message: string): void { - createToast('danger', title, message).show(); - this.resetOptions(); - } - - /** - * `change` event callback to be called any time the value of a SlimSelect instance is changed. - */ - private handleSlimChange(): void { - const element = this.slim.slim; - if (element) { - // Toggle form validation classes when form values change. For example, if the field was - // invalid and the value has now changed, remove the `.is-invalid` class. - if ( - element.container.classList.contains('is-invalid') || - this.base.classList.contains('is-invalid') - ) { - element.container.classList.remove('is-invalid'); - this.base.classList.remove('is-invalid'); - } - } - this.base.dispatchEvent(this.loadEvent); - } - - /** - * Update the API query URL and underlying DOM element's `data-url` attribute. - */ - private updateQueryUrl(): void { - // Create new URL query parameters based on the current state of `queryParams` and create an - // updated API query URL. - const query = {} as Dict; - for (const [key, value] of this.queryParams.entries()) { - query[key] = value; - } - - let url = this.url; - - // Replace any Django template variables in the URL with values from `pathValues` if set. - for (const [key, value] of this.pathValues.entries()) { - for (const result of this.url.matchAll(new RegExp(`({{${key}}})`, 'g'))) { - if (isTruthy(value)) { - url = replaceAll(url, result[1], value.toString()); - } - } - } - const newUrl = queryString.stringifyUrl({ url, query }); - if (this.queryUrl !== newUrl) { - // Only update the URL if it has changed. - this.queryUrl = newUrl; - this.base.setAttribute('data-url', newUrl); - } - } - - /** - * Update an element's API URL based on the value of another element on which this element - * relies. - * - * @param fieldName DOM ID of the other element. - */ - private updateQueryParams(fieldName: string): void { - // Find the element dependency. - const element = document.querySelector(`[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. - * - * @param id DOM ID of the other element. - */ - private updatePathValues(id: string): void { - const key = replaceAll(id, /^id_/i, ''); - const element = getElement(`id_${key}`); - if (element !== null) { - // If this element's URL contains Django template tags ({{), replace the template tag - // with the 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.url.includes(`{{`) && Boolean(this.url.match(new RegExp(`({{(${id})}})`, 'g'))); - - if (hasReplacement) { - if (isTruthy(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, ''); - } - } - } - } - - /** - * Find the select element's placeholder text/label. - */ - private getPlaceholder(): string { - let placeholder = this.name; - if (this.base.id) { - const label = document.querySelector(`label[for="${this.base.id}"]`) as HTMLLabelElement; - // Set the placeholder text to the label value, if it exists. - if (label !== null) { - placeholder = `Select ${label.innerText.trim()}`; - } - } - return placeholder; - } - - /** - * Get this element's disabled options by value. The `data-query-param-exclude` attribute will - * contain a stringified JSON array of option values. - */ - private getDisabledOptions(): string[] { - let disabledOptions = [] as string[]; - if (hasExclusions(this.base)) { - try { - const exclusions = JSON.parse( - this.base.getAttribute('data-query-param-exclude') ?? '[]', - ) as string[]; - disabledOptions = [...disabledOptions, ...exclusions]; - } catch (err) { - console.group( - `Unable to parse data-query-param-exclude value on select element '${this.name}'`, - ); - console.warn(err); - console.groupEnd(); - } - } - return disabledOptions; - } - - /** - * Get this element's disabled attribute keys. For example, if `disabled-indicator` is set to - * `'_occupied'` and an API object contains `{ _occupied: true }`, the option will be disabled. - */ - private getDisabledAttributes(): string[] { - let disabled = [...DISABLED_ATTRIBUTES] as string[]; - const attr = this.base.getAttribute('disabled-indicator'); - if (isTruthy(attr)) { - disabled = [...disabled, attr]; - } - return disabled; - } - - /** - * Parse the `data-url` attribute to add any Django template 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.url.matchAll(new RegExp(`{{(.+)}}`, 'g'))) { - this.pathValues.set(result[1], ''); - } - } - - /** - * Determine if a 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.base.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(); - } - } - - /** - * 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.base.getAttribute('data-static-params'); - - try { - if (isTruthy(serialized)) { - const deserialized = JSON.parse(serialized); - if (isStaticParams(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(); - } - } - - /** - * Set the underlying select element to the same size as the SlimSelect instance. This is - * primarily for built-in HTML form validation (which doesn't really work) but it also makes - * things feel cleaner in the DOM. - */ - private setSlimStyles(): void { - const { width, height } = this.slim.slim.container.getBoundingClientRect(); - this.base.style.opacity = '0'; - this.base.style.width = `${width}px`; - this.base.style.height = `${height}px`; - this.base.style.display = 'block'; - this.base.style.position = 'absolute'; - this.base.style.pointerEvents = 'none'; - } - - /** - * Add scoped style elements specific to each SlimSelect option, if the color property exists. - * As of this writing, this attribute only exist on Tags. The color property is used as the - * background color, and a foreground color is detected based on the luminosity of the background - * color. - */ - private setOptionStyles(): void { - for (const option of this.options) { - // Only create style elements for options that contain a color attribute. - if ( - 'data' in option && - 'id' in option && - typeof option.data !== 'undefined' && - typeof option.id !== 'undefined' && - 'color' in option.data - ) { - const id = option.id as string; - const data = option.data as { color: string }; - - // Create the style element. - const style = document.createElement('style'); - - // Append hash to color to make it a valid hex color. - const bg = `#${data.color}`; - // Detect the foreground color. - const fg = readableColor(bg); - - // Add a unique identifier to the style element. - style.setAttribute('data-netbox', id); - - // Scope the CSS to apply both the list item and the selected item. - style.innerHTML = replaceAll( - ` - div.ss-values div.ss-value[data-id="${id}"], - div.ss-list div.ss-option:not(.ss-disabled)[data-id="${id}"] - { - background-color: ${bg} !important; - color: ${fg} !important; - } - `, - '\n', - '', - ).trim(); - - // Add the style element to the DOM. - document.head.appendChild(style); - } - } - } - - /** - * Remove base element classes from SlimSelect instance. - */ - private resetClasses(): void { - const element = this.slim.slim; - if (element) { - for (const className of this.base.classList) { - element.container.classList.remove(className); - } - } - } - - /** - * Initialize any adjacent reset buttons so that when clicked, the page is reloaded without - * query parameters. - */ - private initResetButton(): void { - const resetButton = findFirstAdjacent( - this.base, - 'button[data-reset-select]', - ); - if (resetButton !== null) { - resetButton.addEventListener('click', () => { - window.location.assign(window.location.origin + window.location.pathname); - }); - } - } - - /** - * Add a refresh button to the search container element. When clicked, the API data will be - * reloaded. - */ - private initRefreshButton(): void { - if (this.allowRefresh) { - const refreshButton = createElement( - 'button', - { type: 'button' }, - ['btn', 'btn-sm', 'btn-ghost-dark'], - [createElement('i', null, ['mdi', 'mdi-reload'])], - ); - refreshButton.addEventListener('click', () => this.loadData()); - refreshButton.type = 'button'; - this.slim.slim.search.container.appendChild(refreshButton); - } - } -} diff --git a/netbox/project-static/src/select_old/api/dynamicParams.ts b/netbox/project-static/src/select_old/api/dynamicParams.ts deleted file mode 100644 index c31c1962b..000000000 --- a/netbox/project-static/src/select_old/api/dynamicParams.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { isTruthy } from '../../util'; -import { isDataDynamicParams } from './types'; - -import type { QueryParam } from './types'; - -/** - * Extension of built-in `Map` to add convenience functions. - */ -export class DynamicParamsMap extends Map { - /** - * Get the query parameter key based on field name. - * - * @param fieldName Related field name. - * @returns `queryParam` key. - */ - public queryParam(fieldName: string): Nullable { - 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}'`, - ); - } - } - } -} diff --git a/netbox/project-static/src/select_old/api/index.ts b/netbox/project-static/src/select_old/api/index.ts deleted file mode 100644 index 3fef1ad6a..000000000 --- a/netbox/project-static/src/select_old/api/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getElements } from '../../util'; -import { APISelect } from './apiSelect'; - -export function initApiSelect(): void { - for (const select of getElements('.netbox-api-select:not([data-ssid])')) { - new APISelect(select); - } -} - -export type { Trigger } from './types'; diff --git a/netbox/project-static/src/select_old/api/types.ts b/netbox/project-static/src/select_old/api/types.ts deleted file mode 100644 index 8179f4a3a..000000000 --- a/netbox/project-static/src/select_old/api/types.ts +++ /dev/null @@ -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; - -/** - * 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=""]`) - * - * @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; - /** - * Include `null` on queries for the related field. For example, if `true`, `?=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; - -/** - * 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); -} diff --git a/netbox/project-static/src/select_old/color.ts b/netbox/project-static/src/select_old/color.ts deleted file mode 100644 index 4c8d6454a..000000000 --- a/netbox/project-static/src/select_old/color.ts +++ /dev/null @@ -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, - 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( - '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: ``, - }); - - // 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); - } -} diff --git a/netbox/project-static/src/select_old/index.ts b/netbox/project-static/src/select_old/index.ts deleted file mode 100644 index 356c8004f..000000000 --- a/netbox/project-static/src/select_old/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { initApiSelect } from './api'; -import { initColorSelect } from './color'; -import { initStaticSelect } from './static'; - -export function initSelect(): void { - for (const func of [initApiSelect, initColorSelect, initStaticSelect]) { - func(); - } -} diff --git a/netbox/project-static/src/select_old/static.ts b/netbox/project-static/src/select_old/static.ts deleted file mode 100644 index 19031bb7d..000000000 --- a/netbox/project-static/src/select_old/static.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SlimSelect from 'slim-select'; -import { getElements } from '../util'; - -export function initStaticSelect(): void { - for (const select of getElements('.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: ``, - placeholder, - }); - - // Don't copy classes from select element to SlimSelect instance. - for (const className of select.classList) { - instance.slim.container.classList.remove(className); - } - } - } -} diff --git a/netbox/project-static/src/select_old/util.ts b/netbox/project-static/src/select_old/util.ts deleted file mode 100644 index daf7839dc..000000000 --- a/netbox/project-static/src/select_old/util.ts +++ /dev/null @@ -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); -}