"
@@ -3371,7 +3703,8 @@ class DomainInformationInline(admin.StackedInline):
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
- if analyst_perm and not superuser_perm:
+ omb_analyst_perm = request.user.groups.filter(name="omb_analysts_group").exists()
+ if (analyst_perm or omb_analyst_perm) and not superuser_perm:
return True
return super().has_change_permission(request, obj)
@@ -3445,6 +3778,23 @@ class DomainInformationInline(admin.StackedInline):
return modified_fieldsets
+ def get_form(self, request, obj=None, **kwargs):
+ """Pass the 'is_omb_analyst' attribute to the form."""
+ form = super().get_form(request, obj, **kwargs)
+
+ # Store attribute in the form for template access
+ self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
+ form.show_contact_as_plain_text = self.is_omb_analyst
+ form.is_omb_analyst = self.is_omb_analyst
+
+ return form
+
+ def get_formset(self, request, obj=None, **kwargs):
+ """Attach request to the formset so that it can be available in the form"""
+ formset = super().get_formset(request, obj, **kwargs)
+ formset.form.request = request # Attach request to form
+ return formset
+
class DomainResource(FsmModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@@ -3454,7 +3804,7 @@ class DomainResource(FsmModelResource):
model = models.Domain
-class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
+class DomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
"""Custom domain admin class to add extra buttons."""
resource_classes = [DomainResource]
@@ -3566,7 +3916,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if self.value():
return queryset.filter(
Q(domain_info__portfolio__federal_type=self.value())
- | Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
+ | Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
)
return queryset
@@ -3593,7 +3943,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
Q(domain_info__portfolio__isnull=False) & Q(domain_info__portfolio__federal_agency__isnull=False),
then=F("domain_info__portfolio__federal_agency__federal_type"),
),
- # Otherwise, return the natively assigned value
+ # Otherwise, return federal type from federal agency
default=F("domain_info__federal_agency__federal_type"),
),
converted_organization_name=Case(
@@ -4020,8 +4370,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Fixes a bug wherein users which are only is_staff
# can access 'change' when GET,
# but cannot access this page when it is a request of type POST.
- if request.user.has_perm("registrar.full_access_permission") or request.user.has_perm(
- "registrar.analyst_access_permission"
+ if (
+ request.user.has_perm("registrar.full_access_permission")
+ or request.user.has_perm("registrar.analyst_access_permission")
+ or request.user.groups.filter(name="omb_analysts_group").exists()
):
return True
return super().has_change_permission(request, obj)
@@ -4036,8 +4388,37 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if portfolio_id:
# Further filter the queryset by the portfolio
qs = qs.filter(domain_info__portfolio=portfolio_id)
+ # Check if user is in OMB analysts group
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ return qs.filter(
+ converted_generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
+ converted_federal_type=BranchChoices.EXECUTIVE,
+ )
return qs
+ def has_view_permission(self, request, obj=None):
+ """Restrict view permissions based on group membership and model attributes."""
+ if request.user.has_perm("registrar.full_access_permission"):
+ return True
+ if obj:
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ return (
+ obj.domain_info.converted_generic_org_type == DomainRequest.OrganizationChoices.FEDERAL
+ and obj.domain_info.converted_federal_type == BranchChoices.EXECUTIVE
+ )
+ return super().has_view_permission(request, obj)
+
+ def get_form(self, request, obj=None, **kwargs):
+ """Pass the 'is_omb_analyst' attribute to the form."""
+ form = super().get_form(request, obj, **kwargs)
+
+ # Store attribute in the form for template access
+ is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
+ form.show_contact_as_plain_text = is_omb_analyst
+ form.is_omb_analyst = is_omb_analyst
+
+ return form
+
class DraftDomainResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@@ -4047,7 +4428,7 @@ class DraftDomainResource(resources.ModelResource):
model = models.DraftDomain
-class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
+class DraftDomainAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
"""Custom draft domain admin class."""
resource_classes = [DraftDomainResource]
@@ -4159,7 +4540,7 @@ class PublicContactResource(resources.ModelResource):
self.after_save_instance(instance, using_transactions, dry_run)
-class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
+class PublicContactAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
"""Custom PublicContact admin class."""
resource_classes = [PublicContactResource]
@@ -4214,6 +4595,11 @@ class PortfolioAdmin(ListHeaderAdmin):
_meta = Meta()
+ def __init__(self, *args, **kwargs):
+ """Initialize the admin class and define a default value for is_omb_analyst."""
+ super().__init__(*args, **kwargs)
+ self.is_omb_analyst = False # Default value in case it's accessed before being set
+
change_form_template = "django/admin/portfolio_change_form.html"
fieldsets = [
# created_on is the created_at field
@@ -4295,6 +4681,19 @@ class PortfolioAdmin(ListHeaderAdmin):
# rather than strip it out of our logic.
analyst_readonly_fields = [] # type: ignore
+ omb_analyst_readonly_fields = [
+ "notes",
+ "organization_type",
+ "organization_name",
+ "federal_agency",
+ "state_territory",
+ "address_line1",
+ "address_line2",
+ "city",
+ "zipcode",
+ "urbanization",
+ ]
+
def get_admin_users(self, obj):
# Filter UserPortfolioPermission objects related to the portfolio
admin_permissions = self.get_user_portfolio_permission_admins(obj)
@@ -4380,6 +4779,8 @@ class PortfolioAdmin(ListHeaderAdmin):
"""Returns the number of administrators for this portfolio"""
admin_count = len(self.get_user_portfolio_permission_admins(obj))
if admin_count > 0:
+ if self.is_omb_analyst:
+ return format_html(f"{admin_count} administrators")
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
# Create a clickable link with the count
return format_html(f'{admin_count} admins')
@@ -4391,6 +4792,8 @@ class PortfolioAdmin(ListHeaderAdmin):
"""Returns the number of basic members for this portfolio"""
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
if member_count > 0:
+ if self.is_omb_analyst:
+ return format_html(f"{member_count} members")
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
# Create a clickable link with the count
return format_html(f'{member_count} basic members')
@@ -4436,12 +4839,35 @@ class PortfolioAdmin(ListHeaderAdmin):
if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields
-
+ # Return restrictive Read-only fields for OMB analysts
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
+ return readonly_fields
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
+ def get_queryset(self, request):
+ """Restrict queryset based on user permissions."""
+ qs = super().get_queryset(request)
+
+ # Check if user is in OMB analysts group
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ self.is_omb_analyst = True
+ return qs.filter(federal_agency__federal_type=BranchChoices.EXECUTIVE)
+
+ return qs # Return full queryset if the user doesn't have the restriction
+
+ def has_view_permission(self, request, obj=None):
+ """Restrict view permissions based on group membership and model attributes."""
+ if request.user.has_perm("registrar.full_access_permission"):
+ return True
+ if obj:
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ return obj.federal_type == BranchChoices.EXECUTIVE
+ return super().has_view_permission(request, obj)
+
def change_view(self, request, object_id, form_url="", extra_context=None):
"""Add related suborganizations and domain groups.
Add the summary for the portfolio members field (list of members that link to change_forms)."""
@@ -4486,6 +4912,17 @@ class PortfolioAdmin(ListHeaderAdmin):
super().save_model(request, obj, form, change)
+ def get_form(self, request, obj=None, **kwargs):
+ """Pass the 'is_omb_analyst' attribute to the form."""
+ form = super().get_form(request, obj, **kwargs)
+
+ # Store attribute in the form for template access
+ self.is_omb_analyst = request.user.groups.filter(name="omb_analysts_group").exists()
+ form.show_contact_as_plain_text = self.is_omb_analyst
+ form.is_omb_analyst = self.is_omb_analyst
+
+ return form
+
class FederalAgencyResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@@ -4495,13 +4932,66 @@ class FederalAgencyResource(resources.ModelResource):
model = models.FederalAgency
-class FederalAgencyAdmin(ListHeaderAdmin, ImportExportModelAdmin):
+class FederalAgencyAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
list_display = ["agency"]
search_fields = ["agency"]
search_help_text = "Search by federal agency."
ordering = ["agency"]
resource_classes = [FederalAgencyResource]
+ # Readonly fields for analysts and superusers
+ readonly_fields = []
+
+ # Read only that we'll leverage for CISA Analysts
+ analyst_readonly_fields = [] # type: ignore
+
+ # Read only that we'll leverage for OMB Analysts
+ omb_analyst_readonly_fields = [
+ "agency",
+ "federal_type",
+ "acronym",
+ "is_fceb",
+ ]
+
+ def get_queryset(self, request):
+ """Restrict queryset based on user permissions."""
+ qs = super().get_queryset(request)
+
+ # Check if user is in OMB analysts group
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ return qs.filter(
+ federal_type=BranchChoices.EXECUTIVE,
+ )
+
+ return qs # Return full queryset if the user doesn't have the restriction
+
+ def has_view_permission(self, request, obj=None):
+ """Restrict view permissions based on group membership and model attributes."""
+ if request.user.has_perm("registrar.full_access_permission"):
+ return True
+ if obj:
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ return obj.federal_type == BranchChoices.EXECUTIVE
+ return super().has_view_permission(request, obj)
+
+ def get_readonly_fields(self, request, obj=None):
+ """Set the read-only state on form elements.
+ We have 2 conditions that determine which fields are read-only:
+ admin user permissions and the domain request creator's status, so
+ we'll use the baseline readonly_fields and extend it as needed.
+ """
+ readonly_fields = list(self.readonly_fields)
+ if request.user.has_perm("registrar.full_access_permission"):
+ return readonly_fields
+ # Return restrictive Read-only fields for OMB analysts
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
+ return readonly_fields
+ # Return restrictive Read-only fields for analysts and
+ # users who might not belong to groups
+ readonly_fields.extend([field for field in self.analyst_readonly_fields])
+ return readonly_fields
+
class UserGroupAdmin(AuditedAdmin):
"""Overwrite the generated UserGroup admin class"""
@@ -4551,11 +5041,11 @@ class WaffleFlagAdmin(FlagAdmin):
return super().changelist_view(request, extra_context=extra_context)
-class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
+class DomainGroupAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
list_display = ["name", "portfolio"]
-class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
+class SuborganizationAdmin(ListHeaderAdmin, ImportExportRegistrarModelAdmin):
list_display = ["name", "portfolio"]
autocomplete_fields = [
@@ -4566,6 +5056,38 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
change_form_template = "django/admin/suborg_change_form.html"
+ readonly_fields = []
+
+ # Even though this is empty, I will leave it as a stub for easy changes in the future
+ # rather than strip it out of our logic.
+ analyst_readonly_fields = [] # type: ignore
+
+ omb_analyst_readonly_fields = [
+ "name",
+ "portfolio",
+ "city",
+ "state_territory",
+ ]
+
+ def get_readonly_fields(self, request, obj=None):
+ """Set the read-only state on form elements.
+ We have conditions that determine which fields are read-only:
+ admin user permissions and analyst (cisa or omb) status, so
+ we'll use the baseline readonly_fields and extend it as needed.
+ """
+ readonly_fields = list(self.readonly_fields)
+
+ if request.user.has_perm("registrar.full_access_permission"):
+ return readonly_fields
+ # Return restrictive Read-only fields for OMB analysts
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ readonly_fields.extend([field for field in self.omb_analyst_readonly_fields])
+ return readonly_fields
+ # Return restrictive Read-only fields for analysts and
+ # users who might not belong to groups
+ readonly_fields.extend([field for field in self.analyst_readonly_fields])
+ return readonly_fields
+
def change_view(self, request, object_id, form_url="", extra_context=None):
"""Add suborg's related domains and requests to context"""
obj = self.get_object(request, object_id)
@@ -4583,6 +5105,30 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
extra_context = {"domain_requests": domain_requests, "domains": domains}
return super().change_view(request, object_id, form_url, extra_context)
+ def get_queryset(self, request):
+ """Custom get_queryset to filter for OMB analysts."""
+ qs = super().get_queryset(request)
+ # Check if user is in OMB analysts group
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ return qs.filter(
+ portfolio__organization_type=DomainRequest.OrganizationChoices.FEDERAL,
+ portfolio__federal_agency__federal_type=BranchChoices.EXECUTIVE,
+ )
+ return qs
+
+ def has_view_permission(self, request, obj=None):
+ """Restrict view permissions based on group membership and model attributes."""
+ if request.user.has_perm("registrar.full_access_permission"):
+ return True
+ if obj:
+ if request.user.groups.filter(name="omb_analysts_group").exists():
+ return (
+ obj.portfolio
+ and obj.portfolio.federal_agency
+ and obj.portfolio.federal_agency.federal_type == BranchChoices.EXECUTIVE
+ )
+ return super().has_view_permission(request, obj)
+
class AllowedEmailAdmin(ListHeaderAdmin):
class Meta:
diff --git a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js
index db6467875..b9084494a 100644
--- a/src/registrar/assets/src/js/getgov-admin/domain-request-form.js
+++ b/src/registrar/assets/src/js/getgov-admin/domain-request-form.js
@@ -105,8 +105,10 @@ export function initApprovedDomain() {
return;
}
- const statusToCheck = "approved";
+ const statusToCheck = "approved"; // when checking against a select
+ const readonlyStatusToCheck = "Approved"; // when checking against a readonly div display value
const statusSelect = document.getElementById("id_status");
+ const statusField = document.querySelector("field-status");
const sessionVariableName = "showApprovedDomain";
let approvedDomainFormGroup = document.querySelector(".field-approved_domain");
@@ -120,18 +122,32 @@ export function initApprovedDomain() {
// Handle showing/hiding the related fields on page load.
function initializeFormGroups() {
- let isStatus = statusSelect.value == statusToCheck;
+ // Status is either in a select or in a readonly div. Both
+ // cases are handled below.
+ let isStatus = false;
+ if (statusSelect) {
+ isStatus = statusSelect.value == statusToCheck;
+ } else {
+ // statusSelect does not exist, indicating readonly
+ if (statusField) {
+ let readonlyDiv = statusField.querySelector("div.readonly");
+ let readonlyStatusText = readonlyDiv.textContent.trim();
+ isStatus = readonlyStatusText == readonlyStatusToCheck;
+ }
+ }
// Initial handling of these groups.
updateFormGroupVisibility(isStatus);
- // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
- statusSelect.addEventListener('change', () => {
- // Show the approved if the status is what we expect.
- isStatus = statusSelect.value == statusToCheck;
- updateFormGroupVisibility(isStatus);
- addOrRemoveSessionBoolean(sessionVariableName, isStatus);
- });
+ if (statusSelect) {
+ // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
+ statusSelect.addEventListener('change', () => {
+ // Show the approved if the status is what we expect.
+ isStatus = statusSelect.value == statusToCheck;
+ updateFormGroupVisibility(isStatus);
+ addOrRemoveSessionBoolean(sessionVariableName, isStatus);
+ });
+ }
// Listen to Back/Forward button navigation and handle approvedDomainFormGroup display based on session storage
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
@@ -322,6 +338,7 @@ class CustomizableEmailBase {
* @property {HTMLElement} modalConfirm - The confirm button in the modal.
* @property {string} apiUrl - The API URL for fetching email content.
* @property {string} statusToCheck - The status to check against. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
+ * @property {string} readonlyStatusToCheck - The status to check against when readonly. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
* @property {string} sessionVariableName - The session variable name. Used for show/hide on textAreaFormGroup/dropdownFormGroup.
* @property {string} apiErrorMessage - The error message that the ajax call returns.
*/
@@ -338,6 +355,7 @@ class CustomizableEmailBase {
this.textAreaFormGroup = config.textAreaFormGroup;
this.dropdownFormGroup = config.dropdownFormGroup;
this.statusToCheck = config.statusToCheck;
+ this.readonlyStatusToCheck = config.readonlyStatusToCheck;
this.sessionVariableName = config.sessionVariableName;
// Non-configurable variables
@@ -363,19 +381,31 @@ class CustomizableEmailBase {
// Handle showing/hiding the related fields on page load.
initializeFormGroups() {
- let isStatus = this.statusSelect.value == this.statusToCheck;
+ let isStatus = false;
+ if (this.statusSelect) {
+ isStatus = this.statusSelect.value == this.statusToCheck;
+ } else {
+ // statusSelect does not exist, indicating readonly
+ if (this.dropdownFormGroup) {
+ let readonlyDiv = this.dropdownFormGroup.querySelector("div.readonly");
+ let readonlyStatusText = readonlyDiv.textContent.trim();
+ isStatus = readonlyStatusText == this.readonlyStatusToCheck;
+ }
+ }
// Initial handling of these groups.
this.updateFormGroupVisibility(isStatus);
- // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
- this.statusSelect.addEventListener('change', () => {
- // Show the action needed field if the status is what we expect.
- // Then track if its shown or hidden in our session cache.
- isStatus = this.statusSelect.value == this.statusToCheck;
- this.updateFormGroupVisibility(isStatus);
- addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
- });
+ if (this.statusSelect) {
+ // Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
+ this.statusSelect.addEventListener('change', () => {
+ // Show the action needed field if the status is what we expect.
+ // Then track if its shown or hidden in our session cache.
+ isStatus = this.statusSelect.value == this.statusToCheck;
+ this.updateFormGroupVisibility(isStatus);
+ addOrRemoveSessionBoolean(this.sessionVariableName, isStatus);
+ });
+ }
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
@@ -403,58 +433,66 @@ class CustomizableEmailBase {
}
initializeDropdown() {
- this.dropdown.addEventListener("change", () => {
- let reason = this.dropdown.value;
- if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
- let searchParams = new URLSearchParams(
- {
- "reason": reason,
- "domain_request_id": this.domainRequestId,
- }
- );
- // Replace the email content
- fetch(`${this.apiUrl}?${searchParams.toString()}`)
- .then(response => {
- return response.json().then(data => data);
- })
- .then(data => {
- if (data.error) {
- console.error("Error in AJAX call: " + data.error);
- }else {
- this.textarea.value = data.email;
- }
- this.updateUserInterface(reason);
- })
- .catch(error => {
- console.error(this.apiErrorMessage, error)
- });
- }
- });
+ if (this.dropdown) {
+ this.dropdown.addEventListener("change", () => {
+ let reason = this.dropdown.value;
+ if (this.initialDropdownValue !== this.dropdown.value || this.initialEmailValue !== this.textarea.value) {
+ let searchParams = new URLSearchParams(
+ {
+ "reason": reason,
+ "domain_request_id": this.domainRequestId,
+ }
+ );
+ // Replace the email content
+ fetch(`${this.apiUrl}?${searchParams.toString()}`)
+ .then(response => {
+ return response.json().then(data => data);
+ })
+ .then(data => {
+ if (data.error) {
+ console.error("Error in AJAX call: " + data.error);
+ }else {
+ this.textarea.value = data.email;
+ }
+ this.updateUserInterface(reason);
+ })
+ .catch(error => {
+ console.error(this.apiErrorMessage, error)
+ });
+ }
+ });
+ }
}
initializeModalConfirm() {
- this.modalConfirm.addEventListener("click", () => {
- this.textarea.removeAttribute('readonly');
- this.textarea.focus();
+ // When the modal confirm button is present, add a listener
+ if (this.modalConfirm) {
+ this.modalConfirm.addEventListener("click", () => {
+ this.textarea.removeAttribute('readonly');
+ this.textarea.focus();
hideElement(this.directEditButton);
hideElement(this.modalTrigger);
- });
+ });
+ }
}
initializeDirectEditButton() {
- this.directEditButton.addEventListener("click", () => {
- this.textarea.removeAttribute('readonly');
- this.textarea.focus();
+ // When the direct edit button is present, add a listener
+ if (this.directEditButton) {
+ this.directEditButton.addEventListener("click", () => {
+ this.textarea.removeAttribute('readonly');
+ this.textarea.focus();
hideElement(this.directEditButton);
hideElement(this.modalTrigger);
- });
+ });
+ }
}
isEmailAlreadySent() {
return this.lastSentEmailContent.value.replace(/\s+/g, '') === this.textarea.value.replace(/\s+/g, '');
}
- updateUserInterface(reason=this.dropdown.value, excluded_reasons=["other"]) {
+ updateUserInterface(reason, excluded_reasons=["other"]) {
if (!reason) {
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
this.showPlaceholderNoReason();
@@ -468,23 +506,25 @@ class CustomizableEmailBase {
// Helper function that makes overriding the readonly textarea easy
showReadonlyTextarea() {
- // A triggering selection is selected, all hands on board:
- this.textarea.setAttribute('readonly', true);
- showElement(this.textarea);
- hideElement(this.textareaPlaceholder);
+ if (this.textarea && this.textareaPlaceholder) {
+ // A triggering selection is selected, all hands on board:
+ this.textarea.setAttribute('readonly', true);
+ showElement(this.textarea);
+ hideElement(this.textareaPlaceholder);
- if (this.isEmailAlreadySentConst) {
- hideElement(this.directEditButton);
- showElement(this.modalTrigger);
+ if (this.isEmailAlreadySentConst) {
+ hideElement(this.directEditButton);
+ showElement(this.modalTrigger);
+ } else {
+ showElement(this.directEditButton);
+ hideElement(this.modalTrigger);
+ }
+
+ if (this.isEmailAlreadySent()) {
+ this.formLabel.innerHTML = "Email sent to creator:";
} else {
- showElement(this.directEditButton);
- hideElement(this.modalTrigger);
- }
-
- if (this.isEmailAlreadySent()) {
- this.formLabel.innerHTML = "Email sent to creator:";
- } else {
- this.formLabel.innerHTML = "Email:";
+ this.formLabel.innerHTML = "Email:";
+ }
}
}
@@ -516,9 +556,10 @@ class customActionNeededEmail extends CustomizableEmailBase {
lastSentEmailContent: document.getElementById("last-sent-action-needed-email-content"),
modalConfirm: document.getElementById("action-needed-reason__confirm-edit-email"),
apiUrl: document.getElementById("get-action-needed-email-for-user-json")?.value || null,
- textAreaFormGroup: document.querySelector('.field-action_needed_reason'),
- dropdownFormGroup: document.querySelector('.field-action_needed_reason_email'),
+ textAreaFormGroup: document.querySelector('.field-action_needed_reason_email'),
+ dropdownFormGroup: document.querySelector('.field-action_needed_reason'),
statusToCheck: "action needed",
+ readonlyStatusToCheck: "Action needed",
sessionVariableName: "showActionNeededReason",
apiErrorMessage: "Error when attempting to grab action needed email: "
}
@@ -529,7 +570,15 @@ class customActionNeededEmail extends CustomizableEmailBase {
// Hide/show the email fields depending on the current status
this.initializeFormGroups();
// Setup the textarea, edit button, helper text
- this.updateUserInterface();
+ let reason = null;
+ if (this.dropdown) {
+ reason = this.dropdown.value;
+ } else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) {
+ if (this.dropdownFormGroup.querySelector("div.readonly").textContent) {
+ reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim()
+ }
+ }
+ this.updateUserInterface(reason);
this.initializeDropdown();
this.initializeModalConfirm();
this.initializeDirectEditButton();
@@ -560,12 +609,6 @@ export function initActionNeededEmail() {
// Initialize UI
const customEmail = new customActionNeededEmail();
- // Check that every variable was setup correctly
- const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
- if (nullItems.length > 0) {
- console.error(`Failed to load customActionNeededEmail(). Some variables were null: ${nullItems.join(", ")}`)
- return;
- }
customEmail.loadActionNeededEmail()
});
}
@@ -581,6 +624,7 @@ class customRejectedEmail extends CustomizableEmailBase {
textAreaFormGroup: document.querySelector('.field-rejection_reason'),
dropdownFormGroup: document.querySelector('.field-rejection_reason_email'),
statusToCheck: "rejected",
+ readonlyStatusToCheck: "Rejected",
sessionVariableName: "showRejectionReason",
errorMessage: "Error when attempting to grab rejected email: "
};
@@ -589,7 +633,15 @@ class customRejectedEmail extends CustomizableEmailBase {
loadRejectedEmail() {
this.initializeFormGroups();
- this.updateUserInterface();
+ let reason = null;
+ if (this.dropdown) {
+ reason = this.dropdown.value;
+ } else if (this.dropdownFormGroup && this.dropdownFormGroup.querySelector("div.readonly")) {
+ if (this.dropdownFormGroup.querySelector("div.readonly").textContent) {
+ reason = this.dropdownFormGroup.querySelector("div.readonly").textContent.trim()
+ }
+ }
+ this.updateUserInterface(reason);
this.initializeDropdown();
this.initializeModalConfirm();
this.initializeDirectEditButton();
@@ -600,7 +652,7 @@ class customRejectedEmail extends CustomizableEmailBase {
this.showPlaceholder("Email:", "Select a rejection reason to see email");
}
- updateUserInterface(reason=this.dropdown.value, excluded_reasons=[]) {
+ updateUserInterface(reason, excluded_reasons=[]) {
super.updateUserInterface(reason, excluded_reasons);
}
}
@@ -619,12 +671,6 @@ export function initRejectedEmail() {
// Initialize UI
const customEmail = new customRejectedEmail();
- // Check that every variable was setup correctly
- const nullItems = Object.entries(customEmail.config).filter(([key, value]) => value === null).map(([key]) => key);
- if (nullItems.length > 0) {
- console.error(`Failed to load customRejectedEmail(). Some variables were null: ${nullItems.join(", ")}`)
- return;
- }
customEmail.loadRejectedEmail()
});
}
@@ -648,7 +694,6 @@ function handleSuborgFieldsAndButtons() {
// Ensure that every variable is present before proceeding
if (!requestedSuborganizationField || !suborganizationCity || !suborganizationStateTerritory || !rejectButton) {
- console.warn("handleSuborganizationSelection() => Could not find required fields.")
return;
}
diff --git a/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js b/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js
index 9a60e1684..3880e63fa 100644
--- a/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js
+++ b/src/registrar/assets/src/js/getgov-admin/helpers-portfolio-dynamic-fields.js
@@ -12,7 +12,9 @@ export function handlePortfolioSelection(
suborgDropdownSelector="#id_sub_organization"
) {
// These dropdown are select2 fields so they must be interacted with via jquery
+ // In the event that these fields are readonly, need a variable to reference their row
const portfolioDropdown = django.jQuery(portfolioDropdownSelector);
+ const portfolioField = document.querySelector(".field-portfolio");
const suborganizationDropdown = django.jQuery(suborgDropdownSelector);
const suborganizationField = document.querySelector(".field-sub_organization");
const requestedSuborganizationField = document.querySelector(".field-requested_suborganization");
@@ -394,17 +396,33 @@ export function handlePortfolioSelection(
* - Various global field elements (e.g., `suborganizationField`, `seniorOfficialField`, `portfolioOrgTypeFieldSet`) are used.
*/
function updatePortfolioFieldsDisplay() {
- // Retrieve the selected portfolio ID
- let portfolio_id = portfolioDropdown.val();
+ let portfolio_id = null;
+ let portfolio_selected = false;
+ // portfolio will be either readonly or a dropdown, handle both cases
+ if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM
+ // Retrieve the selected portfolio ID
+ portfolio_id = portfolioDropdown.val();
+ if (portfolio_id) {
+ portfolio_selected = true;
+ }
+ } else {
+ // get readonly field value
+ let portfolio = portfolioField.querySelector(".readonly").innerText;
+ if (portfolio != "-") {
+ portfolio_selected = true;
+ }
+ }
- if (portfolio_id) {
+ if (portfolio_selected) {
// A portfolio is selected - update suborganization dropdown and show/hide relevant fields
- // Update suborganization dropdown for the selected portfolio
- updateSubOrganizationDropdown(portfolio_id);
+ if (portfolio_id) {
+ // Update suborganization dropdown for the selected portfolio
+ updateSubOrganizationDropdown(portfolio_id);
+ }
// Show fields relevant to a selected portfolio
- showElement(suborganizationField);
+ if (suborganizationField) showElement(suborganizationField);
hideElement(seniorOfficialField);
showElement(portfolioSeniorOfficialField);
@@ -427,7 +445,7 @@ export function handlePortfolioSelection(
// No portfolio is selected - reverse visibility of fields
// Hide suborganization field as no portfolio is selected
- hideElement(suborganizationField);
+ if (suborganizationField) hideElement(suborganizationField);
// Show fields that are relevant when no portfolio is selected
showElement(seniorOfficialField);
@@ -468,10 +486,22 @@ export function handlePortfolioSelection(
* This function ensures the form dynamically reflects whether a specific suborganization is being selected or requested.
*/
function updateSuborganizationFieldsDisplay() {
- let portfolio_id = portfolioDropdown.val();
+ let portfolio_selected = false;
+ // portfolio will be either readonly or a dropdown, handle both cases
+ if (portfolioDropdown.length) { // need to test length since the query will always be defined, even if not in DOM
+ // Retrieve the selected portfolio ID
+ if (portfolioDropdown.val()) {
+ portfolio_selected = true;
+ }
+ } else {
+ // get readonly field value
+ if (portfolioField.querySelector(".readonly").innerText != "-") {
+ portfolio_selected = true;
+ }
+ }
let suborganization_id = suborganizationDropdown.val();
- if (portfolio_id && !suborganization_id) {
+ if (portfolio_selected && !suborganization_id) {
// Show suborganization request fields
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
if (suborganizationCity) showElement(suborganizationCity);
diff --git a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js
index 74729c2b2..7777dabdb 100644
--- a/src/registrar/assets/src/js/getgov-admin/portfolio-form.js
+++ b/src/registrar/assets/src/js/getgov-admin/portfolio-form.js
@@ -21,6 +21,8 @@ function handlePortfolioFields(){
const federalTypeField = document.querySelector(".field-federal_type");
const urbanizationField = document.querySelector(".field-urbanization");
const stateTerritoryDropdown = document.getElementById("id_state_territory");
+ const stateTerritoryField = document.querySelector(".field-state_territory");
+ const stateTerritoryReadonly = stateTerritoryField.querySelector(".readonly");
const seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
const seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
const federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
@@ -85,9 +87,9 @@ function handlePortfolioFields(){
* 2. else show org name, hide federal agency, hide federal type if applicable
*/
function handleOrganizationTypeChange() {
- if (organizationTypeDropdown && organizationNameField) {
- let selectedValue = organizationTypeDropdown.value;
- if (selectedValue === "federal") {
+ if (organizationTypeField && organizationNameField) {
+ let selectedValue = organizationTypeDropdown ? organizationTypeDropdown.value : organizationTypeReadonly.innerText;
+ if (selectedValue === "federal" || selectedValue === "Federal") {
hideElement(organizationNameField);
showElement(federalAgencyField);
if (federalTypeField) {
@@ -207,8 +209,8 @@ function handlePortfolioFields(){
* Handle urbanization
*/
function handleStateTerritoryChange() {
- let selectedValue = stateTerritoryDropdown.value;
- if (selectedValue === "PR") {
+ let selectedValue = stateTerritoryDropdown ? stateTerritoryDropdown.value : stateTerritoryReadonly.innerText;
+ if (selectedValue === "PR" || selectedValue === "Puerto Rico (PR)") {
showElement(urbanizationField)
} else {
hideElement(urbanizationField)
@@ -265,7 +267,7 @@ function handlePortfolioFields(){
* Initializes necessary data and display configurations for the portfolio fields.
*/
function initializePortfolioSettings() {
- if (urbanizationField && stateTerritoryDropdown) {
+ if (urbanizationField && stateTerritoryField) {
handleStateTerritoryChange();
}
handleOrganizationTypeChange();
@@ -285,9 +287,11 @@ function handlePortfolioFields(){
handleStateTerritoryChange();
});
}
- organizationTypeDropdown.addEventListener("change", function() {
- handleOrganizationTypeChange();
- });
+ if (organizationTypeDropdown) {
+ organizationTypeDropdown.addEventListener("change", function() {
+ handleOrganizationTypeChange();
+ });
+ }
}
// Run initial setup functions
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..6d05dfa9b
--- /dev/null
+++ b/src/registrar/assets/src/js/getgov/form-dsdata.js
@@ -0,0 +1,521 @@
+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 data 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 inputs = document.querySelectorAll("input[type='text'], textarea");
+ inputs.forEach(input => {
+ input.addEventListener("input", () => {
+ this.formChanged = true;
+ });
+ });
+
+ const selects = document.querySelectorAll("select");
+ selects.forEach(select => {
+ select.addEventListener("change", () => {
+ 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 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');
+ 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 = () => {
+ // 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);
+ // focus on key tag in the form
+ let keyTagInput = this.addDSDataForm.querySelector('input[name$="-key_tag"]');
+ if (keyTagInput) {
+ keyTagInput.focus();
+ }
+ } else {
+ this.addAlert("error", "Youβve reached the maximum amount of DS Data records (8). To add another record, youβll need to delete one of your saved records.");
+ }
+ };
+ 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.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();
+ }
+ }
+
+ /**
+ * 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');
+ 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 {
+ this.executeCallback();
+ }
+ }
+
+ /**
+ * 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 inputs
+ const inputs = document.querySelectorAll("input[type='text'], textarea");
+ 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 keyTagInput = editRow.querySelector("input[type='text']");
+ let selects = editRow.querySelectorAll("select");
+ let digestInput = editRow.querySelector("textarea");
+ let tds = readOnlyRow.querySelectorAll("td");
+
+ // Copy the key tag input value
+ if (keyTagInput) {
+ tds[0].innerText = keyTagInput.value || "";
+ }
+
+ // Copy select values (showing the selected label instead of value)
+ if (selects[0]) {
+ let selectedOption = selects[0].options[selects[0].selectedIndex];
+ if (tds[1]) {
+ tds[1].innerHTML = `${selectedOption ? selectedOption.text : ""}`;
+ }
+ }
+ if (selects[1]) {
+ let selectedOption = selects[1].options[selects[1].selectedIndex];
+ if (tds[2]) {
+ tds[2].innerText = selectedOption ? selectedOption.text : "";
+ }
+ }
+
+ // Copy the digest input value
+ if (digestInput) {
+ tds[3].innerHTML = `${digestInput.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..6c1d37d57 100644
--- a/src/registrar/assets/src/js/getgov/form-helpers.js
+++ b/src/registrar/assets/src/js/getgov/form-helpers.js
@@ -38,6 +38,16 @@ 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");
+ });
+
+ // Remove the 'usa-input--error' class from all textarea elements
+ domElement.querySelectorAll("textarea.usa-input--error").forEach(textarea => {
+ textarea.classList.remove("usa-input--error");
+ });
}
/**
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/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 f077448aa..490874220 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, hookupCallbacksToRadioToggler } 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 { domain_purpose_choice_callbacks } from './domain-purpose-form.js';
@@ -22,12 +22,14 @@ 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);
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
+hookupYesNoListener("portfolio_additional_details-working_with_eop", "eop-contact-container", null);
+hookupYesNoListener("portfolio_additional_details-has_anything_else_text", 'anything-else-details-container', null);
hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
@@ -35,7 +37,6 @@ hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choic
hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
-
initializeUrbanizationToggle();
userProfileListener();
@@ -51,7 +52,6 @@ initEditMemberDomainsTable();
initDomainRequestForm();
initDomainManagersPage();
-initDomainDSData();
initDomainDNSSEC();
initFormErrorHandling();
diff --git a/src/registrar/assets/src/js/getgov/table-base.js b/src/registrar/assets/src/js/getgov/table-base.js
index bf561fa1f..54b8db3f1 100644
--- a/src/registrar/assets/src/js/getgov/table-base.js
+++ b/src/registrar/assets/src/js/getgov/table-base.js
@@ -103,7 +103,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
{% endfor %}
+ {% if perms.registrar.analyst_access_permission or perms.full_access_permission %}
Analytics
@@ -78,6 +79,7 @@
+ {% endif %}
{% else %}
{% translate 'You donβt have permission to view or edit anything.' %}
+ {% if not adminform.form.is_omb_analyst %}
{# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
+ {% endif %}
- {% if original.state != original.State.DELETED %}
+ {% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
Extend expiration date
@@ -31,9 +33,11 @@
{% endif %}
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
+ {% if not adminform.form.is_omb_analyst %}
|
{% endif %}
- {% if original.state != original.State.DELETED %}
+ {% endif %}
+ {% if original.state != original.State.DELETED and not adminform.form.is_omb_analyst %}
Remove from registry
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 0baabac17..b3cdeb875 100644
--- a/src/registrar/templates/django/admin/includes/contact_detail_list.html
+++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html
@@ -6,7 +6,11 @@
{% if show_formatted_name %}
{% if user.get_formatted_name %}
- {{ user.get_formatted_name }}
+ {% if adminform.form.show_contact_as_plain_text %}
+ {{ user.get_formatted_name }}
+ {% else %}
+ {{ user.get_formatted_name }}
+ {% endif %}
{% else %}
None
{% endif %}
diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
index f12bd67f9..dcf393d82 100644
--- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
+++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
@@ -69,7 +69,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% elif field.field.name == "portfolio_senior_official" %}
{% if original_object.portfolio.senior_official %}
- {{ field.contents }}
+ {% if adminform.form.show_contact_as_plain_text %}
+ {{ field.contents|striptags }}
+ {% else %}
+ {{ field.contents }}
+ {% endif %}
{% else %}
No senior official found.
{% endif %}
@@ -78,7 +82,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% if all_contacts.count > 2 %}
{% for contact in all_contacts %}
- {{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %}
+ {% if adminform.form.show_contact_as_plain_text %}
+ {{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %}
+ {% else %}
+ {{ contact.get_formatted_name }}{% if not forloop.last %}, {% endif %}
+ {% endif %}
{% endfor %}
{% else %}
@@ -153,6 +161,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
No additional members found.
{% endif %}
+ {% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
+
{{ field.contents|striptags }}
+ {% elif field.field.name == "senior_official" and adminform.form.show_contact_as_plain_text %}
+
{{ field.contents|striptags }}
{% else %}
{{ field.contents }}
{% endif %}
diff --git a/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html b/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html
index 7add74323..574c05738 100644
--- a/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html
+++ b/src/registrar/templates/django/admin/includes/portfolio/portfolio_admins_table.html
@@ -16,7 +16,11 @@
{% for admin in admins %}
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
{% if admin.user.email %}
diff --git a/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html b/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html
index 87b56cb60..54ac502d1 100644
--- a/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html
+++ b/src/registrar/templates/django/admin/includes/portfolio/portfolio_fieldset.html
@@ -30,6 +30,9 @@
No senior official found. Create one now.
{% endif %}
+
+ {% elif field.field.name == "creator" and adminform.form.show_contact_as_plain_text %}
+
{{ field.contents|striptags }}
{% else %}
{{ field.contents }}
{% endif %}
diff --git a/src/registrar/templates/django/forms/widgets/textarea.html b/src/registrar/templates/django/forms/widgets/textarea.html
index 068f15399..ffe5dcd4a 100644
--- a/src/registrar/templates/django/forms/widgets/textarea.html
+++ b/src/registrar/templates/django/forms/widgets/textarea.html
@@ -1,5 +1,5 @@
\ No newline at end of file
diff --git a/src/registrar/templates/domain_base.html b/src/registrar/templates/domain_base.html
index 60c931dce..5637fa52b 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 95e8e3d5f..ceab937bc 100644
--- a/src/registrar/templates/domain_dsdata.html
+++ b/src/registrar/templates/domain_dsdata.html
@@ -34,122 +34,376 @@
{% 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. You can add a maximum of 8 DS records.
+
+ {% 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" %}
+
+
+
+ {% else %}
+
+ {% comment %}
+ This section renders Add DS Data form which renders when there are no existing
+ DS records defined on the domain.
+ {% endcomment %}
+
+
+ {% include "includes/required_fields.html" %}
+
+
+
+
{% 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/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" %}
+
+ {% 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" %}
+
+
+ Trigger Disable DNSSEC Modal
- {% 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" %}
-
-
{% endblock %} {# domain_content #}
diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html
index bd8216d05..acd73cc3b 100644
--- a/src/registrar/templates/domain_nameservers.html
+++ b/src/registrar/templates/domain_nameservers.html
@@ -82,7 +82,7 @@
This section does not render if the last form has initial data (this occurs if 13 nameservers already exist)
{% endcomment %}
-
+
{{ form.domain }}