From 682ad5640b50e45ca745c64f71f89afb4e2f0cae Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 30 Nov 2023 15:15:02 -0500 Subject: [PATCH 01/17] wip --- src/registrar/admin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 2f9bc97c5..b25014fcf 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -455,6 +455,8 @@ class DomainInformationAdmin(ListHeaderAdmin): "is_policy_acknowledged", ] + filter_horizontal = ('other_contacts',) + def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. We have 1 conditions that determine which fields are read-only: @@ -591,6 +593,15 @@ class DomainApplicationAdmin(ListHeaderAdmin): "is_policy_acknowledged", ] + filter_horizontal = ('current_websites', 'alternative_domains') + + # lists in filter_horizontal are not sorted properly, sort them + # by website + def formfield_for_manytomany(self, db_field, request, **kwargs): + if db_field.name in ("current_websites", "alternative_domains"): + kwargs["queryset"] = models.Website.objects.all().order_by('website') # Sort websites + return super().formfield_for_manytomany(db_field, request, **kwargs) + # Trigger action when a fieldset is changed def save_model(self, request, obj, form, change): if obj and obj.creator.status != models.User.RESTRICTED: @@ -749,7 +760,7 @@ class DomainAdmin(ListHeaderAdmin): change_form_template = "django/admin/domain_change_form.html" change_list_template = "django/admin/domain_change_list.html" readonly_fields = ["state", "expiration_date"] - + def export_data_type(self, request): # match the CSV example with all the fields response = HttpResponse(content_type="text/csv") From 7904ca0cc4e15b070a2b62ee569693756e74fcce Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 30 Nov 2023 15:43:54 -0500 Subject: [PATCH 02/17] implement filter_horizontal on other contacts on inline DomainInfo --- src/registrar/admin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index b25014fcf..e8eb206c2 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -455,7 +455,7 @@ class DomainInformationAdmin(ListHeaderAdmin): "is_policy_acknowledged", ] - filter_horizontal = ('other_contacts',) + filter_horizontal = ("other_contacts",) def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. @@ -593,13 +593,13 @@ class DomainApplicationAdmin(ListHeaderAdmin): "is_policy_acknowledged", ] - filter_horizontal = ('current_websites', 'alternative_domains') + filter_horizontal = ("current_websites", "alternative_domains") # lists in filter_horizontal are not sorted properly, sort them # by website def formfield_for_manytomany(self, db_field, request, **kwargs): if db_field.name in ("current_websites", "alternative_domains"): - kwargs["queryset"] = models.Website.objects.all().order_by('website') # Sort websites + kwargs["queryset"] = models.Website.objects.all().order_by("website") # Sort websites return super().formfield_for_manytomany(db_field, request, **kwargs) # Trigger action when a fieldset is changed @@ -730,6 +730,7 @@ class DomainInformationInline(admin.StackedInline): fieldsets = DomainInformationAdmin.fieldsets analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields + filter_horizontal = ("other_contacts",) def get_readonly_fields(self, request, obj=None): return DomainInformationAdmin.get_readonly_fields(self, request, obj=None) @@ -760,7 +761,7 @@ class DomainAdmin(ListHeaderAdmin): change_form_template = "django/admin/domain_change_form.html" change_list_template = "django/admin/domain_change_list.html" readonly_fields = ["state", "expiration_date"] - + def export_data_type(self, request): # match the CSV example with all the fields response = HttpResponse(content_type="text/csv") From 856a32210d4f053c141e384f4af2eea33619e692 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 30 Nov 2023 16:27:24 -0500 Subject: [PATCH 03/17] other contacts updated in domain applications --- src/registrar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e8eb206c2..93511929f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -593,7 +593,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): "is_policy_acknowledged", ] - filter_horizontal = ("current_websites", "alternative_domains") + filter_horizontal = ("current_websites", "alternative_domains", "other_contacts") # lists in filter_horizontal are not sorted properly, sort them # by website From 1c657a770485b9528fa7f24fab0af86912ab1460 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 1 Dec 2023 18:01:02 -0500 Subject: [PATCH 04/17] Rough JS for adding then enabling/disabling the related widget buttons on the horizontal multi-select --- src/registrar/admin.py | 20 +++ src/registrar/assets/js/get-gov-admin.js | 162 +++++++++++++++++- src/registrar/templates/admin/base_site.html | 1 + .../django/admin/domain_change_form.html | 5 - 4 files changed, 182 insertions(+), 6 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e8eb206c2..493bc90cd 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -455,7 +455,16 @@ class DomainInformationAdmin(ListHeaderAdmin): "is_policy_acknowledged", ] + # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets + # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) + + # lists in filter_horizontal are not sorted properly, sort them + # by first_name + def formfield_for_manytomany(self, db_field, request, **kwargs): + if db_field.name in ("other_contacts",): + kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts + return super().formfield_for_manytomany(db_field, request, **kwargs) def get_readonly_fields(self, request, obj=None): """Set the read-only state on form elements. @@ -593,6 +602,8 @@ class DomainApplicationAdmin(ListHeaderAdmin): "is_policy_acknowledged", ] + # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets + # to activate the edit/delete/view buttons filter_horizontal = ("current_websites", "alternative_domains") # lists in filter_horizontal are not sorted properly, sort them @@ -730,7 +741,16 @@ class DomainInformationInline(admin.StackedInline): fieldsets = DomainInformationAdmin.fieldsets analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields + # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets + # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) + + # lists in filter_horizontal are not sorted properly, sort them + # by first_name + def formfield_for_manytomany(self, db_field, request, **kwargs): + if db_field.name in ("other_contacts",): + kwargs["queryset"] = models.Contact.objects.all().order_by("first_name") # Sort contacts + return super().formfield_for_manytomany(db_field, request, **kwargs) def get_readonly_fields(self, request, obj=None): return DomainInformationAdmin.get_readonly_fields(self, request, obj=None) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 3b9f19a49..1e3f53563 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -47,4 +47,164 @@ function openInNewTab(el, removeAttribute = false){ } prepareDjangoAdmin(); -})(); \ No newline at end of file +})(); + +/** + * An IIFE to listen to changes on filter_horizontal and enable or disable the change/delete/view buttons as applicable + * + */ +(function extendFilterHorizontalWidgets() { + // Grab a list of our custom filter_horizontal widgets + let filterHorizontalList = []; + checkElementThenAddToList('id_other_contacts_to', filterHorizontalList, 0); + checkElementThenAddToList('id_domain_info-0-other_contacts_to', filterHorizontalList, 0); + checkElementThenAddToList('id_current_websites_to', filterHorizontalList, 0); + checkElementThenAddToList('id_alternative_domains_to', filterHorizontalList, 0); +})(); + +// Function to check for the existence of the element +function checkElementThenAddToList(id, listOfElements, attempts) { + let dynamicElement = document.getElementById(id); + attempts++; + + if (attempts < 6) { + if ((dynamicElement !== null)) { + // Element found, handle it + // Add an event listener on the element + // Add disabled buttons on the element's great-grandparent + customizeSelectElement(dynamicElement, id); + } else { + // Element not found, check again after a delay + setTimeout(() => checkElementThenAddToList(id, listOfElements, attempts), 1000); // Check every 1000 milliseconds (1 second) + } + } +} + +function createAndCustomizeLink(selectEl, selectElId, className, title, imgSrc, imgAlt, dataMappings, dataPopup, position) { + // Create a link element + var link = document.createElement('a'); + + // Set class attribute for the link + link.className = className; + + // Set id + // Add 'change_' to the beginning of the string + let modifiedLinkString = className.split('-')[0] + '_' + selectElId; + // Remove '_to' from the end of the string + modifiedLinkString = modifiedLinkString.replace('_to', ''); + link.id = modifiedLinkString; + + // Set data-href-template + for (const [idPattern, template] of Object.entries(dataMappings)) { + if (selectElId.includes(idPattern)) { + link.setAttribute('data-href-template', template); + break; // Stop checking once a match is found + } + } + + if (dataPopup) + link.setAttribute('data-popup', 'yes'); + + link.title = title; + + // Create an 'img' element + var img = document.createElement('img'); + + // Set attributes for the new image + img.src = imgSrc; + img.alt = imgAlt; + + // Append the image to the link + link.appendChild(img); + + // Insert the link at the specified position + selectEl.closest('.related-widget-wrapper').insertBefore(link, selectEl.closest('.related-widget-wrapper').children[position]); + + // Return the link, which we'll use in the disable and enable functions + return link; +} + +function customizeSelectElement(el, elId) { + let changeLink = createAndCustomizeLink( + el, + elId, + 'related-widget-wrapper-link change-related', + 'Change selected item', + '/public/admin/img/icon-changelink.svg', + 'Change', + { + 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1', + 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', + 'alternative_domains': '/admin/registrar/draftdomain/__fk__/change/?_to_field=id&_popup=1', + }, + true, + 0 + ); + + let deleteLink = createAndCustomizeLink( + el, + elId, + 'related-widget-wrapper-link delete-related', + 'Delete selected item', + '/public/admin/img/icon-deletelink.svg', + 'Delete', + { + 'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1', + 'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', + 'alternative_domains': '/admin/registrar/draftdomain/__fk__/delete/?_to_field=id&_popup=1', + }, + true, + 2 + ); + + let viewLink = createAndCustomizeLink( + el, + elId, + 'related-widget-wrapper-link view-related', + 'View selected item', + '/public/admin/img/icon-viewlink.svg', + 'View', + { + 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id', + 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id', + 'alternative_domains': '/admin/registrar/draftdomain/__fk__/change/?_to_field=id', + }, + false, + 3 + ); + + el.addEventListener('click', function(event) { + // Access the target element that was clicked + var clickedElement = event.target; + + // If one item is selected enable buttons, otherwise disable them + if (el.selectedOptions.length === 1) { + enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, clickedElement.value); + } else { + disableRelatedWidgetButtons(changeLink, deleteLink, viewLink); + } + + }); + + // Disable buttons when the selectors are interated with (items are moved from one column to the other) + let selectorButtons = []; + selectorButtons.push(el.closest(".selector").querySelector(".selector-chooseall")); + selectorButtons.push(el.closest(".selector").querySelector(".selector-add")); + selectorButtons.push(el.closest(".selector").querySelector(".selector-remove")); + + selectorButtons.forEach((selector) => { + selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)}); + }); +} + +function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) { + changeLink.removeAttribute('href'); + deleteLink.removeAttribute('href'); + viewLink.removeAttribute('href'); +} + +function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk) { + changeLink.setAttribute('href', changeLink.getAttribute('data-href-template').replace('__fk__', elementPk)); + deleteLink.setAttribute('href', deleteLink.getAttribute('data-href-template').replace('__fk__', elementPk)); + viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk)); +} diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index dcdd29e2f..c0884c912 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -18,6 +18,7 @@ + {% endblock %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index 6c401ad72..c4461d07f 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -1,11 +1,6 @@ {% extends 'admin/change_form.html' %} {% load i18n static %} -{% block extrahead %} -{{ block.super }} - -{% endblock %} - {% block field_sets %}
From 7d36e4399ea6e8bc66c006b0fd140fff77b03b27 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 1 Dec 2023 18:53:52 -0500 Subject: [PATCH 05/17] format for linting --- src/registrar/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 285de25a5..f79e709fb 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -458,7 +458,7 @@ class DomainInformationAdmin(ListHeaderAdmin): # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) - + # lists in filter_horizontal are not sorted properly, sort them # by first_name def formfield_for_manytomany(self, db_field, request, **kwargs): @@ -742,7 +742,7 @@ class DomainInformationInline(admin.StackedInline): # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets # to activate the edit/delete/view buttons filter_horizontal = ("other_contacts",) - + # lists in filter_horizontal are not sorted properly, sort them # by first_name def formfield_for_manytomany(self, db_field, request, **kwargs): From 4e684a7cad88151267c83841b528ab3937c31fd8 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 4 Dec 2023 10:24:21 -0500 Subject: [PATCH 06/17] Add a click listener on the 'from' list --- src/registrar/assets/js/get-gov-admin.js | 149 ++++++++++++----------- 1 file changed, 79 insertions(+), 70 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 1e3f53563..37b0f4900 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -80,6 +80,76 @@ function checkElementThenAddToList(id, listOfElements, attempts) { } } +function customizeSelectElement(el, elId) { + let changeLink = createAndCustomizeLink( + el, + elId, + 'related-widget-wrapper-link change-related', + 'Change selected item', + '/public/admin/img/icon-changelink.svg', + 'Change', + { + 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1', + 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', + 'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', + }, + true, + 0 + ); + + let deleteLink = createAndCustomizeLink( + el, + elId, + 'related-widget-wrapper-link delete-related', + 'Delete selected item', + '/public/admin/img/icon-deletelink.svg', + 'Delete', + { + 'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1', + 'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', + 'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', + }, + true, + 2 + ); + + let viewLink = createAndCustomizeLink( + el, + elId, + 'related-widget-wrapper-link view-related', + 'View selected item', + '/public/admin/img/icon-viewlink.svg', + 'View', + { + 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id', + 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id', + 'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id', + }, + false, + 3 + ); + + let fromList = el.closest('.selector').querySelector(".selector-available select"); + + fromList.addEventListener('click', function(event) { + handleSelectClick(event, fromList, changeLink, deleteLink, viewLink); + }); + + el.addEventListener('click', function(event) { + handleSelectClick(event, el, changeLink, deleteLink, viewLink); + }); + + // Disable buttons when the selectors are interated with (items are moved from one column to the other) + let selectorButtons = []; + selectorButtons.push(el.closest(".selector").querySelector(".selector-chooseall")); + selectorButtons.push(el.closest(".selector").querySelector(".selector-add")); + selectorButtons.push(el.closest(".selector").querySelector(".selector-remove")); + + selectorButtons.forEach((selector) => { + selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)}); + }); +} + function createAndCustomizeLink(selectEl, selectElId, className, title, imgSrc, imgAlt, dataMappings, dataPopup, position) { // Create a link element var link = document.createElement('a'); @@ -124,77 +194,16 @@ function createAndCustomizeLink(selectEl, selectElId, className, title, imgSrc, return link; } -function customizeSelectElement(el, elId) { - let changeLink = createAndCustomizeLink( - el, - elId, - 'related-widget-wrapper-link change-related', - 'Change selected item', - '/public/admin/img/icon-changelink.svg', - 'Change', - { - 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1', - 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', - 'alternative_domains': '/admin/registrar/draftdomain/__fk__/change/?_to_field=id&_popup=1', - }, - true, - 0 - ); +function handleSelectClick(event, selectElement, changeLink, deleteLink, viewLink) { + // Access the target element that was clicked + var clickedElement = event.target; - let deleteLink = createAndCustomizeLink( - el, - elId, - 'related-widget-wrapper-link delete-related', - 'Delete selected item', - '/public/admin/img/icon-deletelink.svg', - 'Delete', - { - 'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1', - 'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', - 'alternative_domains': '/admin/registrar/draftdomain/__fk__/delete/?_to_field=id&_popup=1', - }, - true, - 2 - ); - - let viewLink = createAndCustomizeLink( - el, - elId, - 'related-widget-wrapper-link view-related', - 'View selected item', - '/public/admin/img/icon-viewlink.svg', - 'View', - { - 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id', - 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id', - 'alternative_domains': '/admin/registrar/draftdomain/__fk__/change/?_to_field=id', - }, - false, - 3 - ); - - el.addEventListener('click', function(event) { - // Access the target element that was clicked - var clickedElement = event.target; - - // If one item is selected enable buttons, otherwise disable them - if (el.selectedOptions.length === 1) { - enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, clickedElement.value); - } else { - disableRelatedWidgetButtons(changeLink, deleteLink, viewLink); - } - - }); - - // Disable buttons when the selectors are interated with (items are moved from one column to the other) - let selectorButtons = []; - selectorButtons.push(el.closest(".selector").querySelector(".selector-chooseall")); - selectorButtons.push(el.closest(".selector").querySelector(".selector-add")); - selectorButtons.push(el.closest(".selector").querySelector(".selector-remove")); - - selectorButtons.forEach((selector) => { - selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)}); - }); + // If one item is selected, enable buttons; otherwise, disable them + if (selectElement.selectedOptions.length === 1) { + enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, clickedElement.value); + } else { + disableRelatedWidgetButtons(changeLink, deleteLink, viewLink); + } } function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) { From 6eabb824ea98bbadee6a4cdf2ad19cf26d8bc6ce Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 11:13:17 -0500 Subject: [PATCH 07/17] update to check both lists when enabling/disabling buttons --- src/registrar/assets/js/get-gov-admin.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 37b0f4900..ea357ed7d 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -132,11 +132,11 @@ function customizeSelectElement(el, elId) { let fromList = el.closest('.selector').querySelector(".selector-available select"); fromList.addEventListener('click', function(event) { - handleSelectClick(event, fromList, changeLink, deleteLink, viewLink); + handleSelectClick(event, fromList, el, changeLink, deleteLink, viewLink); }); el.addEventListener('click', function(event) { - handleSelectClick(event, el, changeLink, deleteLink, viewLink); + handleSelectClick(event, el, fromList, changeLink, deleteLink, viewLink); }); // Disable buttons when the selectors are interated with (items are moved from one column to the other) @@ -194,12 +194,12 @@ function createAndCustomizeLink(selectEl, selectElId, className, title, imgSrc, return link; } -function handleSelectClick(event, selectElement, changeLink, deleteLink, viewLink) { +function handleSelectClick(event, selectElement, relatedSelectElement, changeLink, deleteLink, viewLink) { // Access the target element that was clicked var clickedElement = event.target; // If one item is selected, enable buttons; otherwise, disable them - if (selectElement.selectedOptions.length === 1) { + if (selectElement.selectedOptions.length + relatedSelectElement.selectedOptions.length === 1) { enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, clickedElement.value); } else { disableRelatedWidgetButtons(changeLink, deleteLink, viewLink); From 55d5fce00492e2aad65450e68c590d17c443e603 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 12:22:04 -0500 Subject: [PATCH 08/17] updates for dealing with multiple selects together --- src/registrar/assets/js/get-gov-admin.js | 97 +++++++++++++++--------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index ea357ed7d..12f3f24e1 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -54,36 +54,42 @@ function openInNewTab(el, removeAttribute = false){ * */ (function extendFilterHorizontalWidgets() { - // Grab a list of our custom filter_horizontal widgets - let filterHorizontalList = []; - checkElementThenAddToList('id_other_contacts_to', filterHorizontalList, 0); - checkElementThenAddToList('id_domain_info-0-other_contacts_to', filterHorizontalList, 0); - checkElementThenAddToList('id_current_websites_to', filterHorizontalList, 0); - checkElementThenAddToList('id_alternative_domains_to', filterHorizontalList, 0); + // Initialize custom filter_horizontal widgets; each widget has a "from" select list + // and a "to" select list; initialization is based off of the presence of the + // "to" select list + checkToListThenInitWidget('id_other_contacts_to', 0); + checkToListThenInitWidget('id_domain_info-0-other_contacts_to', 0); + checkToListThenInitWidget('id_current_websites_to', 0); + checkToListThenInitWidget('id_alternative_domains_to', 0); })(); -// Function to check for the existence of the element -function checkElementThenAddToList(id, listOfElements, attempts) { - let dynamicElement = document.getElementById(id); +// Function to check for the existence of the "to" select list element in the DOM, and if and when found, +// initialize the associated widget +function checkToListThenInitWidget(toListId, attempts) { + let toList = document.getElementById(toListId); attempts++; if (attempts < 6) { - if ((dynamicElement !== null)) { - // Element found, handle it + if ((toList !== null)) { + // toList found, handle it // Add an event listener on the element // Add disabled buttons on the element's great-grandparent - customizeSelectElement(dynamicElement, id); + initializeWidgetOnToList(toList, toListId); } else { // Element not found, check again after a delay - setTimeout(() => checkElementThenAddToList(id, listOfElements, attempts), 1000); // Check every 1000 milliseconds (1 second) + setTimeout(() => checkToListThenInitWidget(toListId, attempts), 1000); // Check every 1000 milliseconds (1 second) } } } -function customizeSelectElement(el, elId) { +// Initialize the widget: +// add related buttons to the widget for edit, delete and view +// add event listeners on the from list, the to list, and selector buttons which either enable or disable the related buttons +function initializeWidgetOnToList(toList, toListId) { + // create the change button let changeLink = createAndCustomizeLink( - el, - elId, + toList, + toListId, 'related-widget-wrapper-link change-related', 'Change selected item', '/public/admin/img/icon-changelink.svg', @@ -97,9 +103,10 @@ function customizeSelectElement(el, elId) { 0 ); + // create the delete button let deleteLink = createAndCustomizeLink( - el, - elId, + toList, + toListId, 'related-widget-wrapper-link delete-related', 'Delete selected item', '/public/admin/img/icon-deletelink.svg', @@ -113,9 +120,10 @@ function customizeSelectElement(el, elId) { 2 ); + // create the view button let viewLink = createAndCustomizeLink( - el, - elId, + toList, + toListId, 'related-widget-wrapper-link view-related', 'View selected item', '/public/admin/img/icon-viewlink.svg', @@ -129,28 +137,38 @@ function customizeSelectElement(el, elId) { 3 ); - let fromList = el.closest('.selector').querySelector(".selector-available select"); + // identify the fromList element in the DOM + let fromList = toList.closest('.selector').querySelector(".selector-available select"); fromList.addEventListener('click', function(event) { - handleSelectClick(event, fromList, el, changeLink, deleteLink, viewLink); + handleSelectClick(event, fromList, toList, changeLink, deleteLink, viewLink); }); - el.addEventListener('click', function(event) { - handleSelectClick(event, el, fromList, changeLink, deleteLink, viewLink); + toList.addEventListener('click', function(event) { + handleSelectClick(event, toList, fromList, changeLink, deleteLink, viewLink); }); - // Disable buttons when the selectors are interated with (items are moved from one column to the other) + // Disable buttons when the selectors are interacted with (items are moved from one column to the other) let selectorButtons = []; - selectorButtons.push(el.closest(".selector").querySelector(".selector-chooseall")); - selectorButtons.push(el.closest(".selector").querySelector(".selector-add")); - selectorButtons.push(el.closest(".selector").querySelector(".selector-remove")); + selectorButtons.push(toList.closest(".selector").querySelector(".selector-chooseall")); + selectorButtons.push(toList.closest(".selector").querySelector(".selector-add")); + selectorButtons.push(toList.closest(".selector").querySelector(".selector-remove")); selectorButtons.forEach((selector) => { selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)}); }); } -function createAndCustomizeLink(selectEl, selectElId, className, title, imgSrc, imgAlt, dataMappings, dataPopup, position) { +// create and customize the button, then add to the DOM, relative to the toList +// toList - the element in the DOM for the toList +// toListId - the ID of the element in the DOM +// className - className to add to the created link +// imgSrc - the img.src for the created link +// imgAlt - the img.alt for the created link +// dataMappings - dictionary which relates toListId to href for the created link +// dataPopup - boolean for whether the link should produce a popup window +// position - the position of the button in the list of buttons in the related-widget-wrapper in the widget +function createAndCustomizeLink(toList, toListId, className, title, imgSrc, imgAlt, dataMappings, dataPopup, position) { // Create a link element var link = document.createElement('a'); @@ -158,15 +176,16 @@ function createAndCustomizeLink(selectEl, selectElId, className, title, imgSrc, link.className = className; // Set id - // Add 'change_' to the beginning of the string - let modifiedLinkString = className.split('-')[0] + '_' + selectElId; + // Determine function {change, link, view} from the className + // Add {function}_ to the beginning of the string + let modifiedLinkString = className.split('-')[0] + '_' + toListId; // Remove '_to' from the end of the string modifiedLinkString = modifiedLinkString.replace('_to', ''); link.id = modifiedLinkString; // Set data-href-template for (const [idPattern, template] of Object.entries(dataMappings)) { - if (selectElId.includes(idPattern)) { + if (toListId.includes(idPattern)) { link.setAttribute('data-href-template', template); break; // Stop checking once a match is found } @@ -188,19 +207,27 @@ function createAndCustomizeLink(selectEl, selectElId, className, title, imgSrc, link.appendChild(img); // Insert the link at the specified position - selectEl.closest('.related-widget-wrapper').insertBefore(link, selectEl.closest('.related-widget-wrapper').children[position]); + toList.closest('.related-widget-wrapper').insertBefore(link, toList.closest('.related-widget-wrapper').children[position]); // Return the link, which we'll use in the disable and enable functions return link; } +// Either enable or disable widget buttons when select is clicked. Select can be in either the from list +// or the to list. Action (enable or disable) taken depends on the tocal count of selected items across +// both lists. If exactly one item is selected, buttons are enabled, and urls for the buttons associated +// with the selected item function handleSelectClick(event, selectElement, relatedSelectElement, changeLink, deleteLink, viewLink) { // Access the target element that was clicked var clickedElement = event.target; - // If one item is selected, enable buttons; otherwise, disable them + // If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them if (selectElement.selectedOptions.length + relatedSelectElement.selectedOptions.length === 1) { - enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, clickedElement.value); + if (selectElement.selectedOptions.length) { + enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value); + } else { + enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, relatedSelectElement.selectedOptions[0].value); + } } else { disableRelatedWidgetButtons(changeLink, deleteLink, viewLink); } From 15489a71e2511a94f9a7cf1a9c2e902d58151c71 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 12:23:41 -0500 Subject: [PATCH 09/17] small refinement in handleSelectClick --- src/registrar/assets/js/get-gov-admin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 12f3f24e1..6a5d0a206 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -218,14 +218,14 @@ function createAndCustomizeLink(toList, toListId, className, title, imgSrc, imgA // both lists. If exactly one item is selected, buttons are enabled, and urls for the buttons associated // with the selected item function handleSelectClick(event, selectElement, relatedSelectElement, changeLink, deleteLink, viewLink) { - // Access the target element that was clicked - var clickedElement = event.target; // If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them if (selectElement.selectedOptions.length + relatedSelectElement.selectedOptions.length === 1) { if (selectElement.selectedOptions.length) { + // enable buttons for selected item in selectElement enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value); } else { + // enable buttons for selected item in relatedSelectElement enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, relatedSelectElement.selectedOptions[0].value); } } else { From 43448101077c18d799838816fd8ab84339c73a78 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 16:52:41 -0500 Subject: [PATCH 10/17] wip --- src/registrar/admin.py | 5 ++ src/registrar/assets/js/get-gov-admin.js | 99 ++++++++++++++---------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index f79e709fb..ee5d8ddc7 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -117,6 +117,11 @@ class ListHeaderAdmin(AuditedAdmin): ) return filters + # customize the help_text for all formfields for manytomany + def formfield_for_manytomany(self, db_field, request, **kwargs): + formfield = super().formfield_for_manytomany(db_field, request, **kwargs) + formfield.help_text = formfield.help_text + " If more than one value is selected, the change/delete/view actions will be disabled." + return formfield class UserContactInline(admin.StackedInline): """Edit a user's profile on the user page.""" diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 6a5d0a206..40397c1a2 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -91,9 +91,8 @@ function initializeWidgetOnToList(toList, toListId) { toList, toListId, 'related-widget-wrapper-link change-related', - 'Change selected item', - '/public/admin/img/icon-changelink.svg', 'Change', + '/public/admin/img/icon-changelink.svg', { 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1', 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', @@ -103,49 +102,53 @@ function initializeWidgetOnToList(toList, toListId) { 0 ); - // create the delete button - let deleteLink = createAndCustomizeLink( - toList, - toListId, - 'related-widget-wrapper-link delete-related', - 'Delete selected item', - '/public/admin/img/icon-deletelink.svg', - 'Delete', - { - 'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1', - 'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', - 'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', - }, - true, - 2 - ); + let hasDeletePermission = hasDeletePermissionOnPage(); + console.log("hasDeletePermission = " + hasDeletePermission); + + let deleteLink = null; + if (hasDeletePermission) { + // create the delete button + deleteLink = createAndCustomizeLink( + toList, + toListId, + 'related-widget-wrapper-link delete-related', + 'Delete', + '/public/admin/img/icon-deletelink.svg', + { + 'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1', + 'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', + 'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', + }, + true, + 2 + ); + } // create the view button let viewLink = createAndCustomizeLink( toList, toListId, 'related-widget-wrapper-link view-related', - 'View selected item', - '/public/admin/img/icon-viewlink.svg', 'View', + '/public/admin/img/icon-viewlink.svg', { 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id', 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id', 'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id', }, false, - 3 + hasDeletePermission ? 3 : 2 ); // identify the fromList element in the DOM let fromList = toList.closest('.selector').querySelector(".selector-available select"); fromList.addEventListener('click', function(event) { - handleSelectClick(event, fromList, toList, changeLink, deleteLink, viewLink); + handleSelectClick(fromList, changeLink, deleteLink, viewLink); }); toList.addEventListener('click', function(event) { - handleSelectClick(event, toList, fromList, changeLink, deleteLink, viewLink); + handleSelectClick(toList, changeLink, deleteLink, viewLink); }); // Disable buttons when the selectors are interacted with (items are moved from one column to the other) @@ -168,7 +171,7 @@ function initializeWidgetOnToList(toList, toListId) { // dataMappings - dictionary which relates toListId to href for the created link // dataPopup - boolean for whether the link should produce a popup window // position - the position of the button in the list of buttons in the related-widget-wrapper in the widget -function createAndCustomizeLink(toList, toListId, className, title, imgSrc, imgAlt, dataMappings, dataPopup, position) { +function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, position) { // Create a link element var link = document.createElement('a'); @@ -194,14 +197,15 @@ function createAndCustomizeLink(toList, toListId, className, title, imgSrc, imgA if (dataPopup) link.setAttribute('data-popup', 'yes'); - link.title = title; + link.setAttribute('title-template', action + " selected item") + link.title = link.getAttribute('title-template'); // Create an 'img' element var img = document.createElement('img'); // Set attributes for the new image img.src = imgSrc; - img.alt = imgAlt; + img.alt = action; // Append the image to the link link.appendChild(img); @@ -217,30 +221,39 @@ function createAndCustomizeLink(toList, toListId, className, title, imgSrc, imgA // or the to list. Action (enable or disable) taken depends on the tocal count of selected items across // both lists. If exactly one item is selected, buttons are enabled, and urls for the buttons associated // with the selected item -function handleSelectClick(event, selectElement, relatedSelectElement, changeLink, deleteLink, viewLink) { +function handleSelectClick(selectElement, changeLink, deleteLink, viewLink) { // If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them - if (selectElement.selectedOptions.length + relatedSelectElement.selectedOptions.length === 1) { - if (selectElement.selectedOptions.length) { - // enable buttons for selected item in selectElement - enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value); - } else { - // enable buttons for selected item in relatedSelectElement - enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, relatedSelectElement.selectedOptions[0].value); - } + if (selectElement.selectedOptions.length === 1) { + // enable buttons for selected item in selectElement + enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value, selectElement.selectedOptions[0].text); } else { disableRelatedWidgetButtons(changeLink, deleteLink, viewLink); } } -function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) { - changeLink.removeAttribute('href'); - deleteLink.removeAttribute('href'); - viewLink.removeAttribute('href'); +function hasDeletePermissionOnPage() { + return document.querySelector('.delete-related') != null } -function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk) { - changeLink.setAttribute('href', changeLink.getAttribute('data-href-template').replace('__fk__', elementPk)); - deleteLink.setAttribute('href', deleteLink.getAttribute('data-href-template').replace('__fk__', elementPk)); - viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk)); +function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) { + changeLink.removeAttribute('href'); + changeLink.setAttribute('title', changeLink.getAttribute('title-template')); + if (deleteLink) { + deleteLink.removeAttribute('href'); + deleteLink.setAttribute('title', deleteLink.getAttribute('title-template')); + } + viewLink.removeAttribute('href'); + viewLink.setAttribute('title', viewLink.getAttribute('title-template')); +} + +function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, elementText) { + changeLink.setAttribute('href', changeLink.getAttribute('data-href-template').replace('__fk__', elementPk)); + changeLink.setAttribute('title', changeLink.getAttribute('title-template').replace('selected item', elementText)); + if (deleteLink) { + deleteLink.setAttribute('href', deleteLink.getAttribute('data-href-template').replace('__fk__', elementPk)); + deleteLink.setAttribute('title', deleteLink.getAttribute('title-template').replace('selected item', elementText)); + } + viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk)); + viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText)); } From 5dcdb4a48e31afbef90537b0e8859c58e7a26ec9 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 17:39:43 -0500 Subject: [PATCH 11/17] wip --- src/registrar/assets/js/get-gov-admin.js | 27 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 40397c1a2..b2eb1bb17 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -99,11 +99,10 @@ function initializeWidgetOnToList(toList, toListId) { 'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', }, true, - 0 + true ); let hasDeletePermission = hasDeletePermissionOnPage(); - console.log("hasDeletePermission = " + hasDeletePermission); let deleteLink = null; if (hasDeletePermission) { @@ -120,7 +119,7 @@ function initializeWidgetOnToList(toList, toListId) { 'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', }, true, - 2 + false ); } @@ -137,7 +136,7 @@ function initializeWidgetOnToList(toList, toListId) { 'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id', }, false, - hasDeletePermission ? 3 : 2 + false ); // identify the fromList element in the DOM @@ -170,8 +169,8 @@ function initializeWidgetOnToList(toList, toListId) { // imgAlt - the img.alt for the created link // dataMappings - dictionary which relates toListId to href for the created link // dataPopup - boolean for whether the link should produce a popup window -// position - the position of the button in the list of buttons in the related-widget-wrapper in the widget -function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, position) { +// firstPosition - boolean indicating if link should be first position in list of links, otherwise, just before last link +function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, firstPosition) { // Create a link element var link = document.createElement('a'); @@ -210,8 +209,22 @@ function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dat // Append the image to the link link.appendChild(img); + let relatedWidgetWrapper = toList.closest('.related-widget-wrapper'); // Insert the link at the specified position - toList.closest('.related-widget-wrapper').insertBefore(link, toList.closest('.related-widget-wrapper').children[position]); + if (firstPosition) { + relatedWidgetWrapper.insertBefore(link, relatedWidgetWrapper.children[0]); + } else { + var lastChild = relatedWidgetWrapper.lastChild; + + // Check if lastChild is an element node (not a text node, comment, etc.) + if (lastChild.nodeType === 1) { + var previousSibling = lastChild.previousSibling; + while (previousSibling.nodeType !== 1) { + previousSibling = previousSibling.previousSibling; + } + relatedWidgetWrapper.insertBefore(link, previousSibling.nextSibling); + } + } // Return the link, which we'll use in the disable and enable functions return link; From 3cf7d5138e959fed1bfa8750a292f1cbe0a252cc Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 17:57:39 -0500 Subject: [PATCH 12/17] wip --- src/registrar/models/domain_application.py | 1 + src/registrar/models/domain_information.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 86b8a0f7a..babf51757 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -487,6 +487,7 @@ class DomainApplication(TimeStampedModel): "registrar.Website", blank=True, related_name="current+", + verbose_name="websites", ) approved_domain = models.OneToOneField( diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index d2bc5c53d..9f0d654b0 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -179,6 +179,7 @@ class DomainInformation(TimeStampedModel): "registrar.Contact", blank=True, related_name="contact_applications_information", + verbose_name="contacts", ) no_other_contacts_rationale = models.TextField( From 8ddd5e7b1fc3ec70c9ae41c8215bb672d6c782ac Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 18:08:26 -0500 Subject: [PATCH 13/17] updated comments --- src/registrar/assets/js/get-gov-admin.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index b2eb1bb17..53eeb22a3 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -106,7 +106,7 @@ function initializeWidgetOnToList(toList, toListId) { let deleteLink = null; if (hasDeletePermission) { - // create the delete button + // create the delete button if user has permission to delete deleteLink = createAndCustomizeLink( toList, toListId, @@ -165,11 +165,11 @@ function initializeWidgetOnToList(toList, toListId) { // toList - the element in the DOM for the toList // toListId - the ID of the element in the DOM // className - className to add to the created link +// action - the action to perform on the item {change, delete, view} // imgSrc - the img.src for the created link -// imgAlt - the img.alt for the created link // dataMappings - dictionary which relates toListId to href for the created link // dataPopup - boolean for whether the link should produce a popup window -// firstPosition - boolean indicating if link should be first position in list of links, otherwise, just before last link +// firstPosition - boolean indicating if link should be first position in list of links, otherwise, should be last link function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, firstPosition) { // Create a link element var link = document.createElement('a'); @@ -210,15 +210,19 @@ function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dat link.appendChild(img); let relatedWidgetWrapper = toList.closest('.related-widget-wrapper'); - // Insert the link at the specified position + // If firstPosition is true, insert link as the first child element if (firstPosition) { relatedWidgetWrapper.insertBefore(link, relatedWidgetWrapper.children[0]); } else { + // otherwise, insert the link prior to the last child (which is a div) + // and also prior to any text elements immediately preceding the last + // child node var lastChild = relatedWidgetWrapper.lastChild; // Check if lastChild is an element node (not a text node, comment, etc.) if (lastChild.nodeType === 1) { var previousSibling = lastChild.previousSibling; + // need to work around some white space which has been inserted into the dom while (previousSibling.nodeType !== 1) { previousSibling = previousSibling.previousSibling; } @@ -230,10 +234,9 @@ function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dat return link; } -// Either enable or disable widget buttons when select is clicked. Select can be in either the from list -// or the to list. Action (enable or disable) taken depends on the tocal count of selected items across -// both lists. If exactly one item is selected, buttons are enabled, and urls for the buttons associated -// with the selected item +// Either enable or disable widget buttons when select is clicked. Action (enable or disable) taken depends on the count +// of selected items in selectElement. If exactly one item is selected, buttons are enabled, and urls for the buttons are +// associated with the selected item function handleSelectClick(selectElement, changeLink, deleteLink, viewLink) { // If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them @@ -245,6 +248,8 @@ function handleSelectClick(selectElement, changeLink, deleteLink, viewLink) { } } +// return true if there exist elements on the page with classname of delete-related. +// presence of one or more of these elements indicates user has permission to delete function hasDeletePermissionOnPage() { return document.querySelector('.delete-related') != null } From 2c355353b6cb8ca8d21ab127a070df427ef06ef2 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 4 Dec 2023 18:37:23 -0500 Subject: [PATCH 14/17] updated helper_text in admin for multi selects; formatted for linter --- src/registrar/admin.py | 6 +++++- src/registrar/models/domain_application.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ee5d8ddc7..421e8d53c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -120,9 +120,13 @@ class ListHeaderAdmin(AuditedAdmin): # customize the help_text for all formfields for manytomany def formfield_for_manytomany(self, db_field, request, **kwargs): formfield = super().formfield_for_manytomany(db_field, request, **kwargs) - formfield.help_text = formfield.help_text + " If more than one value is selected, the change/delete/view actions will be disabled." + formfield.help_text = ( + formfield.help_text + + " If more than one value is selected, the change/delete/view actions will be disabled." + ) return formfield + class UserContactInline(admin.StackedInline): """Edit a user's profile on the user page.""" diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index babf51757..9ab3908d4 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -533,6 +533,7 @@ class DomainApplication(TimeStampedModel): "registrar.Contact", blank=True, related_name="contact_applications", + verbose_name="contacts", ) no_other_contacts_rationale = models.TextField( From e11db7fbd36853c40931d6b5d4e4eadcc32b6d66 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 5 Dec 2023 04:51:33 -0500 Subject: [PATCH 15/17] migrations for model changes --- ...inapplication_current_websites_and_more.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py diff --git a/src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py b/src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py new file mode 100644 index 000000000..1548032cd --- /dev/null +++ b/src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.7 on 2023-12-05 09:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0047_transitiondomain_address_line_transitiondomain_city_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="current_websites", + field=models.ManyToManyField( + blank=True, related_name="current+", to="registrar.website", verbose_name="websites" + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="other_contacts", + field=models.ManyToManyField( + blank=True, related_name="contact_applications", to="registrar.contact", verbose_name="contacts" + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="other_contacts", + field=models.ManyToManyField( + blank=True, + related_name="contact_applications_information", + to="registrar.contact", + verbose_name="contacts", + ), + ), + ] From 41f7a882b4562cf53cda6bb3b912cab3b52ec2af Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 5 Dec 2023 04:59:34 -0500 Subject: [PATCH 16/17] undo a migration merge conflict --- ...inapplication_current_websites_and_more.py | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py diff --git a/src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py b/src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py deleted file mode 100644 index 1548032cd..000000000 --- a/src/registrar/migrations/0048_alter_domainapplication_current_websites_and_more.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 4.2.7 on 2023-12-05 09:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("registrar", "0047_transitiondomain_address_line_transitiondomain_city_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="domainapplication", - name="current_websites", - field=models.ManyToManyField( - blank=True, related_name="current+", to="registrar.website", verbose_name="websites" - ), - ), - migrations.AlterField( - model_name="domainapplication", - name="other_contacts", - field=models.ManyToManyField( - blank=True, related_name="contact_applications", to="registrar.contact", verbose_name="contacts" - ), - ), - migrations.AlterField( - model_name="domaininformation", - name="other_contacts", - field=models.ManyToManyField( - blank=True, - related_name="contact_applications_information", - to="registrar.contact", - verbose_name="contacts", - ), - ), - ] From 7f282fd4566ffd7dcc0a8ba5f73625dc73045bd6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 5 Dec 2023 05:01:02 -0500 Subject: [PATCH 17/17] updated migrations --- ...inapplication_current_websites_and_more.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py diff --git a/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py b/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py new file mode 100644 index 000000000..4341bdad6 --- /dev/null +++ b/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.7 on 2023-12-05 10:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0048_alter_transitiondomain_status"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="current_websites", + field=models.ManyToManyField( + blank=True, related_name="current+", to="registrar.website", verbose_name="websites" + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="other_contacts", + field=models.ManyToManyField( + blank=True, related_name="contact_applications", to="registrar.contact", verbose_name="contacts" + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="other_contacts", + field=models.ManyToManyField( + blank=True, + related_name="contact_applications_information", + to="registrar.contact", + verbose_name="contacts", + ), + ), + ]