From 99fde890175608b272f9304d5bfd258d78f10b83 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Thu, 20 Jun 2024 17:16:40 -0400 Subject: [PATCH 01/40] Update domain_invitation_description.html added more detail to give better direction to analysts --- .../includes/descriptions/domain_invitation_description.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 7765b9203..3a5609ee8 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -5,8 +5,8 @@ accept and become a domain manager.

-An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. -A “received” status indicates that the recipient has logged in. +An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. +A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 9d116df83fd29f7f056504b877c599d7296c2d73 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Fri, 21 Jun 2024 21:46:31 -0400 Subject: [PATCH 02/40] Update src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- .../includes/descriptions/domain_invitation_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 3a5609ee8..03d0cd99a 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -6,7 +6,7 @@ accept and become a domain manager.

An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. -A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination. +A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From 64933e02c60c1ffffdf7f09d7c12430450076870 Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Tue, 25 Jun 2024 09:32:46 -0400 Subject: [PATCH 03/40] Update test_admin.py adding another "invited" --- src/registrar/tests/test_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 802974b6e..798dfaad8 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2820,7 +2820,7 @@ class TestDomainInvitationAdmin(TestCase): ) # Assert that the filters are added - self.assertContains(response, "invited", count=4) + self.assertContains(response, "invited", count=5) self.assertContains(response, "Invited", count=2) self.assertContains(response, "retrieved", count=2) self.assertContains(response, "Retrieved", count=2) From 2e28a0ffe5c22a059bb136bcd64ba16e4f5b0dcb Mon Sep 17 00:00:00 2001 From: Cameron Dixon Date: Tue, 25 Jun 2024 12:53:08 -0400 Subject: [PATCH 04/40] Update src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html Co-authored-by: Katherine-Osos <119689946+Katherine-Osos@users.noreply.github.com> --- .../includes/descriptions/domain_invitation_description.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html index 03d0cd99a..23c617f1c 100644 --- a/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html +++ b/src/registrar/templates/django/admin/includes/descriptions/domain_invitation_description.html @@ -5,7 +5,7 @@ accept and become a domain manager.

-An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with a "invited" status will prevent the user from signing in. +An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent. Deleting an invitation with an "invited" status will prevent the user from signing in. A “received” status indicates that the recipient has logged in. Deleting an invitation with a "received" status will stop a user's email address from showing in the domain managers list, but it will not revoke their access from the domain. To remove a user who has already signed in, go to User domain roles and delete the role for the correct domain/manager combination.

From b4214300246ecd0306bff93983370a6ee61b8312 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 17 Jul 2024 10:50:01 -0700 Subject: [PATCH 05/40] Test out copy functionality --- src/registrar/templates/admin/change_form.html | 5 +++++ .../templates/admin/change_form_object_tools.html | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/change_form.html b/src/registrar/templates/admin/change_form.html index 78dac9ac0..3efbe554e 100644 --- a/src/registrar/templates/admin/change_form.html +++ b/src/registrar/templates/admin/change_form.html @@ -1,4 +1,5 @@ {% extends "admin/change_form.html" %} +{% load static i18n %} {% comment %} Replace the Django ul markup with a div. We'll edit the child markup accordingly in change_form_object_tools {% endcomment %} {% block object-tools %} @@ -10,3 +11,7 @@ {% endif %} {% endblock %} + +{% block extrahead %} + +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 28c655bbc..48a6b101d 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -16,5 +16,12 @@

{% translate "History" %}

+ {% if opts.model_name == 'domainrequest' %} +

+ + +

