initial ds data redesign

This commit is contained in:
David Kennedy 2025-03-10 18:52:14 -04:00
parent 8df1a63ab4
commit d6d68c401d
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
8 changed files with 804 additions and 195 deletions

View file

@ -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");
});
}
}
});
}

View file

@ -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();
}
});
}

View file

@ -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");
});
}
/**

View file

@ -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);
}
}

View file

@ -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();

View file

@ -688,7 +688,7 @@ class DomainDsdataForm(forms.Form):
DomainDsdataFormset = formset_factory(
DomainDsdataForm,
extra=0,
extra=1,
can_delete=True,
)

View file

@ -34,122 +34,334 @@
{% endif %}
{% endblock breadcrumb %}
{% if domain.dnssecdata is None %}
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-3">
<div class="usa-alert__body">
You have no DS data added. Enable DNSSEC by adding DS data.
<div class="grid-row grid-gap">
<div class="tablet:grid-col-6">
<h1 class="tablet:margin-bottom-1" id="domain-dsdata">DS data</h1>
</div>
<div class="tablet:grid-col-6 text-right--tablet">
<button type="button" class="usa-button margin-bottom-1 tablet:float-right" id="dsdata-add-button">
Add DS data
</button>
</div>
</div>
<p>In order to enable DNSSEC, you must first configure it with your DNS provider.</p>
<p>Click "Add DS data" and enter the values given by your DNS provider for DS (Delegation Signer) data.</p>
{% 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" %}
<form class="usa-form usa-form--extra-large ds-data-form" method="post" novalidate id="form-container">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
{% if forloop.last %}
{% comment %}This section renders the Add DS data form.{% endcomment %}
<section class="add-dsdata-form display-none section-outlined">
<h2 class="margin-top-0">Add DS record</h2>
<div class="repeatable-form">
<div class="grid-row grid-gap-2 flex-end">
<div class="tablet:grid-col-4">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.key_tag %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.algorithm %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest_type %}
{% endwith %}
</div>
</div>
<div class="grid-row">
<div class="grid-col">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest %}
{% endwith %}
</div>
</div>
</div>
<div class="margin-top-2">
<button
type="submit"
class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Reset the data in the DS records to the registry state (undo changes)"
>Cancel
</button>
<button
id="save-ds-data"
type="submit"
class="usa-button"
>Save
</button>
</div>
</section>
{% endif %}
{% endfor %}
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked" id="dsdata-table">
<thead>
<tr>
<th scope="col" role="columnheader">Key tag</th>
<th scope="col" role="columnheader">Algorithm</th>
<th scope="col" role="columnheader">Digest type</th>
<th scope="col" role="columnheader">Digest</th>
<th scope="col" role="columnheader" class="width-0 padding-right-0">Action</th>
</tr>
</thead>
<tbody>
{% 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 %}
<!-- Readonly row -->
<tr>
<td>{{ form.key_tag.value }}</td>
<td>
{% for value, label in form.algorithm.field.choices %}
{% if value|stringformat:"s" == form.algorithm.value|stringformat:"s" %}
{{ label }}
{% endif %}
{% endfor %}
</td>
<td>
{% for value, label in form.digest_type.field.choices %}
{% if value|stringformat:"s" == form.digest_type.value|stringformat:"s" %}
{{ label }}
{% endif %}
{% endfor %}
</td>
<td>{{ form.digest.value }}</td>
<td class="padding-right-0">
<div class="tablet:display-flex tablet:flex-row">
<button type="button" class='usa-button usa-button--unstyled margin-right-2 margin-top-0 dsdata-edit'>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#edit"></use>
</svg>
Edit <span class="usa-sr-only">DS record {{forloop.counter}}</span>
</button>
<a
role="button"
id="button-trigger-delete-dsdata-{{ forloop.counter }}"
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary visible-mobile-flex dsdata-delete-kebab"
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#delete"></use>
</svg>
Delete
</a>
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
<div class="usa-accordion__heading">
<button
type="button"
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions margin-top-0"
aria-expanded="false"
aria-controls="more-actions-dsdata-{{ forloop.counter }}"
aria-label="More Actions for DS record {{ forloop.counter }}"
>
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
</svg>
</button>
</div>
<div id="more-actions-dsdata-{{ forloop.counter }}" class="usa-accordion__content usa-prose shadow-1 left-auto right-neg-1" hidden>
<h2>More options</h2>
<button
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"
>
Delete
</button>
</div>
</div>
</div>
</td>
</tr>
<!-- Edit row -->
<tr class="edit-row display-none">
<td class="text-bottom">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.key_tag %}
{% endwith %}
</td>
<td class="text-bottom">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.algorithm %}
{% endwith %}
</td>
<td class="text-bottom">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest_type %}
{% endwith %}
</td>
<td class="text-bottom">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest %}
{% endwith %}
</td>
<td class="padding-right-0 text-bottom">
<button class="usa-button usa-button--unstyled display-block margin-top-1" type="submit">Save</button>
<button
type="button"
class="usa-button usa-button--unstyled display-block dsdata-cancel"
name="btn-cancel-click"
aria-label="Reset the data in the DS record form to the registry state (undo changes)"
>Cancel
</button>
<button
type="button"
class="usa-button usa-button--unstyled display-block text-secondary dsdata-delete"
name="btn-delete-click"
aria-label="Delete the DS record from the registry"
>Delete
</button>
<div class="display-none">{{ form.DELETE }}</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</form>
{% else %}
{% comment %}
This section renders Add DS Data form which renders when there are no existing
DS records defined on the domain.
{% endcomment %}
<section class="add-dsdata-form display-none section-outlined">
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--extra-large" method="post" novalidate>
<h2>Add DS record</h2>
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<div class="repeatable-form">
<div class="grid-row grid-gap-2 flex-end">
<div class="tablet:grid-col-4">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.key_tag %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.algorithm %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest_type %}
{% endwith %}
</div>
</div>
<div class="grid-row">
<div class="grid-col">
{% with attr_required=True add_initial_value_attr=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest %}
{% endwith %}
</div>
</div>
</div>
{% endfor %}
<div class="margin-top-2">
<button
type="button"
class="usa-button usa-button--outline dsdata-cancel-add-form"
name="btn-cancel-click"
aria-label="Reset the data in the DS records to the registry state (undo changes)"
>Cancel
</button>
<button
id="save-ds-data"
type="submit"
class="usa-button"
>Save
</button>
</div>
</form>
</section>
{% endif %}
<h1 id="domain-dsdata">DS data</h1>
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
<p>Enter the values given by your DNS provider for DS data.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--extra-large ds-data-form" method="post" novalidate id="form-container">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<fieldset class="repeatable-form">
<legend class="sr-only">DS data record {{forloop.counter}}</legend>
<h2 class="margin-top-0">DS data record {{forloop.counter}}</h2>
<div class="grid-row grid-gap-2 flex-end">
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.key_tag %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.algorithm %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest_type %}
{% endwith %}
</div>
</div>
<div class="grid-row">
<div class="grid-col">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest %}
{% endwith %}
</div>
</div>
<div class="grid-row margin-top-1">
<div class="grid-col">
<button type="button" id="button label" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record text-secondary line-height-sans-5">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
</svg>Delete
<span class="sr-only">DS data record {{forloop.counter}}</span>
</button>
</div>
</div>
</fieldset>
{% endfor %}
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon margin-bottom-2" id="add-form">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
</svg>Add new record
</button>
<button
id="save-ds-data"
type="submit"
class="usa-button"
>Save
</button>
<button
type="submit"
class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Reset the data in the DS records to the registry state (undo changes)"
>Cancel
</button>
</form>
{% if trigger_modal %}
<a
id="ds-toggle-dnssec-alert"
href="#toggle-dnssec-alert"
id="unsaved_changes_trigger"
href="#unsaved-changes-modal"
class="usa-button usa-button--outline margin-top-1 display-none"
aria-controls="unsaved-changes-modal"
data-open-modal
>Trigger unsaved changes modal</a>
<div
class="usa-modal"
id="unsaved-changes-modal"
aria-labelledby="Are you sure you want to continue?"
aria-describedby="You have unsaved changes that will be lost."
>
{% 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" %}
</div>
<a
id="delete_trigger"
href="#delete-modal"
class="usa-button usa-button--outline margin-top-1 display-none"
aria-controls="delete-modal"
data-open-modal
>Trigger delete modal</a>
<div
class="usa-modal"
id="delete-modal"
aria-labelledby="Are you sure you want to delete this DS data record?"
aria-describedby="This action cannot be undone."
>
{% 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" %}
</div>
<a
id="disable_dnssec_trigger"
href="#disable-dnssec-modal"
class="usa-button usa-button--outline margin-top-1 display-none"
aria-controls="toggle-dnssec-alert"
aria-controls="disable-dnssec-modal"
data-open-modal
>Trigger Disable DNSSEC Modal</a
>
{% 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 #}
<div
class="usa-modal"
id="toggle-dnssec-alert"
id="disable-dnssec-modal"
aria-labelledby="Are you sure you want to continue?"
aria-describedby="Your DNSSEC records will be deleted from the registry."
data-force-action
>
{% 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, youll 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, youll 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" %}
</div>
<form method="post" id="disable-override-click-form">
{% csrf_token %}
<input type="hidden" name="disable-override-click" value="1">
</form>
<form method="post" id="btn-cancel-click-form">
{% csrf_token %}
<input type="hidden" name="btn-cancel-click" value="1">
</form>
{% endblock %} {# domain_content #}

View file

@ -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"]),