From d6d68c401d47f9151fe3b458e658e0767b956e3b Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 10 Mar 2025 18:52:14 -0400 Subject: [PATCH 01/77] initial ds data redesign --- .../assets/src/js/getgov/domain-dsdata.js | 27 - .../assets/src/js/getgov/form-dsdata.js | 472 ++++++++++++++++++ .../assets/src/js/getgov/form-helpers.js | 5 + .../assets/src/js/getgov/formset-forms.js | 36 +- src/registrar/assets/src/js/getgov/main.js | 9 +- src/registrar/forms/domain.py | 2 +- src/registrar/templates/domain_dsdata.html | 416 +++++++++++---- src/registrar/views/domain.py | 32 +- 8 files changed, 804 insertions(+), 195 deletions(-) delete mode 100644 src/registrar/assets/src/js/getgov/domain-dsdata.js create mode 100644 src/registrar/assets/src/js/getgov/form-dsdata.js diff --git a/src/registrar/assets/src/js/getgov/domain-dsdata.js b/src/registrar/assets/src/js/getgov/domain-dsdata.js deleted file mode 100644 index 14132d812..000000000 --- a/src/registrar/assets/src/js/getgov/domain-dsdata.js +++ /dev/null @@ -1,27 +0,0 @@ -import { submitForm } from './form-helpers.js'; - -export function initDomainDSData() { - document.addEventListener('DOMContentLoaded', function() { - let domain_dsdata_page = document.getElementById("domain-dsdata"); - if (domain_dsdata_page) { - const override_button = document.getElementById("disable-override-click-button"); - const cancel_button = document.getElementById("btn-cancel-click-button"); - const cancel_close_button = document.getElementById("btn-cancel-click-close-button"); - if (override_button) { - override_button.addEventListener("click", function () { - submitForm("disable-override-click-form"); - }); - } - if (cancel_button) { - cancel_button.addEventListener("click", function () { - submitForm("btn-cancel-click-form"); - }); - } - if (cancel_close_button) { - cancel_close_button.addEventListener("click", function () { - submitForm("btn-cancel-click-form"); - }); - } - } - }); -} \ No newline at end of file diff --git a/src/registrar/assets/src/js/getgov/form-dsdata.js b/src/registrar/assets/src/js/getgov/form-dsdata.js new file mode 100644 index 000000000..e9be4135e --- /dev/null +++ b/src/registrar/assets/src/js/getgov/form-dsdata.js @@ -0,0 +1,472 @@ +import { showElement, hideElement, scrollToElement } from './helpers'; +import { removeErrorsFromElement, removeFormErrors } from './form-helpers'; + +export class DSDataForm { + constructor() { + this.addDSDataButton = document.getElementById('dsdata-add-button'); + this.addDSDataForm = document.querySelector('.add-dsdata-form'); + this.formChanged = false; + this.callback = null; + + // Bind event handlers to maintain 'this' context + this.handleAddFormClick = this.handleAddFormClick.bind(this); + this.handleEditClick = this.handleEditClick.bind(this); + this.handleDeleteClick = this.handleDeleteClick.bind(this); + this.handleDeleteKebabClick = this.handleDeleteKebabClick.bind(this); + this.handleCancelClick = this.handleCancelClick.bind(this); + this.handleCancelAddFormClick = this.handleCancelAddFormClick.bind(this); + } + + /** + * Initialize the DSDataForm by setting up display and event listeners. + */ + init() { + this.initializeDSDataFormDisplay(); + this.initializeEventListeners(); + } + + + /** + * Determines the initial display state of the DS dara form, + * handling validation errors and setting visibility of elements accordingly. + */ + initializeDSDataFormDisplay() { + + // This check indicates that there is an Add DS Data form + // and that form has errors in it. In this case, show the form, and indicate that the form has + // changed. + if (this.addDSDataForm && this.addDSDataForm.querySelector('.usa-input--error')) { + showElement(this.addDSDataForm); + this.formChanged = true; + } + + // handle display of table view errors + // if error exists in an edit-row, make that row show, and readonly row hide + const formTable = document.getElementById('dsdata-table') + if (formTable) { + const editRows = formTable.querySelectorAll('.edit-row'); + editRows.forEach(editRow => { + if (editRow.querySelector('.usa-input--error')) { + const readOnlyRow = editRow.previousElementSibling; + this.formChanged = true; + showElement(editRow); + hideElement(readOnlyRow); + } + }) + } + + } + + /** + * Attaches event listeners to relevant UI elements for interaction handling. + */ + initializeEventListeners() { + this.addDSDataButton.addEventListener('click', this.handleAddFormClick); + + const editButtons = document.querySelectorAll('.dsdata-edit'); + editButtons.forEach(editButton => { + editButton.addEventListener('click', this.handleEditClick); + }); + + const cancelButtons = document.querySelectorAll('.dsdata-cancel'); + cancelButtons.forEach(cancelButton => { + cancelButton.addEventListener('click', this.handleCancelClick); + }); + + const cancelAddFormButtons = document.querySelectorAll('.dsdata-cancel-add-form'); + cancelAddFormButtons.forEach(cancelAddFormButton => { + cancelAddFormButton.addEventListener('click', this.handleCancelAddFormClick); + }); + + const deleteButtons = document.querySelectorAll('.dsdata-delete'); + deleteButtons.forEach(deleteButton => { + deleteButton.addEventListener('click', this.handleDeleteClick); + }); + + const deleteKebabButtons = document.querySelectorAll('.dsdata-delete-kebab'); + deleteKebabButtons.forEach(deleteKebabButton => { + deleteKebabButton.addEventListener('click', this.handleDeleteKebabClick); + }); + + const textInputs = document.querySelectorAll("input[type='text']"); + textInputs.forEach(input => { + input.addEventListener("input", () => { + this.formChanged = true; + }); + }); + + // Set event listeners on the submit buttons for the modals. Event listeners + // should execute the callback function, which has its logic updated prior + // to modal display + const unsaved_changes_modal = document.getElementById('unsaved-changes-modal'); + if (unsaved_changes_modal) { + const submitButton = document.getElementById('unsaved-changes-click-button'); + const closeButton = unsaved_changes_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } + const delete_modal = document.getElementById('delete-modal'); + if (delete_modal) { + const submitButton = document.getElementById('delete-click-button'); + const closeButton = delete_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } + const disable_dnssec_modal = document.getElementById('disable-dnssec-modal'); + if (disable_dnssec_modal) { + const submitButton = document.getElementById('disable-dnssec-click-button'); + const closeButton = disable_dnssec_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } + + } + + /** + * Executes a stored callback function if defined, otherwise logs a warning. + */ + executeCallback() { + if (this.callback) { + this.callback(); + this.callback = null; + } else { + console.warn("No callback function set."); + } + } + + /** + * Handles clicking the 'Add DS data' button, showing the form if needed. + * @param {Event} event - Click event + */ + handleAddFormClick(event) { + this.callback = () => { + console.log("handleAddFormClick callback"); + // Check if any other edit row is currently visible and hide it + document.querySelectorAll('tr.edit-row:not(.display-none)').forEach(openEditRow => { + this.resetEditRowAndFormAndCollapseEditRow(openEditRow); + }); + if (this.addDSDataForm) { + // Check if this.addDSDataForm is visible (i.e., does not have 'display-none') + if (!this.addDSDataForm.classList.contains('display-none')) { + this.resetAddDSDataForm(); + } + // show add ds data form + showElement(this.addDSDataForm); + } + }; + if (this.formChanged) { + //------- Show the unsaved changes confirmation modal + let modalTrigger = document.querySelector("#unsaved_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } + } + + /** + * Handles clicking an 'Edit' button on a readonly row, which hides the readonly row + * and displays the edit row, after performing some checks and possibly displaying modal. + * @param {Event} event - Click event + */ + handleEditClick(event) { + let editButton = event.target; + let readOnlyRow = editButton.closest('tr'); // Find the closest row + let editRow = readOnlyRow.nextElementSibling; // Get the next row + if (!editRow || !readOnlyRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + this.callback = () => { + // Check if any other edit row is currently visible and hide it + document.querySelectorAll('tr.edit-row:not(.display-none)').forEach(openEditRow => { + this.resetEditRowAndFormAndCollapseEditRow(openEditRow); + }); + // Check if this.addDSDataForm is visible (i.e., does not have 'display-none') + if (this.addDSDataForm && !this.addDSDataForm.classList.contains('display-none')) { + this.resetAddDSDataForm(); + } + // hide and show rows as appropriate + hideElement(readOnlyRow); + showElement(editRow); + }; + if (this.formChanged) { + //------- Show the unsaved changes confirmation modal + let modalTrigger = document.querySelector("#unsaved_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } + } + + /** + * Handles clicking a 'Delete' button on an edit row, which hattempts to delete the DS record + * after displaying modal. + * @param {Event} event - Click event + */ + handleDeleteClick(event) { + let deleteButton = event.target; + let editRow = deleteButton.closest('tr'); + if (!editRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + this.deleteRow(editRow); + } + + /** + * Handles clicking a 'Delete' button on a readonly row in a kebab, which attempts to delete the DS record + * after displaying modal. + * @param {Event} event - Click event + */ + handleDeleteKebabClick(event) { + let deleteKebabButton = event.target; + let accordionDiv = deleteKebabButton.closest('div'); + // hide the accordion + accordionDiv.hidden = true; + let readOnlyRow = deleteKebabButton.closest('tr'); // Find the closest row + let editRow = readOnlyRow.nextElementSibling; // Get the next row + if (!editRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + this.deleteRow(editRow); + } + + /** + * Deletes a DS record row. If there is only one DS record, prompt the user + * that they will be disabling DNSSEC. Otherwise, prompt with delete confiration. + * If deletion proceeds, the input fields are cleared, and the form is submitted. + * @param {HTMLElement} editRow - The row corresponding to the DS record being deleted. + */ + deleteRow(editRow) { + // update the callback method + this.callback = () => { + hideElement(editRow); + let deleteInput = editRow.querySelector("input[name$='-DELETE']"); + if (deleteInput) { + deleteInput.checked = true; + } + document.querySelector("form").submit(); + }; + // Check if at least 2 DS data records exist before the delete row action is taken + const thirdDSData = document.getElementById('id_form-2-key_tag') + if (thirdDSData) { + let modalTrigger = document.querySelector('#delete_trigger'); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + let modalTrigger = document.querySelector('#disable_dnssec_trigger'); + if (modalTrigger) { + modalTrigger.click(); + } + } + } + + /** + * Handles the click event on the "Cancel" button in the add DS data form. + * Resets the form fields and hides the add form section. + * @param {Event} event - Click event + */ + handleCancelAddFormClick(event) { + this.resetAddDSDataForm(); + } + + /** + * Handles the click event for the cancel button within the table form. + * + * This method identifies the edit row containing the cancel button and resets + * it to its initial state, restoring the corresponding read-only row. + * + * @param {Event} event - the click event triggered by the cancel button + */ + handleCancelClick(event) { + // get the cancel button that was clicked + let cancelButton = event.target; + // find the closest table row that contains the cancel button + let editRow = cancelButton.closest('tr'); + if (editRow) { + this.resetEditRowAndFormAndCollapseEditRow(editRow); + } else { + console.warn("Expected DOM element but did not find it"); + } + } + + /** + * Resets the edit row, restores its original values, removes validation errors, + * and collapses the edit row while making the readonly row visible again. + * @param {HTMLElement} editRow - The row that is being reset and collapsed. + */ + resetEditRowAndFormAndCollapseEditRow(editRow) { + let readOnlyRow = editRow.previousElementSibling; // Get the next row + if (!editRow || !readOnlyRow) { + console.warn("Expected DOM element but did not find it"); + return; + } + // reset the values set in editRow + this.resetInputValuesInElement(editRow); + // copy values from editRow to readOnlyRow + this.copyEditRowToReadonlyRow(editRow, readOnlyRow); + // remove errors from the editRow + removeErrorsFromElement(editRow); + // remove errors from the entire form + removeFormErrors(); + // reset formChanged + this.resetFormChanged(); + // hide and show rows as appropriate + hideElement(editRow); + showElement(readOnlyRow); + } + + /** + * Resets the 'Add DS data' form by clearing its input fields, removing errors, + * and hiding the form to return it to its initial state. + */ + resetAddDSDataForm() { + if (this.addDSDataForm) { + // reset the values set in addDSDataForm + this.resetInputValuesInElement(this.addDSDataForm); + // remove errors from the addDSDataForm + removeErrorsFromElement(this.addDSDataForm); + // remove errors from the entire form + removeFormErrors(); + // reset formChanged + this.resetFormChanged(); + // hide the addDSDataForm + hideElement(this.addDSDataForm); + } + } + + /** + * Resets all text input fields within the specified DOM element to their initial values. + * Triggers an 'input' event to ensure any event listeners update accordingly. + * @param {HTMLElement} domElement - The parent element containing text input fields to be reset. + */ + resetInputValuesInElement(domElement) { + const inputEvent = new Event('input'); + const changeEvent = new Event('change'); + // Reset text and number inputs + let inputs = domElement.querySelectorAll("input[type='text'], input[type='number']"); + inputs.forEach(input => { + // Reset input value to its initial stored value + input.value = input.dataset.initialValue; + // Dispatch input event to update any event-driven changes + input.dispatchEvent(inputEvent); + }); + // Reset select elements + let selects = domElement.querySelectorAll("select"); + selects.forEach(select => { + // Reset select value to its initial stored value + select.value = select.dataset.initialValue; + // Dispatch change event to update any event-driven changes + select.dispatchEvent(changeEvent); + }); + } + + /** + * Copies values from the editable row's text inputs into the corresponding + * readonly row cells, formatting them appropriately. + * @param {HTMLElement} editRow - The row containing editable input fields. + * @param {HTMLElement} readOnlyRow - The row where values will be displayed in a non-editable format. + */ + copyEditRowToReadonlyRow(editRow, readOnlyRow) { + let numberInput = editRow.querySelector("input[type='number']"); + let selects = editRow.querySelectorAll("select"); + let textInput = editRow.querySelector("input[type='text']"); + let tds = readOnlyRow.querySelectorAll("td"); + let updatedText = ''; + + // Copy the number input value + if (numberInput) { + tds[0].innerText = numberInput.value || ""; + } + + // Copy select values (showing the selected label instead of value) + selects.forEach((select, index) => { + let selectedOption = select.options[select.selectedIndex]; + if (tds[index + 1]) { + tds[index + 1].innerText = selectedOption ? selectedOption.text : ""; + } + }); + + // Copy the text input value + if (textInput) { + tds[3].innerText = textInput.value || ""; + } + } + + /** + * Resets the form change state. + * This method marks the form as unchanged by setting `formChanged` to false. + * It is useful for tracking whether a user has modified any form fields. + */ + resetFormChanged() { + this.formChanged = false; + } + + /** + * Removes all existing alert messages from the main content area. + * This ensures that only the latest alert is displayed to the user. + */ + resetAlerts() { + const mainContent = document.getElementById("main-content"); + if (mainContent) { + // Remove all alert elements within the main content area + mainContent.querySelectorAll(".usa-alert:not(.usa-alert--do-not-reset)").forEach(alert => alert.remove()); + } else { + console.warn("Expecting main-content DOM element"); + } + } + + /** + * Displays an alert message at the top of the main content area. + * It first removes any existing alerts before adding a new one to ensure only the latest alert is visible. + * @param {string} level - The alert level (e.g., 'error', 'success', 'warning', 'info'). + * @param {string} message - The message to display inside the alert. + */ + addAlert(level, message) { + this.resetAlerts(); // Remove any existing alerts before adding a new one + + const mainContent = document.getElementById("main-content"); + if (!mainContent) return; + + // Create a new alert div with appropriate classes based on alert level + const alertDiv = document.createElement("div"); + alertDiv.className = `usa-alert usa-alert--${level} usa-alert--slim margin-bottom-2`; + alertDiv.setAttribute("role", "alert"); // Add the role attribute + + // Create the alert body to hold the message text + const alertBody = document.createElement("div"); + alertBody.className = "usa-alert__body"; + alertBody.textContent = message; + + // Append the alert body to the alert div and insert it at the top of the main content area + alertDiv.appendChild(alertBody); + mainContent.insertBefore(alertDiv, mainContent.firstChild); + + // Scroll the page to make the alert visible to the user + scrollToElement("class", "usa-alert__body"); + } +} + +/** + * Initializes the DSDataForm when the DOM is fully loaded. + */ +export function initFormDSData() { + document.addEventListener('DOMContentLoaded', () => { + if (document.getElementById('dsdata-add-button')) { + const dsDataForm = new DSDataForm(); + dsDataForm.init(); + } + }); +} diff --git a/src/registrar/assets/src/js/getgov/form-helpers.js b/src/registrar/assets/src/js/getgov/form-helpers.js index fabfab98a..7a9b0c38f 100644 --- a/src/registrar/assets/src/js/getgov/form-helpers.js +++ b/src/registrar/assets/src/js/getgov/form-helpers.js @@ -38,6 +38,11 @@ export function removeErrorsFromElement(domElement) { domElement.querySelectorAll("input.usa-input--error").forEach(input => { input.classList.remove("usa-input--error"); }); + + // Remove the 'usa-input--error' class from all select elements + domElement.querySelectorAll("select.usa-input--error").forEach(select => { + select.classList.remove("usa-input--error"); + }); } /** diff --git a/src/registrar/assets/src/js/getgov/formset-forms.js b/src/registrar/assets/src/js/getgov/formset-forms.js index b4a40e5cf..1d2724b7f 100644 --- a/src/registrar/assets/src/js/getgov/formset-forms.js +++ b/src/registrar/assets/src/js/getgov/formset-forms.js @@ -84,7 +84,7 @@ function markForm(e, formLabel){ } /** - * Prepare the namerservers, DS data and Other Contacts formsets' delete button + * Prepare the Other Contacts formsets' delete button * for the last added form. We call this from the Add function * */ @@ -108,7 +108,7 @@ function prepareNewDeleteButton(btn, formLabel) { } /** - * Prepare the namerservers, DS data and Other Contacts formsets' delete buttons + * Prepare the Other Contacts formsets' delete buttons * We will call this on the forms init * */ @@ -172,16 +172,11 @@ export function initFormsetsForms() { let cloneIndex = 0; let formLabel = ''; let isOtherContactsForm = document.querySelector(".other-contacts-form"); - let isDsDataForm = document.querySelector(".ds-data-form"); let isDotgovDomain = document.querySelector(".dotgov-domain-form"); - if( !(isOtherContactsForm || isDotgovDomain || isDsDataForm) ){ + if( !(isOtherContactsForm || isDotgovDomain) ){ return } - // DNSSEC: DS Data - if (isDsDataForm) { - formLabel = "DS data record"; - // The Other Contacts form - } else if (isOtherContactsForm) { + if (isOtherContactsForm) { formLabel = "Organization contact"; container = document.querySelector("#other-employees"); formIdentifier = "other_contacts" @@ -287,26 +282,3 @@ export function initFormsetsForms() { prepareNewDeleteButton(newDeleteButton, formLabel); } } - -export function triggerModalOnDsDataForm() { - let saveButon = document.querySelector("#save-ds-data"); - - // The view context will cause a hitherto hidden modal trigger to - // show up. On save, we'll test for that modal trigger appearing. We'll - // run that test once every 100 ms for 5 secs, which should balance performance - // while accounting for network or lag issues. - if (saveButon) { - let i = 0; - var tryToTriggerModal = setInterval(function() { - i++; - if (i > 100) { - clearInterval(tryToTriggerModal); - } - let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert"); - if (modalTrigger) { - modalTrigger.click() - clearInterval(tryToTriggerModal); - } - }, 50); - } -} diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js index 0529d3614..03d970d7e 100644 --- a/src/registrar/assets/src/js/getgov/main.js +++ b/src/registrar/assets/src/js/getgov/main.js @@ -1,7 +1,8 @@ import { hookupYesNoListener } from './radios.js'; import { initDomainValidators } from './domain-validators.js'; -import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js'; -import { initFormNameservers } from './form-nameservers' +import { initFormsetsForms } from './formset-forms.js'; +import { initFormNameservers } from './form-nameservers'; +import { initFormDSData } from './form-dsdata.js'; import { initializeUrbanizationToggle } from './urbanization.js'; import { userProfileListener, finishUserSetupListener } from './user-profile.js'; import { handleRequestingEntityFieldset } from './requesting-entity.js'; @@ -13,7 +14,6 @@ import { initEditMemberDomainsTable } from './table-edit-member-domains.js'; import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js'; import { initDomainRequestForm } from './domain-request-form.js'; import { initDomainManagersPage } from './domain-managers.js'; -import { initDomainDSData } from './domain-dsdata.js'; import { initDomainDNSSEC } from './domain-dnssec.js'; import { initFormErrorHandling } from './form-errors.js'; import { initButtonLinks } from '../getgov-admin/button-utils.js'; @@ -21,8 +21,8 @@ import { initButtonLinks } from '../getgov-admin/button-utils.js'; initDomainValidators(); initFormsetsForms(); -triggerModalOnDsDataForm(); initFormNameservers(); +initFormDSData(); hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees'); hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null); @@ -42,7 +42,6 @@ initEditMemberDomainsTable(); initDomainRequestForm(); initDomainManagersPage(); -initDomainDSData(); initDomainDNSSEC(); initFormErrorHandling(); diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 538edc7ab..bb87ad119 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -688,7 +688,7 @@ class DomainDsdataForm(forms.Form): DomainDsdataFormset = formset_factory( DomainDsdataForm, - extra=0, + extra=1, can_delete=True, ) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 95e8e3d5f..14b8ad519 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -34,122 +34,334 @@ {% endif %} {% endblock breadcrumb %} - {% if domain.dnssecdata is None %} -
-
- You have no DS data added. Enable DNSSEC by adding DS data. +
+
+

DS data

+
+ +
+
+ +

In order to enable DNSSEC, you must first configure it with your DNS provider.

+ +

Click "Add DS data" and enter the values given by your DNS provider for DS (Delegation Signer) data.

+ + {% comment %} + This template supports the rendering of three different DS data forms, conditionally displayed: + 1 - Add DS Data form (rendered when there are no existing DS data records defined for the domain) + 2 - DS Data table (rendered when the domain has existing DS data, which can be viewed and edited) + 3 - Add DS Data form (rendered above the DS Data table to add a single additional DS Data record) + {% endcomment %} + + {% if formset.initial and formset.forms.0.initial %} + + {% comment %}This section renders both the DS Data table and the Add DS Data form {% endcomment %} + + {% include "includes/required_fields.html" %} + +
+ {% csrf_token %} + {{ formset.management_form }} + + {% for form in formset %} + {% if forloop.last %} + + {% comment %}This section renders the Add DS data form.{% endcomment %} + + {% endif %} + {% endfor %} + + + + + + + + + + + + + + {% for form in formset %} + {% if not forloop.last or form.initial %} + + {% comment %} + This section renders table rows for each existing DS data records. Two rows are rendered, a readonly row + and an edit row. Only one of which is displayed at a time. + {% endcomment %} + + + + + + + + + + + + + + + + + + {% endif %} + {% endfor %} + +
Key tagAlgorithmDigest typeDigestAction
{{ form.key_tag.value }} + {% for value, label in form.algorithm.field.choices %} + {% if value|stringformat:"s" == form.algorithm.value|stringformat:"s" %} + {{ label }} + {% endif %} + {% endfor %} + + {% for value, label in form.digest_type.field.choices %} + {% if value|stringformat:"s" == form.digest_type.value|stringformat:"s" %} + {{ label }} + {% endif %} + {% endfor %} + {{ form.digest.value }} +
+ + + + + Delete + + +
+
+ +
+ +
+
+
+
+ + {% else %} + + {% comment %} + This section renders Add DS Data form which renders when there are no existing + DS records defined on the domain. + {% endcomment %} + + {% endif %} -

DS data

- -

In order to enable DNSSEC, you must first configure it with your DNS hosting service.

- -

Enter the values given by your DNS provider for DS data.

- - {% include "includes/required_fields.html" %} - -
- {% csrf_token %} - {{ formset.management_form }} - - {% for form in formset %} -
- - DS data record {{forloop.counter}} - -

DS data record {{forloop.counter}}

- -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.key_tag %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.algorithm %} - {% endwith %} -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest_type %} - {% endwith %} -
-
- -
-
- {% with attr_required=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest %} - {% endwith %} -
-
- -
-
- -
-
- -
- {% endfor %} - - - - - - -
- - {% if trigger_modal %} +
+ {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_id="unsaved-changes-click-button" modal_button_text="Continue without saving" cancel_button_text="Go back" %} +
+ + +
+ {% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this DS data record?" modal_description="This action cannot be undone." modal_button_id="delete-click-button" modal_button_text="Yes, delete" modal_button_class="usa-button--secondary" %} +
+ + - {% endif %} - {# Use data-force-action to take esc out of the equation and pass cancel_button_resets_ds_form to effectuate a reset in the view #}
- {% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-override-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %} + {% include 'includes/modal.html' with modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, you’ll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-dnssec-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %}
-
- {% csrf_token %} - -
-
- {% csrf_token %} - -
{% endblock %} {# domain_content #} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 3a083393e..4f44f0b01 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1060,10 +1060,6 @@ class DomainDsDataView(DomainFormBaseView): for record in dnssecdata.dsData ) - # Ensure at least 1 record, filled or empty - while len(initial_data) == 0: - initial_data.append({}) - return initial_data def get_success_url(self): @@ -1082,29 +1078,8 @@ class DomainDsDataView(DomainFormBaseView): """Formset submission posts to this view.""" self._get_domain(request) formset = self.get_form() - override = False - # This is called by the form cancel button, - # and also by the modal's X and cancel buttons - if "btn-cancel-click" in request.POST: - url = self.get_success_url() - return HttpResponseRedirect(url) - - # This is called by the Disable DNSSEC modal to override - if "disable-override-click" in request.POST: - override = True - - # This is called when all DNSSEC data has been deleted and the - # Save button is pressed - if len(formset) == 0 and formset.initial != [{}] and override is False: - # trigger the modal - # get context data from super() rather than self - # to preserve the context["form"] - context = super().get_context_data(form=formset) - context["trigger_modal"] = True - return self.render_to_response(context) - - if formset.is_valid() or override: + if formset.is_valid(): return self.form_valid(formset) else: return self.form_invalid(formset) @@ -1116,9 +1091,10 @@ class DomainDsDataView(DomainFormBaseView): dnssecdata = extensions.DNSSECExtension() for form in formset: + if form.cleaned_data.get("DELETE"): # Check if form is marked for deletion + continue # Skip processing this form + try: - # if 'delete' not in form.cleaned_data - # or form.cleaned_data['delete'] == False: dsrecord = { "keyTag": form.cleaned_data["key_tag"], "alg": int(form.cleaned_data["algorithm"]), From 0d44b3426024464c93f4997a9a96ea6638a576de Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 10 Mar 2025 19:02:03 -0400 Subject: [PATCH 02/77] cleaned up modal code --- src/registrar/templates/includes/modal.html | 49 +++++---------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index af49d2b6c..09e2909c0 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -58,18 +58,7 @@ {% endif %}
  • - {% comment %} The cancel button the DS form actually triggers a context change in the view, - in addition to being a close modal hook {% endcomment %} - {% if cancel_button_resets_ds_form %} - - {% elif not cancel_button_only %} + {% if not cancel_button_only %}
  • - {% comment %} The cancel button the DS form actually triggers a context change in the view, - in addition to being a close modal hook {% endcomment %} - {% if cancel_button_resets_ds_form %} - - {% else %} - - {% endif %} + From b1e6730d87cf43441cadac3a734521d59b50cd0d Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 10 Mar 2025 19:30:10 -0400 Subject: [PATCH 03/77] added cancel modal --- .../assets/src/js/getgov/form-dsdata.js | 40 ++++++++++++++++--- src/registrar/templates/domain_dsdata.html | 16 ++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/form-dsdata.js b/src/registrar/assets/src/js/getgov/form-dsdata.js index e9be4135e..a9219ae94 100644 --- a/src/registrar/assets/src/js/getgov/form-dsdata.js +++ b/src/registrar/assets/src/js/getgov/form-dsdata.js @@ -107,6 +107,15 @@ export class DSDataForm { this.executeCallback(); }); } + const cancel_changes_modal = document.getElementById('cancel-changes-modal'); + if (cancel_changes_modal) { + const submitButton = document.getElementById('cancel-changes-click-button'); + const closeButton = cancel_changes_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } const delete_modal = document.getElementById('delete-modal'); if (delete_modal) { const submitButton = document.getElementById('delete-click-button'); @@ -146,7 +155,6 @@ export class DSDataForm { */ handleAddFormClick(event) { this.callback = () => { - console.log("handleAddFormClick callback"); // Check if any other edit row is currently visible and hide it document.querySelectorAll('tr.edit-row:not(.display-none)').forEach(openEditRow => { this.resetEditRowAndFormAndCollapseEditRow(openEditRow); @@ -279,7 +287,18 @@ export class DSDataForm { * @param {Event} event - Click event */ handleCancelAddFormClick(event) { - this.resetAddDSDataForm(); + this.callback = () => { + this.resetAddDSDataForm(); + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } } /** @@ -295,10 +314,21 @@ export class DSDataForm { let cancelButton = event.target; // find the closest table row that contains the cancel button let editRow = cancelButton.closest('tr'); - if (editRow) { - this.resetEditRowAndFormAndCollapseEditRow(editRow); + this.callback = () => { + if (editRow) { + this.resetEditRowAndFormAndCollapseEditRow(editRow); + } else { + console.warn("Expected DOM element but did not find it"); + } + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } } else { - console.warn("Expected DOM element but did not find it"); + this.executeCallback(); } } diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 14b8ad519..42c94c9f5 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -331,6 +331,22 @@ {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_id="unsaved-changes-click-button" modal_button_text="Continue without saving" cancel_button_text="Go back" %} + +
    + {% include 'includes/modal.html' with modal_heading="Are you sure you want to cancel your changes?" modal_description="This action cannot be undone." modal_button_id="cancel-changes-click-button" modal_button_text="Yes, cancel" cancel_button_text="Go back" %} +
    + Date: Tue, 11 Mar 2025 10:58:50 -0400 Subject: [PATCH 04/77] added cancel modal to nameservers page --- .../assets/src/js/getgov/form-nameservers.js | 39 +++++++++++++++++-- .../templates/domain_nameservers.html | 16 ++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/form-nameservers.js b/src/registrar/assets/src/js/getgov/form-nameservers.js index 57b868d70..75a35f35b 100644 --- a/src/registrar/assets/src/js/getgov/form-nameservers.js +++ b/src/registrar/assets/src/js/getgov/form-nameservers.js @@ -176,6 +176,15 @@ export class NameserverForm { this.executeCallback(); }); } + const cancel_changes_modal = document.getElementById('cancel-changes-modal'); + if (cancel_changes_modal) { + const submitButton = document.getElementById('cancel-changes-click-button'); + const closeButton = cancel_changes_modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + closeButton.click(); + this.executeCallback(); + }); + } const delete_modal = document.getElementById('delete-modal'); if (delete_modal) { const submitButton = document.getElementById('delete-click-button'); @@ -338,7 +347,18 @@ export class NameserverForm { * @param {Event} event - Click event */ handleCancelAddFormClick(event) { - this.resetAddNameserversForm(); + this.callback = () => { + this.resetAddNameserversForm(); + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } + } else { + this.executeCallback(); + } } /** @@ -354,10 +374,21 @@ export class NameserverForm { let cancelButton = event.target; // find the closest table row that contains the cancel button let editRow = cancelButton.closest('tr'); - if (editRow) { - this.resetEditRowAndFormAndCollapseEditRow(editRow); + this.callback = () => { + if (editRow) { + this.resetEditRowAndFormAndCollapseEditRow(editRow); + } else { + console.warn("Expected DOM element but did not find it"); + } + } + if (this.formChanged) { + // Show the cancel changes confirmation modal + let modalTrigger = document.querySelector("#cancel_changes_trigger"); + if (modalTrigger) { + modalTrigger.click(); + } } else { - console.warn("Expected DOM element but did not find it"); + this.executeCallback(); } } diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index bd8216d05..b6314ea5f 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -309,6 +309,22 @@ > {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_id="unsaved-changes-click-button" modal_button_text="Continue without saving" cancel_button_text="Go back" %} + + +
    + {% include 'includes/modal.html' with modal_heading="Are you sure you want to cancel your changes?" modal_description="This action cannot be undone." modal_button_id="cancel-changes-click-button" modal_button_text="Yes, cancel" cancel_button_text="Go back" %} +
    Date: Tue, 11 Mar 2025 12:00:16 -0400 Subject: [PATCH 05/77] button click interaction and helper text in form --- .../assets/src/js/getgov/form-dsdata.js | 5 +++ src/registrar/templates/domain_dsdata.html | 34 +++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/form-dsdata.js b/src/registrar/assets/src/js/getgov/form-dsdata.js index a9219ae94..a8c022056 100644 --- a/src/registrar/assets/src/js/getgov/form-dsdata.js +++ b/src/registrar/assets/src/js/getgov/form-dsdata.js @@ -166,6 +166,11 @@ export class DSDataForm { } // show add ds data form showElement(this.addDSDataForm); + // focus on key tag in the form + let keyTagInput = this.addDSDataForm.querySelector('input[name$="-key_tag"]'); + if (keyTagInput) { + keyTagInput.focus(); + } } }; if (this.formChanged) { diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index 42c94c9f5..e419a03b4 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -76,8 +76,10 @@
    - {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.key_tag %} + {% with sublabel_text="Numbers (0-9) only." %} + {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.key_tag %} + {% endwith %} {% endwith %}
    @@ -94,8 +96,10 @@
    - {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest %} + {% with sublabel_text="Numbers (0-9) and letters (a-f) only. SHA-1: 40 chars, SHA-256: 64 chars." %} + {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.digest %} + {% endwith %} {% endwith %}
    @@ -197,7 +201,7 @@ type="button" class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary dsdata-delete-kebab" name="btn-delete-kebab-click" - aria-label="Delete the DS record from the registry" + aria-label="Delete DS record {{ forloop.counter }} from the registry" > Delete @@ -209,9 +213,11 @@ - {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} + {% with sublabel_text="Numbers (0-9) only." %} + {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.key_tag %} {% endwith %} + {% endwith %} {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} @@ -224,9 +230,11 @@ {% endwith %} - {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} + {% with sublabel_text="Numbers (0-9) and letters (a-f) only. SHA-1: 40 chars, SHA-256: 64 chars." %} + {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} {% input_with_errors form.digest %} {% endwith %} + {% endwith %} @@ -271,8 +279,10 @@
    - {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.key_tag %} + {% with sublabel_text="Numbers (0-9) only." %} + {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.key_tag %} + {% endwith %} {% endwith %}
    @@ -289,8 +299,10 @@
    - {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} - {% input_with_errors form.digest %} + {% with sublabel_text="Numbers (0-9) and letters (a-f) only. SHA-1: 40 chars, SHA-256: 64 chars." %} + {% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %} + {% input_with_errors form.digest %} + {% endwith %} {% endwith %}
    From f60ea28062a15c29df41ed8baafc7fa0ab54d362 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 11 Mar 2025 16:51:13 -0400 Subject: [PATCH 06/77] key tag from number to char field --- src/registrar/forms/domain.py | 24 ++++++++++++++++++------ src/registrar/utility/errors.py | 3 +++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index bb87ad119..53517d99e 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -630,14 +630,10 @@ class DomainDsdataForm(forms.Form): if not re.match(r"^[0-9a-fA-F]+$", value): raise forms.ValidationError(str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS))) - key_tag = forms.IntegerField( + key_tag = forms.CharField( required=True, label="Key tag", - validators=[ - MinValueValidator(0, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))), - MaxValueValidator(65535, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))), - ], - error_messages={"required": ("Key tag is required.")}, + error_messages={"required": "Key tag is required."}, ) algorithm = forms.TypedChoiceField( @@ -672,6 +668,22 @@ class DomainDsdataForm(forms.Form): cleaned_data = super().clean() digest_type = cleaned_data.get("digest_type", 0) digest = cleaned_data.get("digest", "") + + # Convert key_tag to an integer safely + key_tag = cleaned_data.get("key_tag", 0) + try: + key_tag = int(key_tag) + if key_tag < 0 or key_tag > 65535: + self.add_error( + "key_tag", + DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE), + ) + except ValueError: + self.add_error( + "key_tag", + DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_CHARS), + ) + # validate length of digest depending on digest_type if digest_type == 1 and len(digest) != 40: self.add_error( diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 0a6f00c36..eccd118f8 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -238,6 +238,7 @@ class DsDataErrorCodes(IntEnum): - 3 INVALID_DIGEST_SHA256 invalid digest for digest type SHA-256 - 4 INVALID_DIGEST_CHARS invalid chars in digest - 5 INVALID_KEYTAG_SIZE invalid key tag size > 65535 + - 6 INVALID_KEYTAG_CHARS invalid key tag, not numeric """ BAD_DATA = 1 @@ -245,6 +246,7 @@ class DsDataErrorCodes(IntEnum): INVALID_DIGEST_SHA256 = 3 INVALID_DIGEST_CHARS = 4 INVALID_KEYTAG_SIZE = 5 + INVALID_KEYTAG_CHARS = 6 class DsDataError(Exception): @@ -261,6 +263,7 @@ class DsDataError(Exception): DsDataErrorCodes.INVALID_DIGEST_SHA256: ("SHA-256 digest must be exactly 64 characters."), DsDataErrorCodes.INVALID_DIGEST_CHARS: ("Digest must contain only alphanumeric characters (0-9, a-f)."), DsDataErrorCodes.INVALID_KEYTAG_SIZE: ("Key tag must be less than 65535."), + DsDataErrorCodes.INVALID_KEYTAG_CHARS: ("Key tag must be numeric (0-9)."), } def __init__(self, *args, code=None, **kwargs): From e92e2567e03a3f5a8542462d01100015e3477d68 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 11 Mar 2025 16:58:43 -0400 Subject: [PATCH 07/77] update js to properly handle text input instead of number --- .../assets/src/js/getgov/form-dsdata.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/registrar/assets/src/js/getgov/form-dsdata.js b/src/registrar/assets/src/js/getgov/form-dsdata.js index a8c022056..3f04dbb26 100644 --- a/src/registrar/assets/src/js/getgov/form-dsdata.js +++ b/src/registrar/assets/src/js/getgov/form-dsdata.js @@ -390,8 +390,8 @@ export class DSDataForm { resetInputValuesInElement(domElement) { const inputEvent = new Event('input'); const changeEvent = new Event('change'); - // Reset text and number inputs - let inputs = domElement.querySelectorAll("input[type='text'], input[type='number']"); + // Reset text inputs + let inputs = domElement.querySelectorAll("input[type='text']"); inputs.forEach(input => { // Reset input value to its initial stored value input.value = input.dataset.initialValue; @@ -415,15 +415,15 @@ export class DSDataForm { * @param {HTMLElement} readOnlyRow - The row where values will be displayed in a non-editable format. */ copyEditRowToReadonlyRow(editRow, readOnlyRow) { - let numberInput = editRow.querySelector("input[type='number']"); + let inputs = editRow.querySelectorAll("input[type='text']"); + let keyTagInput = inputs[0]; let selects = editRow.querySelectorAll("select"); - let textInput = editRow.querySelector("input[type='text']"); + let digestInput = inputs[1]; let tds = readOnlyRow.querySelectorAll("td"); - let updatedText = ''; - // Copy the number input value - if (numberInput) { - tds[0].innerText = numberInput.value || ""; + // Copy the key tag input value + if (keyTagInput) { + tds[0].innerText = keyTagInput.value || ""; } // Copy select values (showing the selected label instead of value) @@ -434,9 +434,9 @@ export class DSDataForm { } }); - // Copy the text input value - if (textInput) { - tds[3].innerText = textInput.value || ""; + // Copy the digest input value + if (digestInput) { + tds[3].innerText = digestInput.value || ""; } } From e56c533e9a1ad78ffa6be8d91b579afe9617ddb0 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 11 Mar 2025 16:16:42 -0600 Subject: [PATCH 08/77] width adjustments --- .../assets/src/sass/_theme/_containers.scss | 6 +- src/registrar/templates/domain_base.html | 4 +- src/registrar/templates/domain_dsdata.html | 259 +++++++++--------- .../templates/domain_request_form.html | 4 +- 4 files changed, 139 insertions(+), 134 deletions(-) diff --git a/src/registrar/assets/src/sass/_theme/_containers.scss b/src/registrar/assets/src/sass/_theme/_containers.scss index 24ad480f2..213389fc0 100644 --- a/src/registrar/assets/src/sass/_theme/_containers.scss +++ b/src/registrar/assets/src/sass/_theme/_containers.scss @@ -22,5 +22,9 @@ // regular grid-container within a widescreen (see instances // where is_widescreen_centered is used in the html). .max-width--grid-container { - max-width: 960px; + max-width: 1024px; +} + +.grid-col--sidenav { + max-width: 230px; } \ No newline at end of file diff --git a/src/registrar/templates/domain_base.html b/src/registrar/templates/domain_base.html index 249f69d32..a1392cbe1 100644 --- a/src/registrar/templates/domain_base.html +++ b/src/registrar/templates/domain_base.html @@ -9,7 +9,7 @@
    -
    +

    @@ -21,7 +21,7 @@ {% endif %}

    -
    +
    {% if not domain.domain_info %}
    diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index e419a03b4..f6574600c 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -123,143 +123,144 @@ {% endif %} {% endfor %} +
    + + + + + + + + + + + + {% for form in formset %} + {% if not forloop.last or form.initial %} -
    Key tagAlgorithmDigest typeDigestAction
    - - - - - - - - - - - {% for form in formset %} - {% if not forloop.last or form.initial %} + {% comment %} + This section renders table rows for each existing DS data records. Two rows are rendered, a readonly row + and an edit row. Only one of which is displayed at a time. + {% endcomment %} + + + + + + + + - - - - - - - - - + + + + + - - - + + - - - {% endif %} - {% endfor %} - -
    Key tagAlgorithmDigest typeDigestAction
    {{ form.key_tag.value }} + {% for value, label in form.algorithm.field.choices %} + {% if value|stringformat:"s" == form.algorithm.value|stringformat:"s" %} + {{ label }} + {% endif %} + {% endfor %} + + {% for value, label in form.digest_type.field.choices %} + {% if value|stringformat:"s" == form.digest_type.value|stringformat:"s" %} + {{ label }} + {% endif %} + {% endfor %} + {{ form.digest.value }} +
    + - {% comment %} - This section renders table rows for each existing DS data records. Two rows are rendered, a readonly row - and an edit row. Only one of which is displayed at a time. - {% endcomment %} - - -
    {{ form.key_tag.value }} - {% for value, label in form.algorithm.field.choices %} - {% if value|stringformat:"s" == form.algorithm.value|stringformat:"s" %} - {{ label }} - {% endif %} - {% endfor %} - - {% for value, label in form.digest_type.field.choices %} - {% if value|stringformat:"s" == form.digest_type.value|stringformat:"s" %} - {{ label }} - {% endif %} - {% endfor %} - {{ form.digest.value }} -
    - + + + Delete + - - - Delete - - -
    -
    - -
    - -
    -
    + + + + + + + + + + {% endif %} + {% endfor %} + + +
    {% else %} diff --git a/src/registrar/templates/domain_request_form.html b/src/registrar/templates/domain_request_form.html index 2142372e8..b9844aa06 100644 --- a/src/registrar/templates/domain_request_form.html +++ b/src/registrar/templates/domain_request_form.html @@ -5,10 +5,10 @@ {% block content %}
    -
    +
    {% include 'domain_request_sidebar.html' %}
    -
    +
    {% if steps.current == steps.first %} From 2d21129a870762765d07c619c606e69ce5ece9c4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 11 Mar 2025 16:32:30 -0600 Subject: [PATCH 09/77] ellipsis --- src/registrar/templates/domain_dsdata.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html index f6574600c..0c5e78859 100644 --- a/src/registrar/templates/domain_dsdata.html +++ b/src/registrar/templates/domain_dsdata.html @@ -160,7 +160,7 @@ {% endif %} {% endfor %} - {{ form.digest.value }} + {{ form.digest.value }}