mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
clean up typescript initialization
This commit is contained in:
parent
0aa8fc2fc2
commit
99f0e31810
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js
vendored
BIN
netbox/project-static/dist/jobs.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js.map
vendored
BIN
netbox/project-static/dist/jobs.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
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.
@ -1,7 +1,33 @@
|
||||
import { Toast } from 'bootstrap';
|
||||
import { Modal, Tab, Toast, Tooltip } from 'bootstrap';
|
||||
import Masonry from 'masonry-layout';
|
||||
import { getElements } from './util';
|
||||
|
||||
type ToastLevel = 'danger' | 'warning' | 'success' | 'info';
|
||||
|
||||
/**
|
||||
* Initialize masonry-layout for homepage (or any other masonry layout cards).
|
||||
*/
|
||||
function initMasonry(): void {
|
||||
for (const grid of getElements<HTMLDivElement>('.masonry')) {
|
||||
new Masonry(grid, {
|
||||
itemSelector: '.masonry-item',
|
||||
percentPosition: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initTooltips() {
|
||||
for (const tooltip of getElements('[data-bs-toggle="tooltip"]')) {
|
||||
new Tooltip(tooltip, { container: 'body', boundary: 'window' });
|
||||
}
|
||||
}
|
||||
|
||||
function initModals() {
|
||||
for (const modal of getElements('[data-bs-toggle="modal"]')) {
|
||||
new Modal(modal);
|
||||
}
|
||||
}
|
||||
|
||||
export function createToast(
|
||||
level: ToastLevel,
|
||||
title: string,
|
||||
@ -71,16 +97,33 @@ export function createToast(
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any active messages from django.contrib.messages and show them in a toast.
|
||||
* Open the tab specified in the URL. For example, /dcim/device-types/1/#tab_frontports will
|
||||
* change the open tab to the Front Ports tab.
|
||||
*/
|
||||
export function initMessageToasts(): void {
|
||||
const elements = document.querySelectorAll<HTMLDivElement>(
|
||||
'body > div#django-messages > div.django-message.toast',
|
||||
);
|
||||
for (const element of elements) {
|
||||
if (element !== null) {
|
||||
const toast = new Toast(element);
|
||||
toast.show();
|
||||
function initTabs() {
|
||||
const { hash } = location;
|
||||
if (hash && hash.match(/^\#tab_.+$/)) {
|
||||
// The tab element will have a data-bs-target attribute with a value of the object type for
|
||||
// the corresponding tab. Once we drop the `tab_` prefix, the hash will match the target
|
||||
// element's data-bs-target value. For example, `#tab_frontports` becomes `#frontports`.
|
||||
const target = hash.replace('tab_', '');
|
||||
for (const element of getElements(`ul.nav.nav-tabs .nav-link[data-bs-target="${target}"]`)) {
|
||||
// Instantiate a Bootstrap tab instance.
|
||||
// See https://getbootstrap.com/docs/5.0/components/navs-tabs/#javascript-behavior
|
||||
const tab = new Tab(element);
|
||||
// Show the tab.
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable any defined Bootstrap Tooltips.
|
||||
*
|
||||
* @see https://getbootstrap.com/docs/5.0/components/tooltips
|
||||
*/
|
||||
export function initBootstrap(): void {
|
||||
for (const func of [initTooltips, initModals, initMasonry, initTabs]) {
|
||||
func();
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { createToast } from './toast';
|
||||
import { isTruthy, getElements, apiPatch, hasError } from './util';
|
||||
import { createToast } from './bs';
|
||||
import { isTruthy, getElements, apiPatch, hasError, slugify } from './util';
|
||||
|
||||
/**
|
||||
* Add onClick callback for toggling rack elevation images.
|
||||
@ -91,8 +91,39 @@ function initConnectionToggle() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a slug field exists, add event listeners to handle automatically generating its value.
|
||||
*/
|
||||
function initReslug(): void {
|
||||
const slugField = document.getElementById('id_slug') as HTMLInputElement;
|
||||
const slugButton = document.getElementById('reslug') as HTMLButtonElement;
|
||||
if (slugField === null || slugButton === null) {
|
||||
return;
|
||||
}
|
||||
const sourceId = slugField.getAttribute('slug-source');
|
||||
const sourceField = document.getElementById(`id_${sourceId}`) as HTMLInputElement;
|
||||
|
||||
if (sourceField === null) {
|
||||
console.error('Unable to find field for slug field.');
|
||||
return;
|
||||
}
|
||||
|
||||
const slugLengthAttr = slugField.getAttribute('maxlength');
|
||||
let slugLength = 50;
|
||||
|
||||
if (slugLengthAttr) {
|
||||
slugLength = Number(slugLengthAttr);
|
||||
}
|
||||
sourceField.addEventListener('blur', () => {
|
||||
slugField.value = slugify(sourceField.value, slugLength);
|
||||
});
|
||||
slugButton.addEventListener('click', () => {
|
||||
slugField.value = slugify(sourceField.value, slugLength);
|
||||
});
|
||||
}
|
||||
|
||||
export function initButtons() {
|
||||
for (const func of [initRackElevation, initConnectionToggle]) {
|
||||
for (const func of [initRackElevation, initConnectionToggle, initReslug]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createToast } from '../toast';
|
||||
import { createToast } from '../bs';
|
||||
import { apiGetBase, getNetboxData, hasError, toggleLoader } from '../util';
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createToast } from '../toast';
|
||||
import { createToast } from '../bs';
|
||||
import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
|
||||
|
||||
/**
|
||||
|
@ -23,7 +23,7 @@ export function getFormData(element: HTMLFormElement): URLSearchParams {
|
||||
/**
|
||||
* Set the value of the number input field based on the selection of the dropdown.
|
||||
*/
|
||||
export function initSpeedSelector(): void {
|
||||
function initSpeedSelector(): void {
|
||||
for (const element of getElements<HTMLAnchorElement>('a.set_speed')) {
|
||||
if (element !== null) {
|
||||
function handleClick(event: Event) {
|
||||
@ -266,7 +266,7 @@ function initScopeSelector() {
|
||||
}
|
||||
|
||||
export function initForms() {
|
||||
for (const func of [initFormElements, initMoveButtons, initScopeSelector]) {
|
||||
for (const func of [initFormElements, initMoveButtons, initSpeedSelector, initScopeSelector]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createToast } from './toast';
|
||||
import { createToast } from './bs';
|
||||
import { apiGetBase, hasError } from './util';
|
||||
|
||||
let timeout: number = 1000;
|
||||
|
16
netbox/project-static/src/messages.ts
Normal file
16
netbox/project-static/src/messages.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Toast } from 'bootstrap';
|
||||
|
||||
/**
|
||||
* Find any active messages from django.contrib.messages and show them in a toast.
|
||||
*/
|
||||
export function initMessages(): void {
|
||||
const elements = document.querySelectorAll<HTMLDivElement>(
|
||||
'body > div#django-messages > div.django-message.toast',
|
||||
);
|
||||
for (const element of elements) {
|
||||
if (element !== null) {
|
||||
const toast = new Toast(element);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,121 +1,34 @@
|
||||
import { Modal, Tooltip } from 'bootstrap';
|
||||
import Masonry from 'masonry-layout';
|
||||
import { initApiSelect, initStaticSelect, initColorSelect } from './select';
|
||||
import { initDateSelector } from './dateSelector';
|
||||
import { initMessageToasts } from './toast';
|
||||
import { initSpeedSelector, initForms } from './forms';
|
||||
import { initForms } from './forms';
|
||||
import { initBootstrap } from './bs';
|
||||
import { initSearch } from './search';
|
||||
import { initSelect } from './select';
|
||||
import { initButtons } from './buttons';
|
||||
import { initSecrets } from './secrets';
|
||||
import { initMessages } from './messages';
|
||||
import { initClipboard } from './clipboard';
|
||||
import { initSearchBar, initInterfaceFilter } from './search';
|
||||
import { initGenerateKeyPair, initLockUnlock, initGetSessionKey } from './secrets';
|
||||
import { initTabs } from './tabs';
|
||||
import { initDateSelector } from './dateSelector';
|
||||
|
||||
import { initTableConfig } from './tableConfig';
|
||||
import { getElements } from './util';
|
||||
|
||||
const INITIALIZERS = [
|
||||
initSearchBar,
|
||||
initMasonry,
|
||||
bindReslug,
|
||||
initApiSelect,
|
||||
initStaticSelect,
|
||||
initDateSelector,
|
||||
initSpeedSelector,
|
||||
initColorSelect,
|
||||
initButtons,
|
||||
initClipboard,
|
||||
initGenerateKeyPair,
|
||||
initLockUnlock,
|
||||
initGetSessionKey,
|
||||
initInterfaceFilter,
|
||||
initTableConfig,
|
||||
initTabs,
|
||||
] as (() => void)[];
|
||||
|
||||
/**
|
||||
* Enable any defined Bootstrap Tooltips.
|
||||
*
|
||||
* @see https://getbootstrap.com/docs/5.0/components/tooltips
|
||||
*/
|
||||
function initBootstrap(): void {
|
||||
if (document !== null) {
|
||||
for (const tooltip of getElements('[data-bs-toggle="tooltip"]')) {
|
||||
new Tooltip(tooltip, { container: 'body', boundary: 'window' });
|
||||
}
|
||||
for (const modal of getElements('[data-bs-toggle="modal"]')) {
|
||||
new Modal(modal);
|
||||
}
|
||||
initMessageToasts();
|
||||
initForms();
|
||||
function init() {
|
||||
for (const init of [
|
||||
initBootstrap,
|
||||
initMessages,
|
||||
initForms,
|
||||
initSearch,
|
||||
initSelect,
|
||||
initDateSelector,
|
||||
initButtons,
|
||||
initClipboard,
|
||||
initSecrets,
|
||||
initTableConfig,
|
||||
]) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize masonry-layout for homepage (or any other masonry layout cards).
|
||||
*/
|
||||
function initMasonry(): void {
|
||||
if (document !== null) {
|
||||
for (const grid of getElements('.masonry')) {
|
||||
new Masonry(grid, {
|
||||
itemSelector: '.masonry-item',
|
||||
percentPosition: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a slug from any input string.
|
||||
*
|
||||
* @param slug Original string.
|
||||
* @param chars Maximum number of characters.
|
||||
* @returns Slugified string.
|
||||
*/
|
||||
function slugify(slug: string, chars: number): string {
|
||||
return slug
|
||||
.replace(/[^\-\.\w\s]/g, '') // Remove unneeded chars
|
||||
.replace(/^[\s\.]+|[\s\.]+$/g, '') // Trim leading/trailing spaces
|
||||
.replace(/[\-\.\s]+/g, '-') // Convert spaces and decimals to hyphens
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.substring(0, chars); // Trim to first chars chars
|
||||
}
|
||||
|
||||
/**
|
||||
* If a slug field exists, add event listeners to handle automatically generating its value.
|
||||
*/
|
||||
function bindReslug(): void {
|
||||
const slugField = document.getElementById('id_slug') as HTMLInputElement;
|
||||
const slugButton = document.getElementById('reslug') as HTMLButtonElement;
|
||||
if (slugField === null || slugButton === null) {
|
||||
return;
|
||||
}
|
||||
const sourceId = slugField.getAttribute('slug-source');
|
||||
const sourceField = document.getElementById(`id_${sourceId}`) as HTMLInputElement;
|
||||
|
||||
if (sourceField === null) {
|
||||
console.error('Unable to find field for slug field.');
|
||||
return;
|
||||
}
|
||||
|
||||
const slugLengthAttr = slugField.getAttribute('maxlength');
|
||||
let slugLength = 50;
|
||||
|
||||
if (slugLengthAttr) {
|
||||
slugLength = Number(slugLengthAttr);
|
||||
}
|
||||
sourceField.addEventListener('blur', () => {
|
||||
slugField.value = slugify(sourceField.value, slugLength);
|
||||
});
|
||||
slugButton.addEventListener('click', () => {
|
||||
slugField.value = slugify(sourceField.value, slugLength);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
initBootstrap();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', initBootstrap);
|
||||
}
|
||||
|
||||
for (const init of INITIALIZERS) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ function isSearchButton(el: any): el is SearchFilterButton {
|
||||
return el?.dataset?.searchValue ?? null !== null;
|
||||
}
|
||||
|
||||
export function initSearchBar() {
|
||||
function initSearchBar() {
|
||||
const dropdown = document.getElementById('object-type-selector');
|
||||
const selectedValue = document.getElementById('selected-value') as HTMLSpanElement;
|
||||
const selectedType = document.getElementById('search-obj-type') as HTMLInputElement;
|
||||
@ -41,7 +41,7 @@ export function initSearchBar() {
|
||||
/**
|
||||
* Initialize Interface Table Filter Elements.
|
||||
*/
|
||||
export function initInterfaceFilter() {
|
||||
function initInterfaceFilter() {
|
||||
for (const element of getElements<HTMLInputElement>('input.interface-filter')) {
|
||||
/**
|
||||
* Filter on-page table by input text.
|
||||
@ -79,3 +79,9 @@ export function initInterfaceFilter() {
|
||||
element.addEventListener('keyup', handleInput);
|
||||
}
|
||||
}
|
||||
|
||||
export function initSearch() {
|
||||
for (const func of [initSearchBar, initInterfaceFilter]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Modal } from 'bootstrap';
|
||||
import { createToast } from './bs';
|
||||
import { apiGetBase, apiPostForm, getElements, isApiError, hasError } from './util';
|
||||
import { createToast } from './toast';
|
||||
|
||||
/**
|
||||
* Initialize Generate Private Key Pair Elements.
|
||||
*/
|
||||
export function initGenerateKeyPair() {
|
||||
function initGenerateKeyPair() {
|
||||
const element = document.getElementById('new_keypair_modal') as HTMLDivElement;
|
||||
const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;
|
||||
// If the elements are not loaded, stop.
|
||||
@ -86,7 +86,7 @@ function toggleSecretButtons(id: string, action: 'lock' | 'unlock') {
|
||||
/**
|
||||
* Initialize Lock & Unlock button event listeners & callbacks.
|
||||
*/
|
||||
export function initLockUnlock() {
|
||||
function initLockUnlock() {
|
||||
const privateKeyModalElem = document.getElementById('privkey_modal');
|
||||
if (privateKeyModalElem === null) {
|
||||
return;
|
||||
@ -184,7 +184,7 @@ function requestSessionKey(privateKey: string) {
|
||||
/**
|
||||
* Initialize Request Session Key Elements.
|
||||
*/
|
||||
export function initGetSessionKey() {
|
||||
function initGetSessionKey() {
|
||||
for (const element of getElements<HTMLButtonElement>('#request_session_key')) {
|
||||
/**
|
||||
* Send the user's input private key to the API to get a session key, which will be stored as
|
||||
@ -200,3 +200,9 @@ export function initGetSessionKey() {
|
||||
element.addEventListener('click', handleClick);
|
||||
}
|
||||
}
|
||||
|
||||
export function initSecrets() {
|
||||
for (const func of [initGenerateKeyPair, initLockUnlock, initGetSessionKey]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
@ -1,167 +0,0 @@
|
||||
import Choices from "choices.js";
|
||||
import queryString from "query-string";
|
||||
import { getApiData, isApiError } from "../util";
|
||||
import { createToast } from "../toast";
|
||||
|
||||
import type { Choices as TChoices } from "choices.js";
|
||||
|
||||
interface CustomSelect extends HTMLSelectElement {
|
||||
dataset: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
function isCustomSelect(el: HTMLSelectElement): el is CustomSelect {
|
||||
return typeof el?.dataset?.url === "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a select element should be filtered by the value of another select element.
|
||||
*
|
||||
* Looks for the DOM attribute `data-query-param-<name of other field>`, which would look like:
|
||||
* `["$<name>"]`
|
||||
*
|
||||
* If the attribute exists, parse out the raw value. In the above example, this would be `name`.
|
||||
* @param element Element to scan
|
||||
* @returns Attribute name, or null if it was not found.
|
||||
*/
|
||||
function getFilteredBy<T extends HTMLElement>(element: T): string[] {
|
||||
const pattern = new RegExp(/\[|\]|"|\$/g);
|
||||
const keys = Object.keys(element.dataset);
|
||||
const filteredBy = [] as string[];
|
||||
for (const key of keys) {
|
||||
if (key.includes("queryParam")) {
|
||||
const value = element.dataset[key];
|
||||
if (typeof value !== "undefined") {
|
||||
const parsed = JSON.parse(value) as string | string[];
|
||||
if (Array.isArray(parsed)) {
|
||||
filteredBy.push(parsed[0].replaceAll(pattern, ""));
|
||||
} else {
|
||||
filteredBy.push(value.replaceAll(pattern, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key === "url" && element.dataset.url?.includes(`{{`)) {
|
||||
/**
|
||||
* If the URL contains a Django/Jinja template variable tag we need to extract the variable
|
||||
* name and consider this a field to monitor for changes.
|
||||
*/
|
||||
const value = element.dataset.url.match(/\{\{(.+)\}\}/);
|
||||
if (value !== null) {
|
||||
filteredBy.push(value[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredBy;
|
||||
}
|
||||
|
||||
export function initApiSelect() {
|
||||
const elements = document.querySelectorAll(
|
||||
".netbox-select2-api"
|
||||
) as NodeListOf<HTMLSelectElement>;
|
||||
|
||||
for (const element of elements) {
|
||||
if (isCustomSelect(element)) {
|
||||
let { url } = element.dataset;
|
||||
|
||||
const instance = new Choices(element, {
|
||||
noChoicesText: "No Options Available",
|
||||
itemSelectText: "",
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve all objects for this object type.
|
||||
*
|
||||
* @param choiceUrl Optionally override the URL for filtering. If not set, the URL
|
||||
* from the DOM attributes is used.
|
||||
* @returns Data parsed into Choices.JS Choices.
|
||||
*/
|
||||
async function getChoices(
|
||||
choiceUrl: string = url
|
||||
): Promise<TChoices.Choice[]> {
|
||||
if (choiceUrl.includes(`{{`)) {
|
||||
return [];
|
||||
}
|
||||
return getApiData(choiceUrl).then((data) => {
|
||||
if (isApiError(data)) {
|
||||
const toast = createToast("danger", data.exception, data.error);
|
||||
toast.show();
|
||||
return [];
|
||||
}
|
||||
const { results } = data;
|
||||
const options = [] as TChoices.Choice[];
|
||||
|
||||
if (results.length !== 0) {
|
||||
for (const result of results) {
|
||||
const choice = {
|
||||
value: result.id.toString(),
|
||||
label: result.name,
|
||||
} as TChoices.Choice;
|
||||
options.push(choice);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
});
|
||||
}
|
||||
|
||||
const filteredBy = getFilteredBy(element);
|
||||
|
||||
if (filteredBy.length !== 0) {
|
||||
for (const filter of filteredBy) {
|
||||
// Find element with the `name` attribute matching this element's filtered-by attribute.
|
||||
const groupElem = document.querySelector(
|
||||
`[name=${filter}]`
|
||||
) as HTMLSelectElement;
|
||||
|
||||
if (groupElem !== null) {
|
||||
/**
|
||||
* When the group's selection changes, re-query the dependant element's options, but
|
||||
* filtered to results matching the group's ID.
|
||||
*
|
||||
* @param event Group's DOM event.
|
||||
*/
|
||||
function handleEvent(event: Event) {
|
||||
let filterUrl: string | undefined;
|
||||
|
||||
const target = event.target as HTMLSelectElement;
|
||||
|
||||
if (target.value) {
|
||||
if (url.includes(`{{`)) {
|
||||
/**
|
||||
* If the URL contains a Django/Jinja template variable tag, we need to replace
|
||||
* the tag with the event's value.
|
||||
*/
|
||||
url = url.replaceAll(/\{\{(.+)\}\}/g, target.value);
|
||||
element.setAttribute("data-url", url);
|
||||
}
|
||||
let queryKey = filter;
|
||||
if (filter?.includes("_group")) {
|
||||
/**
|
||||
* For example, a tenant's group relationship field is `group`, but the field
|
||||
* name is `tenant_group`.
|
||||
*/
|
||||
queryKey = "group";
|
||||
}
|
||||
filterUrl = queryString.stringifyUrl({
|
||||
url,
|
||||
query: { [`${queryKey}_id`]: groupElem.value },
|
||||
});
|
||||
}
|
||||
|
||||
instance.setChoices(
|
||||
() => getChoices(filterUrl),
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
groupElem.addEventListener("addItem", handleEvent);
|
||||
groupElem.addEventListener("removeItem", handleEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance.setChoices(() => getChoices());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./api";
|
||||
export * from "./static";
|
@ -1,16 +0,0 @@
|
||||
import Choices from "choices.js";
|
||||
|
||||
export function initStaticSelect() {
|
||||
const elements = document.querySelectorAll(
|
||||
".netbox-select2-static"
|
||||
) as NodeListOf<HTMLSelectElement>;
|
||||
|
||||
for (const element of elements) {
|
||||
if (element !== null) {
|
||||
new Choices(element, {
|
||||
noChoicesText: "No Options Available",
|
||||
itemSelectText: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import SlimSelect from 'slim-select';
|
||||
import queryString from 'query-string';
|
||||
import { getApiData, isApiError, getElements, isTruthy } from '../util';
|
||||
import { createToast } from '../toast';
|
||||
import { getApiData, isApiError, getElements, isTruthy, hasError } from '../util';
|
||||
import { createToast } from '../bs';
|
||||
import { setOptionStyles, getFilteredBy, toggle } from './util';
|
||||
|
||||
import type { Option } from 'slim-select/dist/data';
|
||||
@ -38,7 +38,7 @@ const REPLACE_PATTERNS = [
|
||||
[new RegExp(/termination_(a|b)_(.+)/g), '$2_id'],
|
||||
// A tenant's group relationship field is `group`, but the field name is `tenant_group`.
|
||||
[new RegExp(/tenant_(group)/g), '$1_id'],
|
||||
// Append `_id` to any fields
|
||||
// Append `_id` to any fields
|
||||
[new RegExp(/^([A-Za-z0-9]+)(_id)?$/g), '$1_id'],
|
||||
] as ReplaceTuple[];
|
||||
|
||||
@ -72,9 +72,12 @@ async function getOptions(
|
||||
.map(option => option.value);
|
||||
|
||||
return getApiData(url).then(data => {
|
||||
if (isApiError(data)) {
|
||||
const toast = createToast('danger', data.exception, data.error);
|
||||
toast.show();
|
||||
if (hasError(data)) {
|
||||
if (isApiError(data)) {
|
||||
createToast('danger', data.exception, data.error).show();
|
||||
return [PLACEHOLDER];
|
||||
}
|
||||
createToast('danger', `Error Fetching Options for field ${select.name}`, data.error).show();
|
||||
return [PLACEHOLDER];
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
export * from './api';
|
||||
export * from './static';
|
||||
export * from './color';
|
||||
import { initApiSelect } from './api';
|
||||
import { initColorSelect } from './color';
|
||||
import { initStaticSelect } from './static';
|
||||
|
||||
export function initSelect() {
|
||||
for (const func of [initApiSelect, initColorSelect, initStaticSelect]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createToast } from './toast';
|
||||
import { createToast } from './bs';
|
||||
import { getElements, apiPatch, hasError, getSelectedOptions } from './util';
|
||||
|
||||
/**
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { Tab } from 'bootstrap';
|
||||
import { getElements } from './util';
|
||||
|
||||
/**
|
||||
* Open the tab specified in the URL. For example, /dcim/device-types/1/#tab_frontports will
|
||||
* change the open tab to the Front Ports tab.
|
||||
*/
|
||||
export function initTabs() {
|
||||
const { hash } = location;
|
||||
if (hash && hash.match(/^\#tab_.+$/)) {
|
||||
// The tab element will have a data-bs-target attribute with a value of the object type for
|
||||
// the corresponding tab. Once we drop the `tab_` prefix, the hash will match the target
|
||||
// element's data-bs-target value. For example, `#tab_frontports` becomes `#frontports`.
|
||||
const target = hash.replace('tab_', '');
|
||||
for (const element of getElements(`ul.nav.nav-tabs .nav-link[data-bs-target="${target}"]`)) {
|
||||
// Instantiate a Bootstrap tab instance.
|
||||
// See https://getbootstrap.com/docs/5.0/components/navs-tabs/#javascript-behavior
|
||||
const tab = new Tab(element);
|
||||
// Show the tab.
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,22 @@ export function hasError(data: Record<string, unknown>): data is ErrorBase {
|
||||
return 'error' in data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a slug from any input string.
|
||||
*
|
||||
* @param slug Original string.
|
||||
* @param chars Maximum number of characters.
|
||||
* @returns Slugified string.
|
||||
*/
|
||||
export function slugify(slug: string, chars: number): string {
|
||||
return slug
|
||||
.replace(/[^\-\.\w\s]/g, '') // Remove unneeded chars
|
||||
.replace(/^[\s\.]+|[\s\.]+$/g, '') // Trim leading/trailing spaces
|
||||
.replace(/[\-\.\s]+/g, '-') // Convert spaces and decimals to hyphens
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.substring(0, chars); // Trim to first chars chars
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to determine if a value is not null, undefined, or empty.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user