refactor domain requests

This commit is contained in:
zandercymatics 2024-09-12 14:09:33 -06:00
parent bfb942a43d
commit 0126d6e81f
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
2 changed files with 424 additions and 568 deletions

View file

@ -33,6 +33,14 @@ const showElement = (element) => {
element.classList.remove('display-none'); 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 * Helper function that scrolls to an element
* @param {string} attributeName - The string "class" or "id" * @param {string} attributeName - The string "class" or "id"
@ -995,28 +1003,28 @@ function unloadModals() {
} }
class LoadTableBase { class LoadTableBase {
constructor(tableSelector, tableWrapper, searchFieldId, searchSubmitId, resetSearchBtn, resetFiltersBtn, noDataDisplay, noSearchresultsDisplay) { constructor(tableSelector, tableWrapperSelector, searchFieldId, searchSubmitId, resetSearchBtn, resetFiltersBtn, noDataDisplay, noSearchresultsDisplay) {
this.domainsWrapper = document.querySelector(tableWrapper); this.tableWrapper = document.querySelector(tableWrapperSelector);
this.tableHeaders = document.querySelectorAll(`${tableSelector} th[data-sortable]`); this.tableHeaders = document.querySelectorAll(`${tableSelector} th[data-sortable]`);
this.currentSortBy = 'id'; this.currentSortBy = 'id';
this.currentOrder = 'asc'; this.currentOrder = 'asc';
this.currentStatus = []; this.currentStatus = [];
this.currentSearchTerm = ''; this.currentSearchTerm = '';
this.scrollToTable = false; this.scrollToTable = false;
this.domainsSearchInput = document.querySelector(searchFieldId); this.searchInput = document.querySelector(searchFieldId);
this.domainsSearchSubmit = document.querySelector(searchSubmitId); this.searchSubmit = document.querySelector(searchSubmitId);
this.tableAnnouncementRegion = document.querySelector(`${tableWrapper} .usa-table__announcement-region`); this.tableAnnouncementRegion = document.querySelector(`${tableWrapperSelector} .usa-table__announcement-region`);
this.resetSearchButton = document.querySelector(resetSearchBtn); this.resetSearchButton = document.querySelector(resetSearchBtn);
this.resetFiltersButton = document.querySelector(resetFiltersBtn); this.resetFiltersButton = document.querySelector(resetFiltersBtn);
// NOTE: these 3 can't be used if filters are active on a page with more than 1 table // 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.statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
this.statusIndicator = document.querySelector('.filter-indicator'); this.statusIndicator = document.querySelector('.filter-indicator');
this.statusToggle = document.querySelector('.usa-button--filter'); this.statusToggle = document.querySelector('.usa-button--filter');
this.noDomainsWrapper = document.querySelector(noDataDisplay); this.noTableWrapper = document.querySelector(noDataDisplay);
this.noSearchResultsWrapper = document.querySelector(noSearchresultsDisplay); this.noSearchResultsWrapper = document.querySelector(noSearchresultsDisplay);
this.portfolioElement = document.getElementById('portfolio-js-value'); this.portfolioElement = document.getElementById('portfolio-js-value');
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
this.initializetableHeaders(); this.initializeTableHeaders();
this.initializeSearchHandler(); this.initializeSearchHandler();
this.initializeStatusToggleHandler(); this.initializeStatusToggleHandler();
this.initializeFilterCheckboxes(); this.initializeFilterCheckboxes();
@ -1188,7 +1196,7 @@ class LoadTableBase {
} }
// Add event listeners to table headers for sorting // Add event listeners to table headers for sorting
initializetableHeaders() { initializeTableHeaders() {
this.tableHeaders.forEach(header => { this.tableHeaders.forEach(header => {
header.addEventListener('click', () => { header.addEventListener('click', () => {
const sortBy = header.getAttribute('data-sortable'); const sortBy = header.getAttribute('data-sortable');
@ -1205,9 +1213,9 @@ class LoadTableBase {
} }
initializeSearchHandler() { initializeSearchHandler() {
this.domainsSearchSubmit.addEventListener('click', (e) => { this.searchSubmit.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
this.currentSearchTerm = this.domainsSearchInput.value; this.currentSearchTerm = this.searchInput.value;
// If the search is blank, we match the resetSearch functionality // If the search is blank, we match the resetSearch functionality
if (this.currentSearchTerm) { if (this.currentSearchTerm) {
showElement(this.resetSearchButton); showElement(this.resetSearchButton);
@ -1272,7 +1280,7 @@ class LoadTableBase {
} }
resetSearch() { resetSearch() {
this.domainsSearchInput.value = ''; this.searchInput.value = '';
this.currentSearchTerm = ''; this.currentSearchTerm = '';
hideElement(this.resetSearchButton); hideElement(this.resetSearchButton);
this.loadTable(1, 'id', 'asc'); this.loadTable(1, 'id', 'asc');
@ -1404,7 +1412,7 @@ class DomainsTable extends LoadTableBase {
} }
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results // handle the display of proper messaging in the event that no domains exist in the list or search returns no results
this.updateDisplay(data, this.domainsWrapper, this.noDomainsWrapper, this.noSearchResultsWrapper, this.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 // identify the DOM element where the domain list will be inserted into the DOM
const domainList = document.querySelector('.domains__table tbody'); const domainList = document.querySelector('.domains__table tbody');
@ -1491,114 +1499,25 @@ class DomainsTable extends LoadTableBase {
} }
/** class DomainRequestsTable extends LoadTableBase {
* 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 domainsTable = new DomainsTable();
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');
this.domainRequestPk = 1
if (domainsTable.domainsWrapper) {
// Initial load
domainsTable.loadTable(1);
} }
});
const utcDateString = (dateString) => {
const date = new Date(dateString);
const utcYear = date.getUTCFullYear();
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
const utcDay = date.getUTCDate().toString().padStart(2, '0');
let utcHours = date.getUTCHours();
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
const ampm = utcHours >= 12 ? 'PM' : 'AM';
utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12'
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`;
};
/** /**
* An IIFE that listens for DOM Content to be loaded, then executes. This function * Loads rows in the domains list, as well as updates pagination around the domains list
* 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 currentStatus = [];
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 resetFiltersButton = document.querySelector('.domains__reset-filters');
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
const statusIndicator = document.querySelector('.filter-indicator');
const statusToggle = document.querySelector('.usa-button--filter');
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. * based on the supplied attributes.
* @param {*} page - the page number of the results (starts with 1) * @param {*} page - the page number of the results (starts with 1)
* @param {*} sortBy - the sort column option * @param {*} sortBy - the sort column option
* @param {*} order - the sort order {asc, desc} * @param {*} order - the sort order {asc, desc}
* @param {*} scroll - control for the scrollToElement functionality * @param {*} scroll - control for the scrollToElement functionality
* @param {*} status - control for the status filter
* @param {*} searchTerm - the search term * @param {*} searchTerm - the search term
* @param {*} portfolio - the portfolio id
*/ */
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, status = currentStatus, 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 domain requests, given params
let baseUrl = document.getElementById("get_domain_requests_json_url"); let baseUrl = document.getElementById("get_domain_requests_json_url");
if (!baseUrl) { if (!baseUrl) {
return; return;
@ -1632,7 +1551,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results // 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); 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 // identify the DOM element where the domain request list will be inserted into the DOM
const tbody = document.querySelector('.domain-requests__table tbody'); const tbody = document.querySelector('.domain-requests__table tbody');
@ -1685,7 +1604,7 @@ document.addEventListener('DOMContentLoaded', function() {
let markupCreatorRow = ''; let markupCreatorRow = '';
if (portfolioValue) { if (this.portfolioValue) {
markupCreatorRow = ` markupCreatorRow = `
<td> <td>
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span> <span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
@ -1780,10 +1699,10 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
` `
domainRequestsSectionWrapper.appendChild(modal); this.tableWrapper.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 // 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) { if (this.portfolioValue) {
modalTrigger = ` modalTrigger = `
<a <a
role="button" role="button"
@ -1866,8 +1785,8 @@ document.addEventListener('DOMContentLoaded', function() {
modals.forEach(modal => { modals.forEach(modal => {
const submitButton = modal.querySelector('.usa-modal__submit'); const submitButton = modal.querySelector('.usa-modal__submit');
const closeButton = modal.querySelector('.usa-modal__close'); const closeButton = modal.querySelector('.usa-modal__close');
submitButton.addEventListener('click', function() { submitButton.addEventListener('click', () => {
pk = submitButton.getAttribute('data-pk'); let pk = submitButton.getAttribute('data-pk');
// Close the modal to remove the USWDS UI local classes // Close the modal to remove the USWDS UI local classes
closeButton.click(); 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 // 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
@ -1875,165 +1794,95 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.total == 1 && data.unfiltered_total > 1) { if (data.total == 1 && data.unfiltered_total > 1) {
pageToDisplay--; pageToDisplay--;
} }
deleteDomainRequest(pk, pageToDisplay); this.deleteDomainRequest(pk, pageToDisplay);
}); });
}); });
// Do not scroll on first page load // Do not scroll on first page load
if (scroll) if (scroll)
ScrollToElement('class', 'domain-requests'); ScrollToElement('class', 'domain-requests');
scrollToTable = true; this.scrollToTable = true;
// update the pagination after the domain requests list is updated // update the pagination after the domain requests list is updated
updatePagination( this.updatePagination(
'domain request', 'domain request',
'#domain-requests-pagination', '#domain-requests-pagination',
'#domain-requests-pagination .usa-pagination__counter', '#domain-requests-pagination .usa-pagination__counter',
'#domain-requests', '#domain-requests',
loadDomainRequests,
data.page, data.page,
data.num_pages, data.num_pages,
data.has_previous, data.has_previous,
data.has_next, data.has_next,
data.total, data.total,
currentSearchTerm
); );
currentSortBy = sortBy; this.currentSortBy = sortBy;
currentOrder = order; this.currentOrder = order;
currentSearchTerm = searchTerm; this.currentSearchTerm = searchTerm;
}) })
.catch(error => console.error('Error fetching domain requests:', error)); .catch(error => console.error('Error fetching domain requests:', error));
} }
// Add event listeners to table headers for sorting /**
tableHeaders.forEach(header => { * 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.
header.addEventListener('click', function() { * @param {*} domainRequestPk - the identifier for the request that we're deleting
const sortBy = this.getAttribute('data-sortable'); * @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
let order = 'asc'; */
// sort order will be ascending, unless the currently sorted column is ascending, and the user deleteDomainRequest(domainRequestPk, pageToDisplay) {
// is selecting the same column to sort in descending order // Use to debug uswds modal issues
if (sortBy === currentSortBy) { //console.log('deleteDomainRequest')
order = currentOrder === 'asc' ? 'desc' : 'asc';
// 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);
}
} }
loadDomainRequests(1, sortBy, order);
});
}); });
domainRequestsSearchSubmit.addEventListener('click', function(e) { /**
e.preventDefault(); * An IIFE that listens for DOM Content to be loaded, then executes. This function
currentSearchTerm = domainRequestsSearchInput.value; * initializes the domain requests list and associated functionality on the home page of the app.
// If the search is blank, we match the resetSearch functionality *
if (currentSearchTerm) { */
showElement(resetSearchButton); document.addEventListener('DOMContentLoaded', function() {
} else { const domainRequestsSectionWrapper = document.querySelector('.domain-requests');
hideElement(resetSearchButton); if (domainRequestsSectionWrapper) {
} const domainRequestsTable = new DomainRequestsTable();
loadDomainRequests(1, 'id', 'asc'); if (domainRequestsTable.tableWrapper) {
resetHeaders(); domainRequestsTable.loadTable(1);
});
if (statusToggle) {
statusToggle.addEventListener('click', function() {
toggleCaret(statusToggle);
});
}
// 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);
} else {
const index = currentStatus.indexOf(checkboxValue);
if (index > -1) {
currentStatus.splice(index, 1);
}
}
// Manage visibility of reset filters button
if (currentStatus.length == 0) {
hideElement(resetFiltersButton);
} else {
showElement(resetFiltersButton);
}
// Disable the auto scroll
scrollToTable = false;
// Call loadTable with updated status
loadDomainRequests(1, 'id', 'asc');
resetHeaders();
updateStatusIndicator();
});
});
// 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();
}
}
function resetFilters() {
currentStatus = [];
statusCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
hideElement(resetFiltersButton);
// Disable the auto scroll
scrollToTable = false;
loadDomainRequests(1, 'id', 'asc');
resetHeaders();
updateStatusIndicator();
// No need to toggle close the filters. The focus shift will trigger that for us.
}
if (resetFiltersButton) {
resetFiltersButton.addEventListener('click', function() {
resetFilters();
});
}
function updateStatusIndicator() {
statusIndicator.innerHTML = '';
// Even if the element is empty, it'll mess up the flex layout unless we set display none
hideElement(statusIndicator);
if (currentStatus.length)
statusIndicator.innerHTML = '(' + currentStatus.length + ')';
showElement(statusIndicator);
}
function closeFilters() {
if (statusToggle.getAttribute("aria-expanded") === "true") {
statusToggle.click();
} }
} }
@ -2045,6 +1894,12 @@ document.addEventListener('DOMContentLoaded', function() {
closeOpenAccordions(event); closeOpenAccordions(event);
}); });
function closeMoreActionMenu(accordionThatIsOpen) {
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
accordionThatIsOpen.click();
}
}
function closeOpenAccordions(event) { function closeOpenAccordions(event) {
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]'); const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
openAccordions.forEach((openAccordionButton) => { openAccordions.forEach((openAccordionButton) => {
@ -2055,22 +1910,23 @@ document.addEventListener('DOMContentLoaded', function() {
closeMoreActionMenu(openAccordionButton); closeMoreActionMenu(openAccordionButton);
} }
}); });
// Close the filter accordion
const openFilterAccordion = document.querySelector('.usa-button--filter[aria-expanded="true"]');
const moreFilterAccordion = openFilterAccordion ? openFilterAccordion.closest('.usa-accordion--select') : undefined;
if (openFilterAccordion) {
if (!moreFilterAccordion.contains(event.target)) {
closeFilters();
}
}
}
// Initial load
loadDomainRequests(1);
} }
}); });
const utcDateString = (dateString) => {
const date = new Date(dateString);
const utcYear = date.getUTCFullYear();
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
const utcDay = date.getUTCDate().toString().padStart(2, '0');
let utcHours = date.getUTCHours();
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
const ampm = utcHours >= 12 ? 'PM' : 'AM';
utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12'
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`;
};
/** /**
* An IIFE that displays confirmation modal on the user profile page * An IIFE that displays confirmation modal on the user profile page

View file

@ -160,7 +160,7 @@
</div> </div>
<button <button
type="button" type="button"
class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domains__reset-filters display-none" class="usa-button usa-button--small padding--8-12-9-12 usa-button--outline usa-button--filter domain-requests__reset-filters display-none"
> >
Clear filters Clear filters
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24"> <svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">