+ {% endif %} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} + From 721e91aac75b77742c229dc7516d03fe865bd3b8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 17 Jul 2024 11:05:09 -0700 Subject: [PATCH 06/40] Add in js file --- src/registrar/assets/js/copy-summary.js | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/registrar/assets/js/copy-summary.js diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js new file mode 100644 index 000000000..42cedf008 --- /dev/null +++ b/src/registrar/assets/js/copy-summary.js @@ -0,0 +1,34 @@ + +document.addEventListener('DOMContentLoaded', function() { + document.getElementById('copy-summary-btn').addEventListener('click', function() { + // Generate the summary text + const organizationType = document.getElementById('id_organization_type').value; + const requestedDomain = document.getElementById('id_requested_domain').value; + const existingWebsites = Array.from(document.querySelectorAll('#id_current_websites')).map(el => el.text).join(', '); + const alternativeDomains = Array.from(document.querySelectorAll('#id_alternative_domains')).map(el => el.text).join(', '); + const submitter = document.getElementById('id_submitter').value; + const seniorOfficial = document.getElementById('id_senior_official').value; + const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n* '); + + const summary = `*Recommendation:*\n\n` + + `*Organization Type:* ${organizationType}\n\n` + + `*Requested Domain:* ${requestedDomain}\n\n` + + `*Existing website(s):*\n${existingWebsites}\n\n` + + `*Rationale:*\n\n` + + `*Alternate Domain(s):*\n* ${alternativeDomains.split(', ').join('\n* ')}\n\n` + + `*Submitter:*\n\n* ${submitter}\n\n` + + `*Senior Official:*\n\n* ${seniorOfficial}\n\n` + + `*Additional Contact(s):*\n\n* ${otherContacts}\n\n`; + + // Create a temporary textarea element to hold the summary + const textArea = document.createElement('textarea'); + textArea.value = summary; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + alert('Summary copied to clipboard!'); + alert("hello"); + }); +}); From e19bcaf55f61cc52d7f43cd38df19378d47290e8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 17 Jul 2024 11:05:42 -0700 Subject: [PATCH 07/40] Remove extra line --- src/registrar/assets/js/copy-summary.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 42cedf008..9edad3f6b 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -29,6 +29,5 @@ document.addEventListener('DOMContentLoaded', function() { document.body.removeChild(textArea); alert('Summary copied to clipboard!'); - alert("hello"); }); }); From bc334bd41fe276eb386753d931a7e284fc44567c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 22 Jul 2024 16:25:14 -0600 Subject: [PATCH 08/40] updated button format and javascript --- src/registrar/assets/js/copy-summary.js | 51 ++++++++++++------- .../admin/change_form_object_tools.html | 19 +++---- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 9edad3f6b..b268956a5 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -8,25 +8,42 @@ document.addEventListener('DOMContentLoaded', function() { const alternativeDomains = Array.from(document.querySelectorAll('#id_alternative_domains')).map(el => el.text).join(', '); const submitter = document.getElementById('id_submitter').value; const seniorOfficial = document.getElementById('id_senior_official').value; - const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n* '); + const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n '); - const summary = `*Recommendation:*\n\n` + - `*Organization Type:* ${organizationType}\n\n` + - `*Requested Domain:* ${requestedDomain}\n\n` + - `*Existing website(s):*\n${existingWebsites}\n\n` + - `*Rationale:*\n\n` + - `*Alternate Domain(s):*\n* ${alternativeDomains.split(', ').join('\n* ')}\n\n` + - `*Submitter:*\n\n* ${submitter}\n\n` + - `*Senior Official:*\n\n* ${seniorOfficial}\n\n` + - `*Additional Contact(s):*\n\n* ${otherContacts}\n\n`; + const summary = `Recommendation:
` + + `Organization Type: ${organizationType}
` + + `Requested Domain: ${requestedDomain}
` + + `Existing website(s): ${existingWebsites}
` + + `Rationale:` + + `Alternate Domain(s): ${alternativeDomains.split(', ').join('\n ')}
` + + `Submitter: ${submitter}
` + + `Senior Official: ${seniorOfficial}
` + + `Additional Contact(s): ${otherContacts}
`; - // Create a temporary textarea element to hold the summary - const textArea = document.createElement('textarea'); - textArea.value = summary; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); + // Create a temporary element + let tempElement = document.createElement('div'); + tempElement.innerHTML = summary; + // Append the element to the body + document.body.appendChild(tempElement); + + // Use the Selection and Range APIs to select the element's content + let range = document.createRange(); + range.selectNodeContents(tempElement); + let selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + // Use the Clipboard API to write the selected HTML content to the clipboard + navigator.clipboard.write([ + new ClipboardItem({ + 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) + }) + ]).then(() => { + console.log('Bold text copied to clipboard successfully!'); + }).catch(err => { + console.error('Failed to copy text: ', err); + }); + document.body.removeChild(tempElement); alert('Summary copied to clipboard!'); }); diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 48a6b101d..c2d22e9e2 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -13,15 +13,16 @@ {% else %} -

- {% translate "History" %} -

- {% if opts.model_name == 'domainrequest' %} -

- - -

- {% endif %} + {% endif %} {% endblock %} From b245dfb2fe801506b3bbc3a0713d5551be8d8330 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 10:55:02 -0600 Subject: [PATCH 09/40] fixed dom extraction --- src/registrar/assets/js/copy-summary.js | 39 ++++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index b268956a5..d12efb872 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -2,23 +2,40 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('copy-summary-btn').addEventListener('click', function() { // Generate the summary text - const organizationType = document.getElementById('id_organization_type').value; - const requestedDomain = document.getElementById('id_requested_domain').value; - const existingWebsites = Array.from(document.querySelectorAll('#id_current_websites')).map(el => el.text).join(', '); - const alternativeDomains = Array.from(document.querySelectorAll('#id_alternative_domains')).map(el => el.text).join(', '); - const submitter = document.getElementById('id_submitter').value; - const seniorOfficial = document.getElementById('id_senior_official').value; - const otherContacts = Array.from(document.querySelectorAll('#id_other_contacts option:checked')).map(el => el.text).join('\n '); + + const organizationTypeElement = document.getElementById('id_organization_type'); + const organizationType = organizationTypeElement.options[organizationTypeElement.selectedIndex].text; + + const alternativeDomainsDiv = document.querySelector('.form-row.field-alternative_domains .readonly'); + const alternativeDomainslinks = alternativeDomainsDiv.querySelectorAll('a'); + const alternativeDomains = Array.from(alternativeDomainslinks).map(link => link.textContent); + + const existingWebsitesDiv = document.querySelector('.form-row.field-current_websites .readonly'); + const existingWebsiteslinks = existingWebsitesDiv.querySelectorAll('a'); + const existingWebsites = Array.from(existingWebsiteslinks).map(link => link.textContent); + + const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly'); + const otherContactslinks = otherContactsDiv.querySelectorAll('a'); + const otherContacts = Array.from(otherContactslinks).map(link => link.textContent); + + const requestedDomainElement = document.getElementById('id_requested_domain'); + const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text; + + const submitterElement = document.getElementById('id_submitter'); + const submitter = submitterElement.options[submitterElement.selectedIndex].text; + + const seniorOfficialElement = document.getElementById('id_senior_official'); + const seniorOfficial = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; const summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + `Requested Domain: ${requestedDomain}
` + - `Existing website(s): ${existingWebsites}
` + - `Rationale:` + - `Alternate Domain(s): ${alternativeDomains.split(', ').join('\n ')}
` + + `Existing website(s): ${existingWebsites.join('
')}
` + + `Rationale:
` + + `Alternate Domain(s): ${alternativeDomains.join('
')}
` + `Submitter: ${submitter}
` + `Senior Official: ${seniorOfficial}
` + - `Additional Contact(s): ${otherContacts}
`; + `Additional Contact(s): ${otherContacts.join('
')}
`; // Create a temporary element let tempElement = document.createElement('div'); From c33a4b61f579f6e93f238521e0d39a66965e0996 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 13:42:02 -0600 Subject: [PATCH 10/40] fixed formatting --- src/registrar/assets/js/copy-summary.js | 63 ++++++++++++++++--- .../admin/includes/contact_detail_list.html | 19 +++--- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index d12efb872..c10adbffa 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -1,41 +1,86 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('copy-summary-btn').addEventListener('click', function() { - // Generate the summary text + /// Generate the summary text + //------ Organization Type const organizationTypeElement = document.getElementById('id_organization_type'); const organizationType = organizationTypeElement.options[organizationTypeElement.selectedIndex].text; + //------ Alternative Domains const alternativeDomainsDiv = document.querySelector('.form-row.field-alternative_domains .readonly'); const alternativeDomainslinks = alternativeDomainsDiv.querySelectorAll('a'); const alternativeDomains = Array.from(alternativeDomainslinks).map(link => link.textContent); + //------ Existing Websites const existingWebsitesDiv = document.querySelector('.form-row.field-current_websites .readonly'); const existingWebsiteslinks = existingWebsitesDiv.querySelectorAll('a'); const existingWebsites = Array.from(existingWebsiteslinks).map(link => link.textContent); + //------ Additional Contacts + // 1 - Create a hyperlinks map so we can display contact details and also link to the contact const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly'); - const otherContactslinks = otherContactsDiv.querySelectorAll('a'); - const otherContacts = Array.from(otherContactslinks).map(link => link.textContent); + const otherContactLinks = otherContactsDiv.querySelectorAll('a'); + const nameToUrlMap = {}; + otherContactLinks.forEach(link => { + const name = link.textContent.trim(); + const url = link.href; + nameToUrlMap[name] = url; + }); + + // 2 - Iterate through contact details and assemble html for summary + let otherContactsSummary = "" + // Get the table rows of contact details + const otherContactsTable = document.querySelector('.form-row.field-other_contacts table tbody'); + const otherContactsRows = otherContactsTable.querySelectorAll('tr'); + const bulletList = document.createElement('ul'); + otherContactsRows.forEach(contactRow => { + // Extract the contact details + const name = contactRow.querySelector('th').textContent.trim(); + const title = contactRow.querySelectorAll('td')[0].textContent.trim(); + const email = contactRow.querySelectorAll('td')[1].textContent.trim(); + const phone = contactRow.querySelectorAll('td')[2].textContent.trim(); + const url = nameToUrlMap[name] || '#'; + // Format the contact information + const listItem = document.createElement('li'); + listItem.innerH = `${name}, ${title}, ${email}, ${phone}`; + bulletList.appendChild(listItem); + }); + otherContactsSummary += bulletList.outerHTML + + //------ Requested Domains const requestedDomainElement = document.getElementById('id_requested_domain'); const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text; - const submitterElement = document.getElementById('id_submitter'); - const submitter = submitterElement.options[submitterElement.selectedIndex].text; + //------ Submitter + // Function to extract text by ID and handle missing elements + function extractTextById(id) { + const element = document.getElementById(id); + return element ? element.textContent.trim()+"," : ''; + } + // Extract the submitter name, title, email, and phone number + const submitterName = extractTextById('contact_info_name'); + const submitterTitle = extractTextById('contact_info_title'); + const submitterEmail = extractTextById('contact_info_email'); + const submitterPhone = extractTextById('contact_info_phone'); + // Format the contact information + let submitterInfo = `${submitterName} ${submitterTitle} ${submitterEmail} ${submitterPhone}`; + + //------ Senior Official const seniorOfficialElement = document.getElementById('id_senior_official'); const seniorOfficial = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; const summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + `Requested Domain: ${requestedDomain}
` + - `Existing website(s): ${existingWebsites.join('
')}
` + + `Existing website(s): ${existingWebsites.join(',')}
` + `Rationale:
` + - `Alternate Domain(s): ${alternativeDomains.join('
')}
` + - `Submitter: ${submitter}
` + + `Alternate Domain(s): ${alternativeDomains.join(',')}
` + + `Submitter: ${submitterInfo}
` + `Senior Official: ${seniorOfficial}
` + - `Additional Contact(s): ${otherContacts.join('
')}
`; + `Additional Contact(s): ${otherContactsSummary}
`; // Create a temporary element let tempElement = document.createElement('div'); diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 2ee490d76..8358e5440 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -2,25 +2,28 @@
+ {% if show_formatted_name %} {% if user.get_formatted_name %} - {{ user.get_formatted_name }}
+ {{ user.get_formatted_name }} {% else %} - None
+ None {% endif %} {% endif %} +
{% if user.has_contact_info %} {# Title #} {% if user.title %} - {{ user.title }} -
+ {{ user.title }} {% else %} - None
+ None {% endif %} +
+ {# Email #} {% if user.email %} - {{ user.email }} + {{ user.email }} {% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
{% else %} @@ -29,7 +32,7 @@ {# Phone #} {% if user.phone %} - {{ user.phone }} + {{ user.phone.as_national }}
{% else %} None
@@ -40,6 +43,6 @@ {% endif %} {% if user_verification_type %} - {{ user_verification_type }} + {{ user_verification_type }} {% endif %}
From 4666da4df709502ff283c8b0592426a4088a4eae Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 13:50:40 -0600 Subject: [PATCH 11/40] fix wierd typo --- src/registrar/assets/js/copy-summary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index c10adbffa..cd437f0a6 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -43,7 +43,7 @@ document.addEventListener('DOMContentLoaded', function() { const url = nameToUrlMap[name] || '#'; // Format the contact information const listItem = document.createElement('li'); - listItem.innerH = `${name}, ${title}, ${email}, ${phone}`; + listItem.innerHTML = `${name}, ${title}, ${email}, ${phone}`; bulletList.appendChild(listItem); }); otherContactsSummary += bulletList.outerHTML From 72fdcfdac05e834adac59e59f83ad2d1804134ef Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 14:22:10 -0600 Subject: [PATCH 12/40] more formatting fixes --- src/registrar/assets/js/copy-summary.js | 33 ++++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index cd437f0a6..824002f76 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -55,31 +55,40 @@ document.addEventListener('DOMContentLoaded', function() { //------ Submitter // Function to extract text by ID and handle missing elements - function extractTextById(id) { - const element = document.getElementById(id); - return element ? element.textContent.trim()+"," : ''; + function extractTextById(id, divElement) { + if (divElement) { + const element = divElement.querySelector(`#${id}`); + return element ? ", " + element.textContent.trim() : ''; + } + return ''; } // Extract the submitter name, title, email, and phone number - const submitterName = extractTextById('contact_info_name'); - const submitterTitle = extractTextById('contact_info_title'); - const submitterEmail = extractTextById('contact_info_email'); - const submitterPhone = extractTextById('contact_info_phone'); - // Format the contact information + const submitterDiv = document.querySelector('.form-row.field-submitter'); + const submitterNameElement = document.getElementById('id_submitter'); + const submitterName = submitterNameElement.options[submitterNameElement.selectedIndex].text; + const submitterTitle = extractTextById('contact_info_title', submitterDiv); + const submitterEmail = extractTextById('contact_info_email', submitterDiv); + const submitterPhone = extractTextById('contact_info_phone', submitterDiv); let submitterInfo = `${submitterName} ${submitterTitle} ${submitterEmail} ${submitterPhone}`; //------ Senior Official + const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official'); const seniorOfficialElement = document.getElementById('id_senior_official'); - const seniorOfficial = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; + const seniorOfficialName = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; + const seniorOfficialTitle = extractTextById('contact_info_title', seniorOfficialDiv); + const seniorOfficialEmail = extractTextById('contact_info_email', seniorOfficialDiv); + const seniorOfficialPhone = extractTextById('contact_info_phone', seniorOfficialDiv); + let seniorOfficialInfo = `${seniorOfficialName} ${seniorOfficialTitle} ${seniorOfficialEmail} ${seniorOfficialPhone}`; const summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + `Requested Domain: ${requestedDomain}
` + - `Existing website(s): ${existingWebsites.join(',')}
` + + `Existing website(s): ${existingWebsites.join(', ')}
` + `Rationale:
` + - `Alternate Domain(s): ${alternativeDomains.join(',')}
` + + `Alternate Domain(s): ${alternativeDomains.join(', ')}
` + `Submitter: ${submitterInfo}
` + - `Senior Official: ${seniorOfficial}
` + + `Senior Official: ${seniorOfficialInfo}
` + `Additional Contact(s): ${otherContactsSummary}
`; // Create a temporary element From 711c71c1147cfbc5a01d6e3c1c8240bfabd9ae98 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 23 Jul 2024 14:37:58 -0600 Subject: [PATCH 13/40] Catching edge-cases --- src/registrar/assets/js/copy-summary.js | 55 ++++++++++--------- .../admin/includes/contact_detail_list.html | 2 +- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 824002f76..de15596d2 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -1,7 +1,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('copy-summary-btn').addEventListener('click', function() { - /// Generate the summary text + /// Generate a rich HTML summary text and copy to clipboard //------ Organization Type const organizationTypeElement = document.getElementById('id_organization_type'); @@ -20,33 +20,38 @@ document.addEventListener('DOMContentLoaded', function() { //------ Additional Contacts // 1 - Create a hyperlinks map so we can display contact details and also link to the contact const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly'); - const otherContactLinks = otherContactsDiv.querySelectorAll('a'); - const nameToUrlMap = {}; - otherContactLinks.forEach(link => { - const name = link.textContent.trim(); - const url = link.href; - nameToUrlMap[name] = url; - }); + let otherContactLinks = []; + if (otherContactsDiv) { + otherContactLinks = otherContactsDiv.querySelectorAll('a'); + const nameToUrlMap = {}; + otherContactLinks.forEach(link => { + const name = link.textContent.trim(); + const url = link.href; + nameToUrlMap[name] = url; + }); + } // 2 - Iterate through contact details and assemble html for summary let otherContactsSummary = "" // Get the table rows of contact details const otherContactsTable = document.querySelector('.form-row.field-other_contacts table tbody'); - const otherContactsRows = otherContactsTable.querySelectorAll('tr'); - const bulletList = document.createElement('ul'); - otherContactsRows.forEach(contactRow => { - // Extract the contact details - const name = contactRow.querySelector('th').textContent.trim(); - const title = contactRow.querySelectorAll('td')[0].textContent.trim(); - const email = contactRow.querySelectorAll('td')[1].textContent.trim(); - const phone = contactRow.querySelectorAll('td')[2].textContent.trim(); - const url = nameToUrlMap[name] || '#'; - // Format the contact information - const listItem = document.createElement('li'); - listItem.innerHTML = `${name}, ${title}, ${email}, ${phone}`; - bulletList.appendChild(listItem); - }); - otherContactsSummary += bulletList.outerHTML + if (otherContactsTable) { + const otherContactsRows = otherContactsTable.querySelectorAll('tr'); + const bulletList = document.createElement('ul'); + otherContactsRows.forEach(contactRow => { + // Extract the contact details + const name = contactRow.querySelector('th').textContent.trim(); + const title = contactRow.querySelectorAll('td')[0].textContent.trim(); + const email = contactRow.querySelectorAll('td')[1].textContent.trim(); + const phone = contactRow.querySelectorAll('td')[2].textContent.trim(); + const url = nameToUrlMap[name] || '#'; + // Format the contact information + const listItem = document.createElement('li'); + listItem.innerHTML = `${name}, ${title}, ${email}, ${phone}`; + bulletList.appendChild(listItem); + }); + otherContactsSummary += bulletList.outerHTML + } //------ Requested Domains @@ -69,7 +74,7 @@ document.addEventListener('DOMContentLoaded', function() { const submitterTitle = extractTextById('contact_info_title', submitterDiv); const submitterEmail = extractTextById('contact_info_email', submitterDiv); const submitterPhone = extractTextById('contact_info_phone', submitterDiv); - let submitterInfo = `${submitterName} ${submitterTitle} ${submitterEmail} ${submitterPhone}`; + let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`; //------ Senior Official @@ -79,7 +84,7 @@ document.addEventListener('DOMContentLoaded', function() { const seniorOfficialTitle = extractTextById('contact_info_title', seniorOfficialDiv); const seniorOfficialEmail = extractTextById('contact_info_email', seniorOfficialDiv); const seniorOfficialPhone = extractTextById('contact_info_phone', seniorOfficialDiv); - let seniorOfficialInfo = `${seniorOfficialName} ${seniorOfficialTitle} ${seniorOfficialEmail} ${seniorOfficialPhone}`; + let seniorOfficialInfo = `${seniorOfficialName}${seniorOfficialTitle}${seniorOfficialEmail}${seniorOfficialPhone}`; const summary = `Recommendation:
` + `Organization Type: ${organizationType}
` + diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 8358e5440..418d1464b 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -32,7 +32,7 @@ {# Phone #} {% if user.phone %} - {{ user.phone.as_national }} + {{ user.phone }}
{% else %} None
From 44fea22cf976efccc1aaf7be47b544c239282c10 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 30 Jul 2024 09:13:50 -0700 Subject: [PATCH 14/40] Add new developer sandbox 'ad' infrastructure --- .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/manifests/manifest-ad.yaml | 32 ++++++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 4 files changed, 35 insertions(+) create mode 100644 ops/manifests/manifest-ad.yaml diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 3ebee59f9..70ff8ee95 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,7 @@ on: - stable - staging - development + - ad - ms - ag - litterbox diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 49e4b5e5f..b6fa0fec5 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - staging - development + - ad - ms - ag - litterbox diff --git a/ops/manifests/manifest-ad.yaml b/ops/manifests/manifest-ad.yaml new file mode 100644 index 000000000..73d6f96ff --- /dev/null +++ b/ops/manifests/manifest-ad.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-ad + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + health-check-invocation-timeout: 40 + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-ad.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-ad.app.cloud.gov + services: + - getgov-credentials + - getgov-ad-database diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 3da0a104a..8dc2587b9 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -665,6 +665,7 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-ad.app.cloud.gov", "getgov-ms.app.cloud.gov", "getgov-ag.app.cloud.gov", "getgov-litterbox.app.cloud.gov", From 53e1fe96931b584b824bf474c7c30126681d4614 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 13:31:38 -0600 Subject: [PATCH 15/40] update code for extracting "other contact" details --- src/registrar/assets/js/copy-summary.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index de15596d2..2416b07bb 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -34,16 +34,16 @@ document.addEventListener('DOMContentLoaded', function() { // 2 - Iterate through contact details and assemble html for summary let otherContactsSummary = "" // Get the table rows of contact details - const otherContactsTable = document.querySelector('.form-row.field-other_contacts table tbody'); - if (otherContactsTable) { - const otherContactsRows = otherContactsTable.querySelectorAll('tr'); - const bulletList = document.createElement('ul'); - otherContactsRows.forEach(contactRow => { - // Extract the contact details - const name = contactRow.querySelector('th').textContent.trim(); - const title = contactRow.querySelectorAll('td')[0].textContent.trim(); - const email = contactRow.querySelectorAll('td')[1].textContent.trim(); - const phone = contactRow.querySelectorAll('td')[2].textContent.trim(); + // Select all contact elements + const contacts = document.querySelectorAll('.dja-detail-list dl'); + + // Iterate through each contact element + contacts.forEach(contact => { + const name = contact.querySelector('a#contact_info_name').innerText; + const title = contact.querySelector('span#contact_info_title').innerText; + const email = contact.querySelector('span#contact_info_email').innerText; + const phone = contact.querySelector('span#contact_info_phone').innerText; + const url = nameToUrlMap[name] || '#'; // Format the contact information const listItem = document.createElement('li'); @@ -51,7 +51,7 @@ document.addEventListener('DOMContentLoaded', function() { bulletList.appendChild(listItem); }); otherContactsSummary += bulletList.outerHTML - } + }); //------ Requested Domains From 6c82ec9dc296438a352890585f1d778ca8647ab4 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 30 Jul 2024 13:33:14 -0700 Subject: [PATCH 16/40] Update for organization to have election --- src/registrar/models/domain_request.py | 8 ++++++++ src/registrar/utility/csv_export.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index a7252e16b..1ff1e501a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -215,6 +215,14 @@ class DomainRequest(TimeStampedModel): } return org_election_map + @classmethod + def get_org_label(cls, org_name: str): + # Translating the key that is given to the direct readable value + if not org_name: + return None + + return cls(org_name).label if org_name else None + class OrganizationChoicesVerbose(models.TextChoices): """ Tertiary organization choices diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 5fbd255aa..d852df5db 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -374,8 +374,9 @@ class DomainExport(BaseExport): if first_ready_on is None: first_ready_on = "(blank)" - domain_org_type = model.get("generic_org_type") - human_readable_domain_org_type = DomainRequest.OrganizationChoices.get_org_label(domain_org_type) + # organization_type has generic_org_type AND is_election + domain_org_type = model.get("organization_type") + human_readable_domain_org_type = DomainRequest.OrgChoicesElectionOffice.get_org_label(domain_org_type) domain_federal_type = model.get("federal_type") human_readable_domain_federal_type = BranchChoices.get_branch_label(domain_federal_type) domain_type = human_readable_domain_org_type From e446aa9511fa98b3655cbe79ec601c75a89d10e1 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 16:05:15 -0600 Subject: [PATCH 17/40] copy logic updates, icon updates --- src/registrar/assets/js/copy-summary.js | 12 +++++------- .../templates/admin/change_form_object_tools.html | 8 +++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js index 2416b07bb..178f61185 100644 --- a/src/registrar/assets/js/copy-summary.js +++ b/src/registrar/assets/js/copy-summary.js @@ -21,9 +21,9 @@ document.addEventListener('DOMContentLoaded', function() { // 1 - Create a hyperlinks map so we can display contact details and also link to the contact const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly'); let otherContactLinks = []; + const nameToUrlMap = {}; if (otherContactsDiv) { otherContactLinks = otherContactsDiv.querySelectorAll('a'); - const nameToUrlMap = {}; otherContactLinks.forEach(link => { const name = link.textContent.trim(); const url = link.href; @@ -38,20 +38,20 @@ document.addEventListener('DOMContentLoaded', function() { const contacts = document.querySelectorAll('.dja-detail-list dl'); // Iterate through each contact element + const bulletList = document.createElement('ul'); contacts.forEach(contact => { const name = contact.querySelector('a#contact_info_name').innerText; const title = contact.querySelector('span#contact_info_title').innerText; const email = contact.querySelector('span#contact_info_email').innerText; const phone = contact.querySelector('span#contact_info_phone').innerText; - const url = nameToUrlMap[name] || '#'; + // Format the contact information const listItem = document.createElement('li'); listItem.innerHTML = `${name}, ${title}, ${email}, ${phone}`; bulletList.appendChild(listItem); - }); - otherContactsSummary += bulletList.outerHTML }); + otherContactsSummary += bulletList.outerHTML //------ Requested Domains @@ -115,12 +115,10 @@ document.addEventListener('DOMContentLoaded', function() { 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) }) ]).then(() => { - console.log('Bold text copied to clipboard successfully!'); + console.log('Summary copied to clipboard successfully!'); }).catch(err => { console.error('Failed to copy text: ', err); }); document.body.removeChild(tempElement); - - alert('Summary copied to clipboard!'); }); }); diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index c2d22e9e2..1094f4c86 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -1,4 +1,5 @@ {% load i18n admin_urls %} +{% load i18n static %} {% comment %} Replace li with p for more semantic HTML if we have a single child {% endcomment %} {% block object-tools-items %} @@ -19,7 +20,12 @@ {% if opts.model_name == 'domainrequest' %}
  • - {% translate "Copy request summary" %} + + + + + {% translate "Copy request summary" %} +
  • {% endif %} From 0953e50bb0d01155436f1ec5ab49e971d19cbbe6 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 17:39:02 -0600 Subject: [PATCH 18/40] styling updates --- src/registrar/assets/sass/_theme/_buttons.scss | 5 +++++ src/registrar/templates/admin/change_form_object_tools.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 7fa379c0b..577c9ce29 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -128,6 +128,11 @@ a.withdraw:active { vertical-align: bottom; } +a.historylink .usa-icon { + vertical-align: middle; + margin: 0px; +} + a.usa-button--unstyled:visited { color: color('primary'); } diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 1094f4c86..03c519241 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -22,7 +22,7 @@
  • - + {% translate "Copy request summary" %} From 7b56562417f3a5c2eb5b8e8982eabf9c3092860e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 30 Jul 2024 18:45:44 -0600 Subject: [PATCH 19/40] Fixed error with javascript file, more styling --- src/registrar/assets/js/copy-summary.js | 124 ------------- src/registrar/assets/js/get-gov-admin.js | 163 +++++++++++++++++- .../assets/sass/_theme/_buttons.scss | 7 +- src/registrar/assets/sass/_theme/_links.scss | 5 + .../templates/admin/change_form.html | 4 - .../admin/change_form_object_tools.html | 6 +- 6 files changed, 165 insertions(+), 144 deletions(-) delete mode 100644 src/registrar/assets/js/copy-summary.js diff --git a/src/registrar/assets/js/copy-summary.js b/src/registrar/assets/js/copy-summary.js deleted file mode 100644 index 178f61185..000000000 --- a/src/registrar/assets/js/copy-summary.js +++ /dev/null @@ -1,124 +0,0 @@ - -document.addEventListener('DOMContentLoaded', function() { - document.getElementById('copy-summary-btn').addEventListener('click', function() { - /// Generate a rich HTML summary text and copy to clipboard - - //------ Organization Type - const organizationTypeElement = document.getElementById('id_organization_type'); - const organizationType = organizationTypeElement.options[organizationTypeElement.selectedIndex].text; - - //------ Alternative Domains - const alternativeDomainsDiv = document.querySelector('.form-row.field-alternative_domains .readonly'); - const alternativeDomainslinks = alternativeDomainsDiv.querySelectorAll('a'); - const alternativeDomains = Array.from(alternativeDomainslinks).map(link => link.textContent); - - //------ Existing Websites - const existingWebsitesDiv = document.querySelector('.form-row.field-current_websites .readonly'); - const existingWebsiteslinks = existingWebsitesDiv.querySelectorAll('a'); - const existingWebsites = Array.from(existingWebsiteslinks).map(link => link.textContent); - - //------ Additional Contacts - // 1 - Create a hyperlinks map so we can display contact details and also link to the contact - const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly'); - let otherContactLinks = []; - const nameToUrlMap = {}; - if (otherContactsDiv) { - otherContactLinks = otherContactsDiv.querySelectorAll('a'); - otherContactLinks.forEach(link => { - const name = link.textContent.trim(); - const url = link.href; - nameToUrlMap[name] = url; - }); - } - - // 2 - Iterate through contact details and assemble html for summary - let otherContactsSummary = "" - // Get the table rows of contact details - // Select all contact elements - const contacts = document.querySelectorAll('.dja-detail-list dl'); - - // Iterate through each contact element - const bulletList = document.createElement('ul'); - contacts.forEach(contact => { - const name = contact.querySelector('a#contact_info_name').innerText; - const title = contact.querySelector('span#contact_info_title').innerText; - const email = contact.querySelector('span#contact_info_email').innerText; - const phone = contact.querySelector('span#contact_info_phone').innerText; - const url = nameToUrlMap[name] || '#'; - - // Format the contact information - const listItem = document.createElement('li'); - listItem.innerHTML = `${name}, ${title}, ${email}, ${phone}`; - bulletList.appendChild(listItem); - }); - otherContactsSummary += bulletList.outerHTML - - - //------ Requested Domains - const requestedDomainElement = document.getElementById('id_requested_domain'); - const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text; - - //------ Submitter - // Function to extract text by ID and handle missing elements - function extractTextById(id, divElement) { - if (divElement) { - const element = divElement.querySelector(`#${id}`); - return element ? ", " + element.textContent.trim() : ''; - } - return ''; - } - // Extract the submitter name, title, email, and phone number - const submitterDiv = document.querySelector('.form-row.field-submitter'); - const submitterNameElement = document.getElementById('id_submitter'); - const submitterName = submitterNameElement.options[submitterNameElement.selectedIndex].text; - const submitterTitle = extractTextById('contact_info_title', submitterDiv); - const submitterEmail = extractTextById('contact_info_email', submitterDiv); - const submitterPhone = extractTextById('contact_info_phone', submitterDiv); - let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`; - - - //------ Senior Official - const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official'); - const seniorOfficialElement = document.getElementById('id_senior_official'); - const seniorOfficialName = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; - const seniorOfficialTitle = extractTextById('contact_info_title', seniorOfficialDiv); - const seniorOfficialEmail = extractTextById('contact_info_email', seniorOfficialDiv); - const seniorOfficialPhone = extractTextById('contact_info_phone', seniorOfficialDiv); - let seniorOfficialInfo = `${seniorOfficialName}${seniorOfficialTitle}${seniorOfficialEmail}${seniorOfficialPhone}`; - - const summary = `Recommendation:
    ` + - `Organization Type: ${organizationType}
    ` + - `Requested Domain: ${requestedDomain}
    ` + - `Existing website(s): ${existingWebsites.join(', ')}
    ` + - `Rationale:
    ` + - `Alternate Domain(s): ${alternativeDomains.join(', ')}
    ` + - `Submitter: ${submitterInfo}
    ` + - `Senior Official: ${seniorOfficialInfo}
    ` + - `Additional Contact(s): ${otherContactsSummary}
    `; - - // Create a temporary element - let tempElement = document.createElement('div'); - tempElement.innerHTML = summary; - // Append the element to the body - document.body.appendChild(tempElement); - - // Use the Selection and Range APIs to select the element's content - let range = document.createRange(); - range.selectNodeContents(tempElement); - let selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - // Use the Clipboard API to write the selected HTML content to the clipboard - navigator.clipboard.write([ - new ClipboardItem({ - 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) - }) - ]).then(() => { - console.log('Summary copied to clipboard successfully!'); - }).catch(err => { - console.error('Failed to copy text: ', err); - }); - document.body.removeChild(tempElement); - }); -}); diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index d8bc21899..fe25b2feb 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -207,6 +207,7 @@ function addOrRemoveSessionBoolean(name, add){ })(); + /** An IIFE for pages in DjangoAdmin that use a clipboard button */ (function (){ @@ -233,21 +234,17 @@ function addOrRemoveSessionBoolean(name, add){ buttonIcon.setAttribute('xlink:href', baseHref + '#check'); // Change the button text - nearestSpan = button.querySelector("span") + let nearestSpan = button.querySelector("span") + let original_text = nearestSpan.innerText nearestSpan.innerText = "Copied to clipboard" setTimeout(function() { // Change back to the copy icon buttonIcon.setAttribute('xlink:href', currentHref); - if (button.classList.contains('usa-button__small-text')) { - nearestSpan.innerText = "Copy email"; - } else { - nearestSpan.innerText = "Copy"; - } + nearestSpan.innerText = original_text; }, 2000); } - }).catch(function(error) { console.error('Clipboard copy failed', error); }); @@ -605,3 +602,155 @@ function initializeWidgetOnList(list, parentId) { } } })(); + + +/** An IIFE for copy summary button (appears in DomainRegistry models) +*/ +(function (){ + const copyButton = document.getElementById('copy-summary-btn'); + + if (copyButton) { + copyButton.addEventListener('click', function() { + /// Generate a rich HTML summary text and copy to clipboard + + //------ Organization Type + const organizationTypeElement = document.getElementById('id_organization_type'); + const organizationType = organizationTypeElement.options[organizationTypeElement.selectedIndex].text; + + //------ Alternative Domains + const alternativeDomainsDiv = document.querySelector('.form-row.field-alternative_domains .readonly'); + const alternativeDomainslinks = alternativeDomainsDiv.querySelectorAll('a'); + const alternativeDomains = Array.from(alternativeDomainslinks).map(link => link.textContent); + + //------ Existing Websites + const existingWebsitesDiv = document.querySelector('.form-row.field-current_websites .readonly'); + const existingWebsiteslinks = existingWebsitesDiv.querySelectorAll('a'); + const existingWebsites = Array.from(existingWebsiteslinks).map(link => link.textContent); + + //------ Additional Contacts + // 1 - Create a hyperlinks map so we can display contact details and also link to the contact + const otherContactsDiv = document.querySelector('.form-row.field-other_contacts .readonly'); + let otherContactLinks = []; + const nameToUrlMap = {}; + if (otherContactsDiv) { + otherContactLinks = otherContactsDiv.querySelectorAll('a'); + otherContactLinks.forEach(link => { + const name = link.textContent.trim(); + const url = link.href; + nameToUrlMap[name] = url; + }); + } + + // 2 - Iterate through contact details and assemble html for summary + let otherContactsSummary = "" + // Get the table rows of contact details + // Select all contact elements + const contacts = document.querySelectorAll('.dja-detail-list dl'); + + // Iterate through each contact element + const bulletList = document.createElement('ul'); + contacts.forEach(contact => { + const name = contact.querySelector('a#contact_info_name').innerText; + const title = contact.querySelector('span#contact_info_title').innerText; + const email = contact.querySelector('span#contact_info_email').innerText; + const phone = contact.querySelector('span#contact_info_phone').innerText; + const url = nameToUrlMap[name] || '#'; + + // Format the contact information + const listItem = document.createElement('li'); + listItem.innerHTML = `${name}, ${title}, ${email}, ${phone}`; + bulletList.appendChild(listItem); + }); + otherContactsSummary += bulletList.outerHTML + + + //------ Requested Domains + const requestedDomainElement = document.getElementById('id_requested_domain'); + const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text; + + //------ Submitter + // Function to extract text by ID and handle missing elements + function extractTextById(id, divElement) { + if (divElement) { + const element = divElement.querySelector(`#${id}`); + return element ? ", " + element.textContent.trim() : ''; + } + return ''; + } + // Extract the submitter name, title, email, and phone number + const submitterDiv = document.querySelector('.form-row.field-submitter'); + const submitterNameElement = document.getElementById('id_submitter'); + const submitterName = submitterNameElement.options[submitterNameElement.selectedIndex].text; + const submitterTitle = extractTextById('contact_info_title', submitterDiv); + const submitterEmail = extractTextById('contact_info_email', submitterDiv); + const submitterPhone = extractTextById('contact_info_phone', submitterDiv); + let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`; + + + //------ Senior Official + const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official'); + const seniorOfficialElement = document.getElementById('id_senior_official'); + const seniorOfficialName = seniorOfficialElement.options[seniorOfficialElement.selectedIndex].text; + const seniorOfficialTitle = extractTextById('contact_info_title', seniorOfficialDiv); + const seniorOfficialEmail = extractTextById('contact_info_email', seniorOfficialDiv); + const seniorOfficialPhone = extractTextById('contact_info_phone', seniorOfficialDiv); + let seniorOfficialInfo = `${seniorOfficialName}${seniorOfficialTitle}${seniorOfficialEmail}${seniorOfficialPhone}`; + + const summary = `Recommendation:
    ` + + `Organization Type: ${organizationType}
    ` + + `Requested Domain: ${requestedDomain}
    ` + + `Existing website(s): ${existingWebsites.join(', ')}
    ` + + `Rationale:
    ` + + `Alternate Domain(s): ${alternativeDomains.join(', ')}
    ` + + `Submitter: ${submitterInfo}
    ` + + `Senior Official: ${seniorOfficialInfo}
    ` + + `Additional Contact(s): ${otherContactsSummary}
    `; + + // Create a temporary element + let tempElement = document.createElement('div'); + tempElement.innerHTML = summary; + // Append the element to the body + document.body.appendChild(tempElement); + + // Use the Selection and Range APIs to select the element's content + let range = document.createRange(); + range.selectNodeContents(tempElement); + let selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + // Use the Clipboard API to write the selected HTML content to the clipboard + navigator.clipboard.write([ + new ClipboardItem({ + 'text/html': new Blob([tempElement.innerHTML], { type: 'text/html' }) + }) + ]).then(() => { + // Change the icon to a checkmark on successful copy + let buttonIcon = copyButton.querySelector('.usa-button__clipboard use'); + if (buttonIcon) { + let currentHref = buttonIcon.getAttribute('xlink:href'); + let baseHref = currentHref.split('#')[0]; + + // Append the new icon reference + buttonIcon.setAttribute('xlink:href', baseHref + '#check'); + + // Change the button text + nearestSpan = copyButton.querySelector("span") + original_text = nearestSpan.innerText + nearestSpan.innerText = "Copied to clipboard" + + setTimeout(function() { + // Change back to the copy icon + buttonIcon.setAttribute('xlink:href', currentHref); + nearestSpan.innerText = original_text + }, 2000); + + } + console.log('Summary copied to clipboard successfully!'); + }).catch(err => { + console.error('Failed to copy text: ', err); + }); + document.body.removeChild(tempElement); + }); + } +})(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 577c9ce29..d246366d8 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -128,11 +128,6 @@ a.withdraw:active { vertical-align: bottom; } -a.historylink .usa-icon { - vertical-align: middle; - margin: 0px; -} - a.usa-button--unstyled:visited { color: color('primary'); } @@ -218,4 +213,4 @@ a.usa-button--unstyled:visited { .margin-right-neg-4px { margin-right: -4px; -} +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_links.scss b/src/registrar/assets/sass/_theme/_links.scss index e9b71733a..35546face 100644 --- a/src/registrar/assets/sass/_theme/_links.scss +++ b/src/registrar/assets/sass/_theme/_links.scss @@ -15,3 +15,8 @@ margin-right: units(0.5); } } + +.modelLink-icon { + margin-bottom: 2px; + vertical-align: middle; +} \ No newline at end of file diff --git a/src/registrar/templates/admin/change_form.html b/src/registrar/templates/admin/change_form.html index 3efbe554e..f2ac7f2df 100644 --- a/src/registrar/templates/admin/change_form.html +++ b/src/registrar/templates/admin/change_form.html @@ -10,8 +10,4 @@ {% endblock %} {% endif %} -{% endblock %} - -{% block extrahead %} - {% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 03c519241..ec50f493a 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -16,12 +16,12 @@ {% else %}