diff --git a/docs/developer/adding-feature-flags.md b/docs/developer/adding-feature-flags.md index dd97c7497..dc51b9e85 100644 --- a/docs/developer/adding-feature-flags.md +++ b/docs/developer/adding-feature-flags.md @@ -9,10 +9,10 @@ We use [django-waffle](https://waffle.readthedocs.io/en/stable/) for our feature 3. Click `Add waffle flag`. 4. Add the model as you would normally. Refer to waffle's documentation [regarding attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) for more information on them. -### Enabling the profile_feature flag +### Enabling a feature flag 1. On the app, navigate to `\admin`. 2. Under models, click `Waffle flags`. -3. Click the `profile_feature` record. This should exist by default, if not - create one with that name. +3. Click the featue flag record. This should exist by default, if not - create one with that name. 4. (Important) Set the field `Everyone` to `Unknown`. This field overrides all other settings when set to anything else. 5. Configure the settings as you see fit. diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index b85ea6335..35b2e3971 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -126,7 +126,15 @@ class AvailableAPITest(MockEppLib): def setUp(self): super().setUp() - self.user = get_user_model().objects.create(username="username") + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + title = "title" + phone = "8080102431" + self.user = get_user_model().objects.create( + username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone + ) def test_available_get(self): self.client.force_login(self.user) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 1c8551d4e..40c2bc050 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1977,11 +1977,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): so we should display that information using this function. """ - - if hasattr(obj, "creator"): - recipient = obj.creator - else: - recipient = None + recipient = obj.creator # Displays a warning in admin when an email cannot be sent if recipient and recipient.email: @@ -2186,7 +2182,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries emails = self.get_all_action_needed_reason_emails(obj) extra_context["action_needed_reason_emails"] = json.dumps(emails) - extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") # Denote if an action needed email was sent or not email_sent = request.session.get("action_needed_email_sent", False) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index cd42fd322..027ef4344 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -33,6 +33,14 @@ const showElement = (element) => { element.classList.remove('display-none'); }; +/** + * Helper function to get the CSRF token from the cookie + * +*/ +function getCsrfToken() { + return document.querySelector('input[name="csrfmiddlewaretoken"]').value; +} + /** * Helper function that scrolls to an element * @param {string} attributeName - The string "class" or "id" @@ -994,59 +1002,169 @@ function unloadModals() { window.modal.off(); } -/** +class LoadTableBase { + constructor(tableSelector, tableWrapperSelector, searchFieldId, searchSubmitId, resetSearchBtn, resetFiltersBtn, noDataDisplay, noSearchresultsDisplay) { + this.tableWrapper = document.querySelector(tableWrapperSelector); + this.tableHeaders = document.querySelectorAll(`${tableSelector} th[data-sortable]`); + this.currentSortBy = 'id'; + this.currentOrder = 'asc'; + this.currentStatus = []; + this.currentSearchTerm = ''; + this.scrollToTable = false; + this.searchInput = document.querySelector(searchFieldId); + this.searchSubmit = document.querySelector(searchSubmitId); + this.tableAnnouncementRegion = document.querySelector(`${tableWrapperSelector} .usa-table__announcement-region`); + this.resetSearchButton = document.querySelector(resetSearchBtn); + this.resetFiltersButton = document.querySelector(resetFiltersBtn); + // NOTE: these 3 can't be used if filters are active on a page with more than 1 table + this.statusCheckboxes = document.querySelectorAll('input[name="filter-status"]'); + this.statusIndicator = document.querySelector('.filter-indicator'); + this.statusToggle = document.querySelector('.usa-button--filter'); + this.noTableWrapper = document.querySelector(noDataDisplay); + this.noSearchResultsWrapper = document.querySelector(noSearchresultsDisplay); + this.portfolioElement = document.getElementById('portfolio-js-value'); + this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; + this.initializeTableHeaders(); + this.initializeSearchHandler(); + this.initializeStatusToggleHandler(); + this.initializeFilterCheckboxes(); + this.initializeResetSearchButton(); + this.initializeResetFiltersButton(); + this.initializeAccordionAccessibilityListeners(); + } + + /** * Generalized function to update pagination for a list. * @param {string} itemName - The name displayed in the counter * @param {string} paginationSelector - CSS selector for the pagination container. * @param {string} counterSelector - CSS selector for the pagination counter. - * @param {string} linkAnchor - CSS selector for the header element to anchor the links to. - * @param {Function} loadPageFunction - Function to call when a page link is clicked. + * @param {string} tableSelector - CSS selector for the header element to anchor the links to. * @param {number} currentPage - The current page number (starting with 1). * @param {number} numPages - The total number of pages. * @param {boolean} hasPrevious - Whether there is a page before the current page. * @param {boolean} hasNext - Whether there is a page after the current page. - * @param {number} totalItems - The total number of items. - * @param {string} searchTerm - The search term - */ -function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) { - const paginationContainer = document.querySelector(paginationSelector); - const paginationCounter = document.querySelector(counterSelector); - const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); - paginationCounter.innerHTML = ''; - paginationButtons.innerHTML = ''; + * @param {number} total - The total number of items. + */ + updatePagination( + itemName, + paginationSelector, + counterSelector, + parentTableSelector, + currentPage, + numPages, + hasPrevious, + hasNext, + totalItems, + ) { + const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); + const counterSelectorEl = document.querySelector(counterSelector); + const paginationSelectorEl = document.querySelector(paginationSelector); + counterSelectorEl.innerHTML = ''; + paginationButtons.innerHTML = ''; - // Buttons should only be displayed if there are more than one pages of results - paginationButtons.classList.toggle('display-none', numPages <= 1); + // Buttons should only be displayed if there are more than one pages of results + paginationButtons.classList.toggle('display-none', numPages <= 1); - // Counter should only be displayed if there is more than 1 item - paginationContainer.classList.toggle('display-none', totalItems < 1); + // Counter should only be displayed if there is more than 1 item + paginationSelectorEl.classList.toggle('display-none', totalItems < 1); - paginationCounter.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${searchTerm ? ' for ' + '"' + searchTerm + '"' : ''}`; + counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`; - if (hasPrevious) { - const prevPageItem = document.createElement('li'); - prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - prevPageItem.innerHTML = ` - - - Previous - - `; - prevPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - loadPageFunction(currentPage - 1); - }); - paginationButtons.appendChild(prevPageItem); + if (hasPrevious) { + const prevPageItem = document.createElement('li'); + prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; + prevPageItem.innerHTML = ` + + + Previous + + `; + prevPageItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(currentPage - 1); + }); + paginationButtons.appendChild(prevPageItem); + } + + // Add first page and ellipsis if necessary + if (currentPage > 2) { + paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage)); + if (currentPage > 3) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; + ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); + ellipsis.innerHTML = ''; + paginationButtons.appendChild(ellipsis); + } + } + + // Add pages around the current page + for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { + paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage)); + } + + // Add last page and ellipsis if necessary + if (currentPage < numPages - 1) { + if (currentPage < numPages - 2) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; + ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); + ellipsis.innerHTML = ''; + paginationButtons.appendChild(ellipsis); + } + paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage)); + } + + if (hasNext) { + const nextPageItem = document.createElement('li'); + nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; + nextPageItem.innerHTML = ` + + Next + + + `; + nextPageItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(currentPage + 1); + }); + paginationButtons.appendChild(nextPageItem); + } } + /** + * A helper that toggles content/ no content/ no search results + * + */ + updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { + const { unfiltered_total, total } = data; + if (unfiltered_total) { + if (total) { + showElement(dataWrapper); + hideElement(noSearchResultsWrapper); + hideElement(noDataWrapper); + } else { + hideElement(dataWrapper); + showElement(noSearchResultsWrapper); + hideElement(noDataWrapper); + } + } else { + hideElement(dataWrapper); + hideElement(noSearchResultsWrapper); + showElement(noDataWrapper); + } + }; + // Helper function to create a page item - function createPageItem(page) { + createPageItem(page, parentTableSelector, currentPage) { const pageItem = document.createElement('li'); pageItem.className = 'usa-pagination__item usa-pagination__page-no'; pageItem.innerHTML = ` - ${page} + ${page} `; if (page === currentPage) { pageItem.querySelector('a').classList.add('usa-current'); @@ -1054,134 +1172,212 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc } pageItem.querySelector('a').addEventListener('click', (event) => { event.preventDefault(); - loadPageFunction(page); + this.loadTable(page); }); return pageItem; } - // Add first page and ellipsis if necessary - if (currentPage > 2) { - paginationButtons.appendChild(createPageItem(1)); - if (currentPage > 3) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } + /** + * A helper that resets sortable table headers + * + */ + unsetHeader = (header) => { + header.removeAttribute('aria-sort'); + let headerName = header.innerText; + const headerLabel = `${headerName}, sortable column, currently unsorted"`; + const headerButtonLabel = `Click to sort by ascending order.`; + header.setAttribute("aria-label", headerLabel); + header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); + }; + + // Abstract method (to be implemented in the child class) + loadTable(page, sortBy, order) { + throw new Error('loadData() must be implemented in a subclass'); } - // Add pages around the current page - for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { - paginationButtons.appendChild(createPageItem(i)); - } - - // Add last page and ellipsis if necessary - if (currentPage < numPages - 1) { - if (currentPage < numPages - 2) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } - paginationButtons.appendChild(createPageItem(numPages)); - } - - if (hasNext) { - const nextPageItem = document.createElement('li'); - nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - nextPageItem.innerHTML = ` - - Next - - - `; - nextPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - loadPageFunction(currentPage + 1); + // Add event listeners to table headers for sorting + initializeTableHeaders() { + this.tableHeaders.forEach(header => { + header.addEventListener('click', () => { + const sortBy = header.getAttribute('data-sortable'); + let order = 'asc'; + // sort order will be ascending, unless the currently sorted column is ascending, and the user + // is selecting the same column to sort in descending order + if (sortBy === this.currentSortBy) { + order = this.currentOrder === 'asc' ? 'desc' : 'asc'; + } + // load the results with the updated sort + this.loadTable(1, sortBy, order); + }); + }); + } + + initializeSearchHandler() { + this.searchSubmit.addEventListener('click', (e) => { + e.preventDefault(); + this.currentSearchTerm = this.searchInput.value; + // If the search is blank, we match the resetSearch functionality + if (this.currentSearchTerm) { + showElement(this.resetSearchButton); + } else { + hideElement(this.resetSearchButton); + } + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + }); + } + + initializeStatusToggleHandler() { + if (this.statusToggle) { + this.statusToggle.addEventListener('click', () => { + toggleCaret(this.statusToggle); + }); + } + } + + // Add event listeners to status filter checkboxes + initializeFilterCheckboxes() { + this.statusCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', () => { + const checkboxValue = checkbox.value; + + // Update currentStatus array based on checkbox state + if (checkbox.checked) { + this.currentStatus.push(checkboxValue); + } else { + const index = this.currentStatus.indexOf(checkboxValue); + if (index > -1) { + this.currentStatus.splice(index, 1); + } + } + + // Manage visibility of reset filters button + if (this.currentStatus.length == 0) { + hideElement(this.resetFiltersButton); + } else { + showElement(this.resetFiltersButton); + } + + // Disable the auto scroll + this.scrollToTable = false; + + // Call loadTable with updated status + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + this.updateStatusIndicator(); + }); + }); + } + + // Reset UI and accessibility + resetHeaders() { + this.tableHeaders.forEach(header => { + // Unset sort UI in headers + this.unsetHeader(header); + }); + // Reset the announcement region + this.tableAnnouncementRegion.innerHTML = ''; + } + + resetSearch() { + this.searchInput.value = ''; + this.currentSearchTerm = ''; + hideElement(this.resetSearchButton); + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + } + + initializeResetSearchButton() { + if (this.resetSearchButton) { + this.resetSearchButton.addEventListener('click', () => { + this.resetSearch(); + }); + } + } + + resetFilters() { + this.currentStatus = []; + this.statusCheckboxes.forEach(checkbox => { + checkbox.checked = false; + }); + hideElement(this.resetFiltersButton); + + // Disable the auto scroll + this.scrollToTable = false; + + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + this.updateStatusIndicator(); + // No need to toggle close the filters. The focus shift will trigger that for us. + } + + initializeResetFiltersButton() { + if (this.resetFiltersButton) { + this.resetFiltersButton.addEventListener('click', () => { + this.resetFilters(); + }); + } + } + + updateStatusIndicator() { + this.statusIndicator.innerHTML = ''; + // Even if the element is empty, it'll mess up the flex layout unless we set display none + hideElement(this.statusIndicator); + if (this.currentStatus.length) + this.statusIndicator.innerHTML = '(' + this.currentStatus.length + ')'; + showElement(this.statusIndicator); + } + + closeFilters() { + if (this.statusToggle.getAttribute("aria-expanded") === "true") { + this.statusToggle.click(); + } + } + + initializeAccordionAccessibilityListeners() { + // Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter, + // user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button. + // NOTE: We may need to evolve this as we add more filters. + document.addEventListener('focusin', (event) => { + const accordion = document.querySelector('.usa-accordion--select'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + + if (accordionThatIsOpen && !accordion.contains(event.target)) { + this.closeFilters(); + } + }); + + // Close when user clicks outside + // NOTE: We may need to evolve this as we add more filters. + document.addEventListener('click', (event) => { + const accordion = document.querySelector('.usa-accordion--select'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + + if (accordionThatIsOpen && !accordion.contains(event.target)) { + this.closeFilters(); + } }); - paginationButtons.appendChild(nextPageItem); } } -/** - * A helper that toggles content/ no content/ no search results - * -*/ -const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { - const { unfiltered_total, total } = data; - if (unfiltered_total) { - if (total) { - showElement(dataWrapper); - hideElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } else { - hideElement(dataWrapper); - showElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } - } else { - hideElement(dataWrapper); - hideElement(noSearchResultsWrapper); - showElement(noDataWrapper); +class DomainsTable extends LoadTableBase { + + constructor() { + super('.domains__table', '.domains__table-wrapper', '#domains__search-field', '#domains__search-field-submit', '.domains__reset-search', '.domains__reset-filters', '.domains__no-data', '.domains__no-search-results'); } -}; - -/** - * A helper that resets sortable table headers - * -*/ -const unsetHeader = (header) => { - header.removeAttribute('aria-sort'); - let headerName = header.innerText; - const headerLabel = `${headerName}, sortable column, currently unsorted"`; - const headerButtonLabel = `Click to sort by ascending order.`; - header.setAttribute("aria-label", headerLabel); - header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); -}; - -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domains list and associated functionality on the home page of the app. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const domainsWrapper = document.querySelector('.domains__table-wrapper'); - - if (domainsWrapper) { - let currentSortBy = 'id'; - let currentOrder = 'asc'; - const noDomainsWrapper = document.querySelector('.domains__no-data'); - const noSearchResultsWrapper = document.querySelector('.domains__no-search-results'); - let scrollToTable = false; - let currentStatus = []; - let currentSearchTerm = ''; - const domainsSearchInput = document.getElementById('domains__search-field'); - const domainsSearchSubmit = document.getElementById('domains__search-field-submit'); - const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]'); - const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region'); - const resetSearchButton = document.querySelector('.domains__reset-search'); - const resetFiltersButton = document.querySelector('.domains__reset-filters'); - const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]'); - const statusIndicator = document.querySelector('.domain__filter-indicator'); - const statusToggle = document.querySelector('.usa-button--filter'); - const portfolioElement = document.getElementById('portfolio-js-value'); - const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; - - /** + /** * Loads rows in the domains list, as well as updates pagination around the domains list * based on the supplied attributes. * @param {*} page - the page number of the results (starts with 1) * @param {*} sortBy - the sort column option * @param {*} order - the sort order {asc, desc} * @param {*} scroll - control for the scrollToElement functionality + * @param {*} status - control for the status filter * @param {*} searchTerm - the search term * @param {*} portfolio - the portfolio id */ - function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) { + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + // fetch json of page of domais, given params let baseUrl = document.getElementById("get_domains_json_url"); if (!baseUrl) { @@ -1194,10 +1390,19 @@ document.addEventListener('DOMContentLoaded', function() { } // fetch json of page of domains, given params - let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}` + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "status": status, + "search_term": searchTerm + } + ); if (portfolio) - url += `&portfolio=${portfolio}` + searchParams.append("portfolio", portfolio) + let url = `${baseUrlValue}?${searchParams.toString()}` fetch(url) .then(response => response.json()) .then(data => { @@ -1207,7 +1412,7 @@ document.addEventListener('DOMContentLoaded', function() { } // handle the display of proper messaging in the event that no domains exist in the list or search returns no results - updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, currentSearchTerm); + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); // identify the DOM element where the domain list will be inserted into the DOM const domainList = document.querySelector('.domains__table tbody'); @@ -1225,7 +1430,7 @@ document.addEventListener('DOMContentLoaded', function() { let markupForSuborganizationRow = ''; - if (portfolioValue) { + if (this.portfolioValue) { markupForSuborganizationRow = ` ${suborganization} @@ -1271,181 +1476,438 @@ document.addEventListener('DOMContentLoaded', function() { // Do not scroll on first page load if (scroll) ScrollToElement('class', 'domains'); - scrollToTable = true; + this.scrollToTable = true; // update pagination - updatePagination( + this.updatePagination( 'domain', '#domains-pagination', '#domains-pagination .usa-pagination__counter', '#domains', - loadDomains, data.page, data.num_pages, data.has_previous, data.has_next, data.total, - currentSearchTerm ); - currentSortBy = sortBy; - currentOrder = order; - currentSearchTerm = searchTerm; + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; }) .catch(error => console.error('Error fetching domains:', error)); + } +} + + +class DomainRequestsTable extends LoadTableBase { + + constructor() { + super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results'); + } + /** + * Loads rows in the domains list, as well as updates pagination around the domains list + * based on the supplied attributes. + * @param {*} page - the page number of the results (starts with 1) + * @param {*} sortBy - the sort column option + * @param {*} order - the sort order {asc, desc} + * @param {*} scroll - control for the scrollToElement functionality + * @param {*} status - control for the status filter + * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id + */ + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) { + let baseUrl = document.getElementById("get_domain_requests_json_url"); + if (!baseUrl) { + return; } - // Add event listeners to table headers for sorting - tableHeaders.forEach(header => { - header.addEventListener('click', function() { - const sortBy = this.getAttribute('data-sortable'); - let order = 'asc'; - // sort order will be ascending, unless the currently sorted column is ascending, and the user - // is selecting the same column to sort in descending order - if (sortBy === currentSortBy) { - order = currentOrder === 'asc' ? 'desc' : 'asc'; - } - // load the results with the updated sort - loadDomains(1, sortBy, order); - }); - }); + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } - domainsSearchSubmit.addEventListener('click', function(e) { - e.preventDefault(); - currentSearchTerm = domainsSearchInput.value; - // If the search is blank, we match the resetSearch functionality - if (currentSearchTerm) { - showElement(resetSearchButton); - } else { - hideElement(resetSearchButton); + // add searchParams + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "status": status, + "search_term": searchTerm } - loadDomains(1, 'id', 'asc'); - resetHeaders(); - }); + ); + if (portfolio) + searchParams.append("portfolio", portfolio) - if (statusToggle) { - statusToggle.addEventListener('click', function() { - toggleCaret(statusToggle); - }); - } + let url = `${baseUrlValue}?${searchParams.toString()}` + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; + } - // Add event listeners to status filter checkboxes - statusCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', function() { - const checkboxValue = this.value; - - // Update currentStatus array based on checkbox state - if (this.checked) { - currentStatus.push(checkboxValue); + // handle the display of proper messaging in the event that no requests exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + + // identify the DOM element where the domain request list will be inserted into the DOM + const tbody = document.querySelector('.domain-requests__table tbody'); + tbody.innerHTML = ''; + + // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases + // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, + // which will cause bad delete requests to be sent. + const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); + preExistingModalPlaceholders.forEach(element => { + element.remove(); + }); + + // remove any existing modal elements from the DOM so they can be properly re-initialized + // after the DOM content changes and there are new delete modal buttons added + unloadModals(); + + let needsDeleteColumn = false; + + needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); + + // Remove existing delete th and td if they exist + let existingDeleteTh = document.querySelector('.delete-header'); + if (!needsDeleteColumn) { + if (existingDeleteTh) + existingDeleteTh.remove(); } else { - const index = currentStatus.indexOf(checkboxValue); - if (index > -1) { - currentStatus.splice(index, 1); + if (!existingDeleteTh) { + const delheader = document.createElement('th'); + delheader.setAttribute('scope', 'col'); + delheader.setAttribute('role', 'columnheader'); + delheader.setAttribute('class', 'delete-header'); + delheader.innerHTML = ` + Delete Action`; + let tableHeaderRow = document.querySelector('.domain-requests__table thead tr'); + tableHeaderRow.appendChild(delheader); } } - // Manage visibility of reset filters button - if (currentStatus.length == 0) { - hideElement(resetFiltersButton); - } else { - showElement(resetFiltersButton); - } + data.domain_requests.forEach(request => { + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const domainName = request.requested_domain ? request.requested_domain : `New domain request
(${utcDateString(request.created_at)})`; + const actionUrl = request.action_url; + const actionLabel = request.action_label; + const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; + + // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) + // Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed + let modalTrigger = ''; - // Disable the auto scroll - scrollToTable = false; + let markupCreatorRow = ''; - // Call loadDomains with updated status - loadDomains(1, 'id', 'asc'); - resetHeaders(); - updateStatusIndicator(); - }); - }); + if (this.portfolioValue) { + markupCreatorRow = ` + + ${request.creator ? request.creator : ''} + + ` + } - // Reset UI and accessibility - function resetHeaders() { - tableHeaders.forEach(header => { - // Unset sort UI in headers - unsetHeader(header); - }); - // Reset the announcement region - tableAnnouncementRegion.innerHTML = ''; - } + // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages + if (request.is_deletable) { + let modalHeading = ''; + let modalDescription = ''; - function resetSearch() { - domainsSearchInput.value = ''; - currentSearchTerm = ''; - hideElement(resetSearchButton); - loadDomains(1, 'id', 'asc'); - resetHeaders(); - } + if (request.requested_domain) { + modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + } else { + if (request.created_at) { + modalHeading = 'Are you sure you want to delete this domain request?'; + modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; + } else { + modalHeading = 'Are you sure you want to delete New domain request?'; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + } + } - if (resetSearchButton) { - resetSearchButton.addEventListener('click', function() { - resetSearch(); - }); - } + modalTrigger = ` + + Delete ${domainName} + ` - function resetFilters() { - currentStatus = []; - statusCheckboxes.forEach(checkbox => { - checkbox.checked = false; - }); - hideElement(resetFiltersButton); + const modalSubmit = ` + + ` - // Disable the auto scroll - scrollToTable = false; + const modal = document.createElement('div'); + modal.setAttribute('class', 'usa-modal'); + modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); + modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); + modal.setAttribute('aria-describedby', 'Domain will be removed'); + modal.setAttribute('data-force-action', ''); - loadDomains(1, 'id', 'asc'); - resetHeaders(); - updateStatusIndicator(); - // No need to toggle close the filters. The focus shift will trigger that for us. - } + modal.innerHTML = ` +
+
+ +
+ +
+ +
+ +
+ ` - if (resetFiltersButton) { - resetFiltersButton.addEventListener('click', function() { - resetFilters(); - }); - } + this.tableWrapper.appendChild(modal); - function updateStatusIndicator() { - statusIndicator.innerHTML = ''; - // Even if the element is empty, it'll mess up the flex layout unless we set display none - statusIndicator.hideElement(); - if (currentStatus.length) - statusIndicator.innerHTML = '(' + currentStatus.length + ')'; - statusIndicator.showElement(); - } + // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly + if (this.portfolioValue) { + modalTrigger = ` + + Delete ${domainName} + - function closeFilters() { - if (statusToggle.getAttribute("aria-expanded") === "true") { - statusToggle.click(); - } - } +
+
+ +
+ +
+ ` + } + } - // Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter, - // user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button. - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('focusin', function(event) { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - - if (accordionThatIsOpen && !accordion.contains(event.target)) { - closeFilters(); - } - }); - // Close when user clicks outside - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('click', function(event) { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + const row = document.createElement('tr'); + row.innerHTML = ` + + ${domainName} + + + ${submissionDate} + + ${markupCreatorRow} + + ${request.status} + + + + + ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} + + + ${needsDeleteColumn ? ''+modalTrigger+'' : ''} + `; + tbody.appendChild(row); + }); + + // initialize modals immediately after the DOM content is updated + initializeModals(); + + // Now the DOM and modals are ready, add listeners to the submit buttons + const modals = document.querySelectorAll('.usa-modal__content'); + + modals.forEach(modal => { + const submitButton = modal.querySelector('.usa-modal__submit'); + const closeButton = modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + let pk = submitButton.getAttribute('data-pk'); + // Close the modal to remove the USWDS UI local classes + closeButton.click(); + // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page + let pageToDisplay = data.page; + if (data.total == 1 && data.unfiltered_total > 1) { + pageToDisplay--; + } + this.deleteDomainRequest(pk, pageToDisplay); + }); + }); + + // Do not scroll on first page load + if (scroll) + ScrollToElement('class', 'domain-requests'); + this.scrollToTable = true; + + // update the pagination after the domain requests list is updated + this.updatePagination( + 'domain request', + '#domain-requests-pagination', + '#domain-requests-pagination .usa-pagination__counter', + '#domain-requests', + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; + }) + .catch(error => console.error('Error fetching domain requests:', error)); + } + + /** + * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. + * @param {*} domainRequestPk - the identifier for the request that we're deleting + * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page + */ + deleteDomainRequest(domainRequestPk, pageToDisplay) { + // Use to debug uswds modal issues + //console.log('deleteDomainRequest') - if (accordionThatIsOpen && !accordion.contains(event.target)) { - closeFilters(); + // Get csrf token + const csrfToken = getCsrfToken(); + // Create FormData object and append the CSRF token + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; + + fetch(`/domain-request/${domainRequestPk}/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': csrfToken, + }, + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + // Update data and UI + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + }) + .catch(error => console.error('Error fetching domain requests:', error)); + } +} + + +/** + * An IIFE that listens for DOM Content to be loaded, then executes. This function + * initializes the domains list and associated functionality on the home page of the app. + * + */ +document.addEventListener('DOMContentLoaded', function() { + const isDomainsPage = document.querySelector("#domains") + if (isDomainsPage){ + const domainsTable = new DomainsTable(); + if (domainsTable.tableWrapper) { + // Initial load + domainsTable.loadTable(1); + } + } +}); + +/** + * An IIFE that listens for DOM Content to be loaded, then executes. This function + * initializes the domain requests list and associated functionality on the home page of the app. + * + */ +document.addEventListener('DOMContentLoaded', function() { + const domainRequestsSectionWrapper = document.querySelector('.domain-requests'); + if (domainRequestsSectionWrapper) { + const domainRequestsTable = new DomainRequestsTable(); + if (domainRequestsTable.tableWrapper) { + domainRequestsTable.loadTable(1); + } + } + + document.addEventListener('focusin', function(event) { + closeOpenAccordions(event); + }); + + document.addEventListener('click', function(event) { + closeOpenAccordions(event); + }); + + function closeMoreActionMenu(accordionThatIsOpen) { + if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") { + accordionThatIsOpen.click(); + } + } + + function closeOpenAccordions(event) { + const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]'); + openAccordions.forEach((openAccordionButton) => { + // Find the corresponding accordion + const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); + if (accordion && !accordion.contains(event.target)) { + // Close the accordion if the click is outside + closeMoreActionMenu(openAccordionButton); } }); - - // Initial load - loadDomains(1); } }); @@ -1463,458 +1925,6 @@ const utcDateString = (dateString) => { return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`; }; -/** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domain requests list and associated functionality on the home page of the app. - * - */ -document.addEventListener('DOMContentLoaded', function() { - const domainRequestsSectionWrapper = document.querySelector('.domain-requests'); - const domainRequestsWrapper = document.querySelector('.domain-requests__table-wrapper'); - - if (domainRequestsWrapper) { - let currentSortBy = 'id'; - let currentOrder = 'asc'; - const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data'); - const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results'); - let scrollToTable = false; - let currentSearchTerm = ''; - const domainRequestsSearchInput = document.getElementById('domain-requests__search-field'); - const domainRequestsSearchSubmit = document.getElementById('domain-requests__search-field-submit'); - const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]'); - const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region'); - const resetSearchButton = document.querySelector('.domain-requests__reset-search'); - const portfolioElement = document.getElementById('portfolio-js-value'); - const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null; - - /** - * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. - * @param {*} domainRequestPk - the identifier for the request that we're deleting - * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page - */ - function deleteDomainRequest(domainRequestPk,pageToDisplay) { - - // Use to debug uswds modal issues - //console.log('deleteDomainRequest') - - // Get csrf token - const csrfToken = getCsrfToken(); - // Create FormData object and append the CSRF token - const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; - - fetch(`/domain-request/${domainRequestPk}/delete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - }, - body: formData - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Update data and UI - loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, scrollToTable, currentSearchTerm); - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } - - // Helper function to get the CSRF token from the cookie - function getCsrfToken() { - return document.querySelector('input[name="csrfmiddlewaretoken"]').value; - } - - /** - * Loads rows in the domain requests list, as well as updates pagination around the domain requests list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - */ - function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) { - // fetch json of page of domain requests, given params - let baseUrl = document.getElementById("get_domain_requests_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - // fetch json of page of requests, given params - let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}` - if (portfolio) - url += `&portfolio=${portfolio}` - - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no requests exist in the list or search returns no results - updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, currentSearchTerm); - - // identify the DOM element where the domain request list will be inserted into the DOM - const tbody = document.querySelector('.domain-requests__table tbody'); - tbody.innerHTML = ''; - - // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases - // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, - // which will cause bad delete requests to be sent. - const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); - preExistingModalPlaceholders.forEach(element => { - element.remove(); - }); - - // remove any existing modal elements from the DOM so they can be properly re-initialized - // after the DOM content changes and there are new delete modal buttons added - unloadModals(); - - let needsDeleteColumn = false; - - needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); - - // Remove existing delete th and td if they exist - let existingDeleteTh = document.querySelector('.delete-header'); - if (!needsDeleteColumn) { - if (existingDeleteTh) - existingDeleteTh.remove(); - } else { - if (!existingDeleteTh) { - const delheader = document.createElement('th'); - delheader.setAttribute('scope', 'col'); - delheader.setAttribute('role', 'columnheader'); - delheader.setAttribute('class', 'delete-header'); - delheader.innerHTML = ` - Delete Action`; - let tableHeaderRow = document.querySelector('.domain-requests__table thead tr'); - tableHeaderRow.appendChild(delheader); - } - } - - data.domain_requests.forEach(request => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const domainName = request.requested_domain ? request.requested_domain : `New domain request
(${utcDateString(request.created_at)})`; - const actionUrl = request.action_url; - const actionLabel = request.action_label; - const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; - - // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) - // Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed - let modalTrigger = ''; - - let markupCreatorRow = ''; - - if (portfolioValue) { - markupCreatorRow = ` - - ${request.creator ? request.creator : ''} - - ` - } - - // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages - if (request.is_deletable) { - let modalHeading = ''; - let modalDescription = ''; - - if (request.requested_domain) { - modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } else { - if (request.created_at) { - modalHeading = 'Are you sure you want to delete this domain request?'; - modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; - } else { - modalHeading = 'Are you sure you want to delete New domain request?'; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } - } - - modalTrigger = ` - - Delete ${domainName} - ` - - const modalSubmit = ` - - ` - - const modal = document.createElement('div'); - modal.setAttribute('class', 'usa-modal'); - modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); - modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); - modal.setAttribute('aria-describedby', 'Domain will be removed'); - modal.setAttribute('data-force-action', ''); - - modal.innerHTML = ` -
-
- -
- -
- -
- -
- ` - - domainRequestsSectionWrapper.appendChild(modal); - - // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly - if (portfolioValue) { - modalTrigger = ` - - Delete ${domainName} - - -
-
- -
- -
- ` - } - } - - - const row = document.createElement('tr'); - row.innerHTML = ` - - ${domainName} - - - ${submissionDate} - - ${markupCreatorRow} - - ${request.status} - - - - - ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} - - - ${needsDeleteColumn ? ''+modalTrigger+'' : ''} - `; - tbody.appendChild(row); - }); - - // initialize modals immediately after the DOM content is updated - initializeModals(); - - // Now the DOM and modals are ready, add listeners to the submit buttons - const modals = document.querySelectorAll('.usa-modal__content'); - - modals.forEach(modal => { - const submitButton = modal.querySelector('.usa-modal__submit'); - const closeButton = modal.querySelector('.usa-modal__close'); - submitButton.addEventListener('click', function() { - pk = submitButton.getAttribute('data-pk'); - // Close the modal to remove the USWDS UI local classes - closeButton.click(); - // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page - let pageToDisplay = data.page; - if (data.total == 1 && data.unfiltered_total > 1) { - pageToDisplay--; - } - deleteDomainRequest(pk, pageToDisplay); - }); - }); - - // Do not scroll on first page load - if (scroll) - ScrollToElement('class', 'domain-requests'); - scrollToTable = true; - - // update the pagination after the domain requests list is updated - updatePagination( - 'domain request', - '#domain-requests-pagination', - '#domain-requests-pagination .usa-pagination__counter', - '#domain-requests', - loadDomainRequests, - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - currentSearchTerm - ); - currentSortBy = sortBy; - currentOrder = order; - currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } - - // Add event listeners to table headers for sorting - tableHeaders.forEach(header => { - header.addEventListener('click', function() { - const sortBy = this.getAttribute('data-sortable'); - let order = 'asc'; - // sort order will be ascending, unless the currently sorted column is ascending, and the user - // is selecting the same column to sort in descending order - if (sortBy === currentSortBy) { - order = currentOrder === 'asc' ? 'desc' : 'asc'; - } - loadDomainRequests(1, sortBy, order); - }); - }); - - domainRequestsSearchSubmit.addEventListener('click', function(e) { - e.preventDefault(); - currentSearchTerm = domainRequestsSearchInput.value; - // If the search is blank, we match the resetSearch functionality - if (currentSearchTerm) { - showElement(resetSearchButton); - } else { - hideElement(resetSearchButton); - } - loadDomainRequests(1, 'id', 'asc'); - resetHeaders(); - }); - - // Reset UI and accessibility - function resetHeaders() { - tableHeaders.forEach(header => { - // unset sort UI in headers - unsetHeader(header); - }); - // Reset the announcement region - tableAnnouncementRegion.innerHTML = ''; - } - - function resetSearch() { - domainRequestsSearchInput.value = ''; - currentSearchTerm = ''; - hideElement(resetSearchButton); - loadDomainRequests(1, 'id', 'asc'); - resetHeaders(); - } - - if (resetSearchButton) { - resetSearchButton.addEventListener('click', function() { - resetSearch(); - }); - } - - function closeMoreActionMenu(accordionThatIsOpen) { - if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") { - accordionThatIsOpen.click(); - } - } - - document.addEventListener('focusin', function(event) { - closeOpenAccordions(event); - }); - - document.addEventListener('click', function(event) { - closeOpenAccordions(event); - }); - - function closeOpenAccordions(event) { - const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]'); - openAccordions.forEach((openAccordionButton) => { - // Find the corresponding accordion - const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); - if (accordion && !accordion.contains(event.target)) { - // Close the accordion if the click is outside - closeMoreActionMenu(openAccordionButton); - } - }); - } - - // Initial load - loadDomainRequests(1); - } -}); - /** * An IIFE that displays confirmation modal on the user profile page diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 96740a15c..03d9e38c6 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -245,7 +245,6 @@ TEMPLATES = [ "registrar.context_processors.is_production", "registrar.context_processors.org_user_status", "registrar.context_processors.add_path_to_context", - "registrar.context_processors.add_has_profile_feature_flag_to_context", "registrar.context_processors.portfolio_permissions", ], }, diff --git a/src/registrar/context_processors.py b/src/registrar/context_processors.py index 41046ed1c..3ceb25af1 100644 --- a/src/registrar/context_processors.py +++ b/src/registrar/context_processors.py @@ -1,5 +1,4 @@ from django.conf import settings -from waffle.decorators import flag_is_active def language_code(request): @@ -54,10 +53,6 @@ def add_path_to_context(request): return {"path": getattr(request, "path", None)} -def add_has_profile_feature_flag_to_context(request): - return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")} - - def portfolio_permissions(request): """Make portfolio permissions for the request user available in global context""" portfolio_context = { diff --git a/src/registrar/models/waffle_flag.py b/src/registrar/models/waffle_flag.py index d185c2a82..9dd11a3d8 100644 --- a/src/registrar/models/waffle_flag.py +++ b/src/registrar/models/waffle_flag.py @@ -9,7 +9,7 @@ class WaffleFlag(AbstractUserFlag): Custom implementation of django-waffles 'Flag' object. Read more here: https://waffle.readthedocs.io/en/stable/types/flag.html - Use this class when dealing with feature flags, such as profile_feature. + Use this class when dealing with feature flags. """ class Meta: diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 6207591ba..5a75577df 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -72,12 +72,6 @@ class CheckUserProfileMiddleware: """Runs pre-processing logic for each view. Checks for the finished_setup flag on the current user. If they haven't done so, then we redirect them to the finish setup page.""" - # Check that the user is "opted-in" to the profile feature flag - has_profile_feature_flag = flag_is_active(request, "profile_feature") - - # If they aren't, skip this check entirely - if not has_profile_feature_flag: - return None if request.user.is_authenticated: profile_page = self.profile_page diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index dd08004a3..26f57238a 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -83,13 +83,6 @@ {% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %} {% endif %} - - {# Conditionally display profile #} - {% if not has_profile_feature_flag %} - {% url 'domain-your-contact-information' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=is_editable %} - {% endif %} - {% url 'domain-security-email' pk=domain.id as url %} {% if security_email is not None and security_email not in hidden_security_emails%} {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %} diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index f4319f53d..fd94e0ef1 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -16,11 +16,9 @@

Time to complete the form

If you have all the information you need, completing your domain request might take around 15 minutes.

- {% if has_profile_feature_flag %}

How we’ll reach you

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review. If the contact information below is not correct, visit your profile to make updates.

{% include "includes/profile_information.html" with user=user%} - {% endif %} {% block form_buttons %} diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index 0c123948e..375e0229c 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -23,13 +23,21 @@ Reset + {% if portfolio %} + + {% else %} + {% endif %} + +
+

Status

+
+ Select to apply status filter +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + {% endif %}