Remove obsolete APISelect code

This commit is contained in:
Jeremy Stretch 2024-02-07 15:38:10 -05:00
parent d164833c6e
commit bed47a4f59
13 changed files with 69 additions and 1434 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

@ -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';

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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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<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}'`,
);
}
}
}
}

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

@ -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();
}
}

View File

@ -1,27 +0,0 @@
import SlimSelect from 'slim-select';
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);
}
}
}
}

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);
}