From 21db209f47059d9b085b2feab634f14ef50b9088 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Thu, 22 Apr 2021 15:58:46 -0700 Subject: [PATCH] fix issue where select fields with a pre-populated value were reset when forms were submitted, due to having the disabled attribute set. --- netbox/project-static/dist/netbox.js | Bin 458893 -> 460406 bytes netbox/project-static/dist/netbox.js.map | Bin 1300987 -> 1303596 bytes netbox/project-static/src/forms.ts | 82 ++++--- netbox/project-static/src/select/api.ts | 261 ++++++++++----------- netbox/project-static/src/select/color.ts | 2 +- netbox/project-static/src/select/static.ts | 2 +- netbox/project-static/src/select/util.ts | 31 ++- netbox/templates/generic/object_edit.html | 18 +- 8 files changed, 201 insertions(+), 195 deletions(-) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 992a49015b9bd2dc2724a4ed5b444e4684b759df..75f297da2c751eb79ff03619cd9dd1ed5cec3700 100644 GIT binary patch delta 6315 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 delta 4850 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 AGXMYp diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 0e2a7e8926ccef3fd899be9ed94e0ded0027bf64..ebac5eddc2b859d24e7a332e10f9caf8a047249f 100644 GIT binary patch delta 7750 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 VOK*PnKxc=DzlwBcN9A)1{|6)NVIBYg delta 6277 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 aR$FUyA9HM2nLWJVXZTd)7yPd0FZ>UB2$*RA diff --git a/netbox/project-static/src/forms.ts b/netbox/project-static/src/forms.ts index 3a340c044..92728ef37 100644 --- a/netbox/project-static/src/forms.ts +++ b/netbox/project-static/src/forms.ts @@ -43,6 +43,44 @@ function initSpeedSelector(): void { } } +function handleFormSubmit(event: Event, form: HTMLFormElement): void { + // Track the names of each invalid field. + const invalids = new Set(); + + for (const element of form.querySelectorAll('*[name]')) { + if (!element.validity.valid) { + invalids.add(element.name); + + // If the field is invalid, but contains the .is-valid class, remove it. + if (element.classList.contains('is-valid')) { + element.classList.remove('is-valid'); + } + // If the field is invalid, but doesn't contain the .is-invalid class, add it. + if (!element.classList.contains('is-invalid')) { + element.classList.add('is-invalid'); + } + } else { + // If the field is valid, but contains the .is-invalid class, remove it. + if (element.classList.contains('is-invalid')) { + element.classList.remove('is-invalid'); + } + // If the field is valid, but doesn't contain the .is-valid class, add it. + if (!element.classList.contains('is-valid')) { + element.classList.add('is-valid'); + } + } + } + + if (invalids.size !== 0) { + // If there are invalid fields, pick the first field and scroll to it. + const firstInvalid = form.elements.namedItem(Array.from(invalids)[0]) as Element; + scrollTo(firstInvalid); + + // If the form has invalid fields, don't submit it. + event.preventDefault(); + } +} + /** * Attach an event listener to each form's submitter (button[type=submit]). When called, the * callback checks the validity of each form field and adds the appropriate Bootstrap CSS class @@ -50,53 +88,13 @@ function initSpeedSelector(): void { */ function initFormElements() { for (const form of getElements('form')) { - const { elements } = form; // Find each of the form's submitters. Most object edit forms have a "Create" and // a "Create & Add", so we need to add a listener to both. - const submitters = form.querySelectorAll('button[type=submit]'); + const submitters = form.querySelectorAll('button[type=submit]'); - function callback(event: Event): void { - // Track the names of each invalid field. - const invalids = new Set(); - - for (const el of elements) { - const element = (el as unknown) as FormControls; - - if (!element.validity.valid) { - invalids.add(element.name); - - // If the field is invalid, but contains the .is-valid class, remove it. - if (element.classList.contains('is-valid')) { - element.classList.remove('is-valid'); - } - // If the field is invalid, but doesn't contain the .is-invalid class, add it. - if (!element.classList.contains('is-invalid')) { - element.classList.add('is-invalid'); - } - } else { - // If the field is valid, but contains the .is-invalid class, remove it. - if (element.classList.contains('is-invalid')) { - element.classList.remove('is-invalid'); - } - // If the field is valid, but doesn't contain the .is-valid class, add it. - if (!element.classList.contains('is-valid')) { - element.classList.add('is-valid'); - } - } - } - - if (invalids.size !== 0) { - // If there are invalid fields, pick the first field and scroll to it. - const firstInvalid = elements.namedItem(Array.from(invalids)[0]) as Element; - scrollTo(firstInvalid); - - // If the form has invalid fields, don't submit it. - event.preventDefault(); - } - } for (const submitter of submitters) { // Add the event listener to each submitter. - submitter.addEventListener('click', callback); + submitter.addEventListener('click', event => handleFormSubmit(event, form)); } } } diff --git a/netbox/project-static/src/select/api.ts b/netbox/project-static/src/select/api.ts index 9db037c05..007e0814a 100644 --- a/netbox/project-static/src/select/api.ts +++ b/netbox/project-static/src/select/api.ts @@ -2,12 +2,12 @@ import SlimSelect from 'slim-select'; import queryString from 'query-string'; import { getApiData, isApiError, getElements, isTruthy, hasError } from '../util'; import { createToast } from '../bs'; -import { setOptionStyles, getFilteredBy, toggle } from './util'; +import { setOptionStyles, toggle, getDependencyIds } from './util'; import type { Option } from 'slim-select/dist/data'; type WithUrl = { - url: string; + 'data-url': string; }; type WithExclude = { @@ -16,18 +16,16 @@ type WithExclude = { type ReplaceTuple = [RegExp, string]; -interface CustomSelect> extends HTMLSelectElement { - dataset: T; -} +type CustomSelect> = HTMLSelectElement & T; -function isCustomSelect(el: HTMLSelectElement): el is CustomSelect { - return typeof el?.dataset?.url === 'string'; +function hasUrl(el: HTMLSelectElement): el is CustomSelect { + const value = el.getAttribute('data-url'); + return typeof value === 'string' && value !== ''; } function hasExclusions(el: HTMLSelectElement): el is CustomSelect { - return ( - typeof el?.dataset?.queryParamExclude === 'string' && el?.dataset?.queryParamExclude !== '' - ); + const exclude = el.getAttribute('data-query-param-exclude'); + return typeof exclude === 'string' && exclude !== ''; } const DISABLED_ATTRIBUTES = ['occupied'] as string[]; @@ -68,65 +66,71 @@ async function getOptions( // existing object. When we fetch options from the API later, we can set any of the options // contained in this array to `selected`. const selectOptions = Array.from(select.options) - .filter(option => option.value !== '') - .map(option => option.value); + .map(option => option.getAttribute('value')) + .filter(isTruthy); - return getApiData(url).then(data => { - 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(); + const data = await getApiData(url); + 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]; + } - const { results } = data; - const options = [PLACEHOLDER] as Option[]; + const { results } = data; + const options = [PLACEHOLDER] as Option[]; - for (const result of results) { - const text = getDisplayName(result, select); - const data = {} as Record; - const value = result.id.toString(); + for (const result of results) { + const text = getDisplayName(result, select); + 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 = k.replaceAll('_', '-'); - data[key] = String(v); + // 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 = k.replaceAll('_', '-'); + data[key] = String(v); + } + // Set option to disabled if the result contains a matching key and is truthy. + if (DISABLED_ATTRIBUTES.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; } } - - let style, selected, disabled; - - // Set pre-selected options. - if (selectOptions.includes(value)) { - selected = true; - } - - // Set option to disabled if it is contained within the disabled array. - if (selectOptions.some(option => disabledOptions.includes(option))) { - disabled = true; - } - - // Set option to disabled if the result contains a matching key and is truthy. - if (DISABLED_ATTRIBUTES.some(key => Object.keys(result).includes(key) && result[key])) { - disabled = true; - } - - const option = { - value, - text, - data, - style, - selected, - disabled, - } as Option; - - options.push(option); } - return options; - }); + + // Set option to disabled if it is contained within the disabled array. + if (selectOptions.some(option => disabledOptions.includes(option))) { + disabled = true; + } + + // Set pre-selected options. + if (selectOptions.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.push(option); + } + return options; } /** @@ -175,27 +179,27 @@ function getDisplayName(result: APIObjectBase, select: HTMLSelectElement): strin */ export function initApiSelect() { for (const select of getElements('.netbox-select2-api')) { - const filterMap = getFilteredBy(select); + const dependencies = getDependencyIds(select); // Initialize an event, so other elements relying on this element can subscribe to this // element's value. const event = new Event(`netbox.select.onload.${select.name}`); // Query Parameters - will have attributes added below. const query = {} as Record; - // List of OTHER elements THIS element relies on for query filtering. - const groupBy = [] as HTMLSelectElement[]; - if (isCustomSelect(select)) { + if (hasUrl(select)) { // Store the original URL, so it can be referred back to as filter-by elements change. - const originalUrl = JSON.parse(JSON.stringify(select.dataset.url)) as string; - // Unpack the original URL with the intent of reassigning it as context updates. - let { url } = select.dataset; + // const originalUrl = select.getAttribute('data-url') as string; + // Get the original URL with the intent of reassigning it as context updates. + let url = select.getAttribute('data-url') ?? ''; const placeholder = getPlaceholder(select); let disabledOptions = [] as string[]; if (hasExclusions(select)) { try { - const exclusions = JSON.parse(select.dataset.queryParamExclude) as string[]; + const exclusions = JSON.parse( + select.getAttribute('data-query-param-exclude') ?? '[]', + ) as string[]; disabledOptions = [...disabledOptions, ...exclusions]; } catch (err) { console.warn( @@ -207,7 +211,7 @@ export function initApiSelect() { const instance = new SlimSelect({ select, allowDeselect: true, - deselectLabel: ``, + deselectLabel: ``, placeholder, onChange() { const element = instance.slim.container ?? null; @@ -233,42 +237,52 @@ export function initApiSelect() { instance.slim.container.classList.remove(className); } - for (let [key, value] of filterMap) { - if (value === '') { - // An empty value is set if the key contains a `$`, indicating reliance on another field. - const elem = document.querySelector(`[name=${key}]`) as HTMLSelectElement; - if (elem !== null) { - groupBy.push(elem); - if (elem.value !== '') { - // If the element's form value exists, add it to the map. - value = elem.value; - filterMap.set(key, elem.value); + /** + * Update an element's API URL based on the value of another element upon which this element + * relies. + * + * @param id DOM ID of the other element. + */ + function updateQuery(id: string) { + let key = id; + // Find the element dependency. + const element = document.getElementById(`id_${id}`) as Nullable; + if (element !== null) { + if (element.value !== '') { + // If the dependency has a value, parse the dependency's name (form key) for any + // required replacements. + for (const [pattern, replacement] of REPLACE_PATTERNS) { + if (id.match(pattern)) { + key = id.replaceAll(pattern, replacement); + break; + } + } + // 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/`. + if (url.includes(`{{`)) { + for (const test of url.matchAll(new RegExp(`({{(${id}|${key})}})`, 'g'))) { + // The template tag may contain the original element name or the post-parsed value. + url = url.replaceAll(test[1], element.value); + } + // Set the DOM attribute to reflect the change. + select.setAttribute('data-url', url); } } - } - - // A non-empty value indicates a static query parameter. - for (const [pattern, replacement] of REPLACE_PATTERNS) { - // Check the query param key to see if we should modify it. - if (key.match(pattern)) { - key = key.replaceAll(pattern, replacement); - break; + if (isTruthy(element.value)) { + // Add the dependency's value to the URL query. + query[key] = element.value; } } - - if (url.includes(`{{`) && value !== '') { - // If the URL contains a Django/Jinja template variable, we need to replace the - // tag with the event's value. - url = url.replaceAll(new RegExp(`{{${key}}}`, 'g'), value); - select.setAttribute('data-url', url); - } - - // Add post-replaced key/value pairs to the query object. - if (isTruthy(value)) { - query[key] = value; - } + } + // Process each of the dependencies, updating this element's URL or other attributes as + // needed. + for (const dep of dependencies) { + updateQuery(dep); } + // Create a valid encoded URL with all query params. url = queryString.stringifyUrl({ url, query }); /** @@ -279,64 +293,35 @@ export function initApiSelect() { */ function handleEvent(event: Event) { const target = event.target as HTMLSelectElement; - - if (isTruthy(target.value)) { - let name = target.name; - - for (const [pattern, replacement] of REPLACE_PATTERNS) { - // Check the query param key to see if we should modify it. - if (name.match(pattern)) { - name = name.replaceAll(pattern, replacement); - break; - } - } - - if (url.includes(`{{`) && target.name && target.value) { - // If the URL (still) contains a Django/Jinja template variable, we need to replace - // the tag with the event's value. - url = url.replaceAll(new RegExp(`{{${target.name}}}`, 'g'), target.value); - select.setAttribute('data-url', url); - } - - if (filterMap.get(target.name) === '') { - // Update empty filter map values now that there is a value. - filterMap.set(target.name, target.value); - } - // Add post-replaced key/value pairs to the query object. - query[name] = target.value; - // Create a URL with all relevant query parameters. - url = queryString.stringifyUrl({ url, query }); - } else { - url = originalUrl; - } + // Update the element's URL after any changes to a dependency. + updateQuery(target.id); // Disable the element while data is loading. toggle('disable', instance); // Load new data. getOptions(url, select, disabledOptions) .then(data => instance.setData(data)) + .catch(console.error) .finally(() => { // Re-enable the element after data has loaded. toggle('enable', instance); // Inform any event listeners that data has updated. select.dispatchEvent(event); }); - // Stop event bubbling. - event.preventDefault(); } - for (const group of groupBy) { - // Re-fetch data when the group changes. - group.addEventListener('change', handleEvent); - - // Subscribe this instance (the child that relies on `group`) to any changes of the - // group's value, so we can re-render options. - select.addEventListener(`netbox.select.onload.${group.name}`, handleEvent); + for (const dep of dependencies) { + const element = document.getElementById(`id_${dep}`); + if (element !== null) { + element.addEventListener('change', handleEvent); + } + select.addEventListener(`netbox.select.onload.${dep}`, handleEvent); } // Load data. getOptions(url, select, disabledOptions) .then(options => instance.setData(options)) + .catch(console.error) .finally(() => { // Set option styles, if the field calls for it (color selectors). setOptionStyles(instance); diff --git a/netbox/project-static/src/select/color.ts b/netbox/project-static/src/select/color.ts index bde01406c..a413cebf4 100644 --- a/netbox/project-static/src/select/color.ts +++ b/netbox/project-static/src/select/color.ts @@ -34,7 +34,7 @@ export function initColorSelect(): void { select, allowDeselect: true, // Inherit the calculated color on the deselect icon. - deselectLabel: ``, + deselectLabel: ``, }); // Style the select container to match any pre-selectd options. diff --git a/netbox/project-static/src/select/static.ts b/netbox/project-static/src/select/static.ts index b82ef7ffe..d65d06d80 100644 --- a/netbox/project-static/src/select/static.ts +++ b/netbox/project-static/src/select/static.ts @@ -14,7 +14,7 @@ export function initStaticSelect() { const instance = new SlimSelect({ select, allowDeselect: true, - deselectLabel: ``, + deselectLabel: ``, placeholder, }); diff --git a/netbox/project-static/src/select/util.ts b/netbox/project-static/src/select/util.ts index 6e2c0381d..e12d79ef1 100644 --- a/netbox/project-static/src/select/util.ts +++ b/netbox/project-static/src/select/util.ts @@ -63,7 +63,7 @@ export function setOptionStyles(instance: SlimSelect): void { const fg = readableColor(bg); // Add a unique identifier to the style element. - style.dataset.netbox = id; + style.setAttribute('data-netbox', id); // Scope the CSS to apply both the list item and the selected item. style.innerHTML = ` @@ -155,3 +155,32 @@ export function getFilteredBy(element: T): Map(element: Nullable): Generator { + const keyPattern = new RegExp(/data-query-param-/g); + if (element !== null) { + for (const attr of element.attributes) { + if (attr.name.startsWith('data-query-param') && attr.name !== 'data-query-param-exclude') { + const dep = attr.name.replaceAll(keyPattern, ''); + yield dep; + for (const depNext of getAllDependencyIds(document.getElementById(`id_${dep}`))) { + yield depNext; + } + } else if (attr.name === 'data-url' && attr.value.includes(`{{`)) { + const value = attr.value.match(/\{\{(.+)\}\}/); + if (value !== null) { + const dep = value[1]; + yield dep; + for (const depNext of getAllDependencyIds(document.getElementById(`id_${dep}`))) { + yield depNext; + } + } + } + } + } +} + +export function getDependencyIds(element: Nullable): string[] { + const ids = new Set(getAllDependencyIds(element)); + return Array.from(ids).map(i => i.replaceAll('_id', '')); +} diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 6089ce123..3d0216ce0 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -3,17 +3,11 @@ {% block title %}{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}{% endblock %} {% block controls %} -{% if settings.DOCS_ROOT %} - -{% endif %} + {% if settings.DOCS_ROOT %} + + {% endif %} {% endblock %} {% block content %} @@ -26,7 +20,7 @@ {% block form %} {% if form.Meta.fieldsets %} - {# Render grouped fields accoring to Form #} + {# Render grouped fields according to Form #} {% for group, fields in form.Meta.fieldsets %}

{{ group }}