mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-26 12:38:36 +02:00
Merge pull request #2739 from cisagov/dk/2593-domain-request-search-bar
#2593: Domain request search bar and filter - [DK]
This commit is contained in:
commit
8adb33b430
5 changed files with 975 additions and 735 deletions
|
@ -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"
|
||||||
|
@ -994,40 +1002,79 @@ function unloadModals() {
|
||||||
window.modal.off();
|
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.
|
* Generalized function to update pagination for a list.
|
||||||
* @param {string} itemName - The name displayed in the counter
|
* @param {string} itemName - The name displayed in the counter
|
||||||
* @param {string} paginationSelector - CSS selector for the pagination container.
|
* @param {string} paginationSelector - CSS selector for the pagination container.
|
||||||
* @param {string} counterSelector - CSS selector for the pagination counter.
|
* @param {string} counterSelector - CSS selector for the pagination counter.
|
||||||
* @param {string} linkAnchor - CSS selector for the header element to anchor the links to.
|
* @param {string} tableSelector - CSS selector for the header element to anchor the links to.
|
||||||
* @param {Function} loadPageFunction - Function to call when a page link is clicked.
|
|
||||||
* @param {number} currentPage - The current page number (starting with 1).
|
* @param {number} currentPage - The current page number (starting with 1).
|
||||||
* @param {number} numPages - The total number of pages.
|
* @param {number} numPages - The total number of pages.
|
||||||
* @param {boolean} hasPrevious - Whether there is a page before the current page.
|
* @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 {boolean} hasNext - Whether there is a page after the current page.
|
||||||
* @param {number} totalItems - The total number of items.
|
* @param {number} total - The total number of items.
|
||||||
* @param {string} searchTerm - The search term
|
|
||||||
*/
|
*/
|
||||||
function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
|
updatePagination(
|
||||||
const paginationContainer = document.querySelector(paginationSelector);
|
itemName,
|
||||||
const paginationCounter = document.querySelector(counterSelector);
|
paginationSelector,
|
||||||
|
counterSelector,
|
||||||
|
parentTableSelector,
|
||||||
|
currentPage,
|
||||||
|
numPages,
|
||||||
|
hasPrevious,
|
||||||
|
hasNext,
|
||||||
|
totalItems,
|
||||||
|
) {
|
||||||
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
||||||
paginationCounter.innerHTML = '';
|
const counterSelectorEl = document.querySelector(counterSelector);
|
||||||
|
const paginationSelectorEl = document.querySelector(paginationSelector);
|
||||||
|
counterSelectorEl.innerHTML = '';
|
||||||
paginationButtons.innerHTML = '';
|
paginationButtons.innerHTML = '';
|
||||||
|
|
||||||
// Buttons should only be displayed if there are more than one pages of results
|
// Buttons should only be displayed if there are more than one pages of results
|
||||||
paginationButtons.classList.toggle('display-none', numPages <= 1);
|
paginationButtons.classList.toggle('display-none', numPages <= 1);
|
||||||
|
|
||||||
// Counter should only be displayed if there is more than 1 item
|
// Counter should only be displayed if there is more than 1 item
|
||||||
paginationContainer.classList.toggle('display-none', totalItems < 1);
|
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) {
|
if (hasPrevious) {
|
||||||
const prevPageItem = document.createElement('li');
|
const prevPageItem = document.createElement('li');
|
||||||
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
prevPageItem.innerHTML = `
|
prevPageItem.innerHTML = `
|
||||||
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
<a href="${parentTableSelector}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
||||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||||
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
|
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1036,32 +1083,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
||||||
`;
|
`;
|
||||||
prevPageItem.querySelector('a').addEventListener('click', (event) => {
|
prevPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
loadPageFunction(currentPage - 1);
|
this.loadTable(currentPage - 1);
|
||||||
});
|
});
|
||||||
paginationButtons.appendChild(prevPageItem);
|
paginationButtons.appendChild(prevPageItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to create a page item
|
|
||||||
function createPageItem(page) {
|
|
||||||
const pageItem = document.createElement('li');
|
|
||||||
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
|
||||||
pageItem.innerHTML = `
|
|
||||||
<a href="${linkAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
|
||||||
`;
|
|
||||||
if (page === currentPage) {
|
|
||||||
pageItem.querySelector('a').classList.add('usa-current');
|
|
||||||
pageItem.querySelector('a').setAttribute('aria-current', 'page');
|
|
||||||
}
|
|
||||||
pageItem.querySelector('a').addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
loadPageFunction(page);
|
|
||||||
});
|
|
||||||
return pageItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add first page and ellipsis if necessary
|
// Add first page and ellipsis if necessary
|
||||||
if (currentPage > 2) {
|
if (currentPage > 2) {
|
||||||
paginationButtons.appendChild(createPageItem(1));
|
paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage));
|
||||||
if (currentPage > 3) {
|
if (currentPage > 3) {
|
||||||
const ellipsis = document.createElement('li');
|
const ellipsis = document.createElement('li');
|
||||||
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
||||||
|
@ -1073,7 +1102,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
||||||
|
|
||||||
// Add pages around the current page
|
// Add pages around the current page
|
||||||
for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) {
|
for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) {
|
||||||
paginationButtons.appendChild(createPageItem(i));
|
paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add last page and ellipsis if necessary
|
// Add last page and ellipsis if necessary
|
||||||
|
@ -1085,14 +1114,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
||||||
ellipsis.innerHTML = '<span>…</span>';
|
ellipsis.innerHTML = '<span>…</span>';
|
||||||
paginationButtons.appendChild(ellipsis);
|
paginationButtons.appendChild(ellipsis);
|
||||||
}
|
}
|
||||||
paginationButtons.appendChild(createPageItem(numPages));
|
paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNext) {
|
if (hasNext) {
|
||||||
const nextPageItem = document.createElement('li');
|
const nextPageItem = document.createElement('li');
|
||||||
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
nextPageItem.innerHTML = `
|
nextPageItem.innerHTML = `
|
||||||
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
<a href="${parentTableSelector}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
||||||
<span class="usa-pagination__link-text">Next</span>
|
<span class="usa-pagination__link-text">Next</span>
|
||||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||||
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
|
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
|
||||||
|
@ -1101,7 +1130,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
||||||
`;
|
`;
|
||||||
nextPageItem.querySelector('a').addEventListener('click', (event) => {
|
nextPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
loadPageFunction(currentPage + 1);
|
this.loadTable(currentPage + 1);
|
||||||
});
|
});
|
||||||
paginationButtons.appendChild(nextPageItem);
|
paginationButtons.appendChild(nextPageItem);
|
||||||
}
|
}
|
||||||
|
@ -1111,7 +1140,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, linkAnc
|
||||||
* A helper that toggles content/ no content/ no search results
|
* A helper that toggles content/ no content/ no search results
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
||||||
const { unfiltered_total, total } = data;
|
const { unfiltered_total, total } = data;
|
||||||
if (unfiltered_total) {
|
if (unfiltered_total) {
|
||||||
if (total) {
|
if (total) {
|
||||||
|
@ -1130,11 +1159,29 @@ const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to create a page item
|
||||||
|
createPageItem(page, parentTableSelector, currentPage) {
|
||||||
|
const pageItem = document.createElement('li');
|
||||||
|
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
||||||
|
pageItem.innerHTML = `
|
||||||
|
<a href="${parentTableSelector}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||||
|
`;
|
||||||
|
if (page === currentPage) {
|
||||||
|
pageItem.querySelector('a').classList.add('usa-current');
|
||||||
|
pageItem.querySelector('a').setAttribute('aria-current', 'page');
|
||||||
|
}
|
||||||
|
pageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.loadTable(page);
|
||||||
|
});
|
||||||
|
return pageItem;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper that resets sortable table headers
|
* A helper that resets sortable table headers
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const unsetHeader = (header) => {
|
unsetHeader = (header) => {
|
||||||
header.removeAttribute('aria-sort');
|
header.removeAttribute('aria-sort');
|
||||||
let headerName = header.innerText;
|
let headerName = header.innerText;
|
||||||
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
|
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
|
||||||
|
@ -1143,34 +1190,181 @@ const unsetHeader = (header) => {
|
||||||
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
|
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Abstract method (to be implemented in the child class)
|
||||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
loadTable(page, sortBy, order) {
|
||||||
* initializes the domains list and associated functionality on the home page of the app.
|
throw new Error('loadData() must be implemented in a subclass');
|
||||||
*
|
}
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const domainsWrapper = document.querySelector('.domains__table-wrapper');
|
|
||||||
|
|
||||||
if (domainsWrapper) {
|
// Add event listeners to table headers for sorting
|
||||||
let currentSortBy = 'id';
|
initializeTableHeaders() {
|
||||||
let currentOrder = 'asc';
|
this.tableHeaders.forEach(header => {
|
||||||
const noDomainsWrapper = document.querySelector('.domains__no-data');
|
header.addEventListener('click', () => {
|
||||||
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
|
const sortBy = header.getAttribute('data-sortable');
|
||||||
let scrollToTable = false;
|
let order = 'asc';
|
||||||
let currentStatus = [];
|
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||||
let currentSearchTerm = '';
|
// is selecting the same column to sort in descending order
|
||||||
const domainsSearchInput = document.getElementById('domains__search-field');
|
if (sortBy === this.currentSortBy) {
|
||||||
const domainsSearchSubmit = document.getElementById('domains__search-field-submit');
|
order = this.currentOrder === 'asc' ? 'desc' : 'asc';
|
||||||
const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
|
}
|
||||||
const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region');
|
// load the results with the updated sort
|
||||||
const resetSearchButton = document.querySelector('.domains__reset-search');
|
this.loadTable(1, sortBy, order);
|
||||||
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;
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||||
* based on the supplied attributes.
|
* based on the supplied attributes.
|
||||||
|
@ -1178,10 +1372,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
* @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
|
* @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
|
// fetch json of page of domais, given params
|
||||||
let baseUrl = document.getElementById("get_domains_json_url");
|
let baseUrl = document.getElementById("get_domains_json_url");
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
|
@ -1194,10 +1390,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch json of page of domains, given params
|
// 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)
|
if (portfolio)
|
||||||
url += `&portfolio=${portfolio}`
|
searchParams.append("portfolio", portfolio)
|
||||||
|
|
||||||
|
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.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
|
// 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
|
// 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');
|
||||||
|
@ -1225,7 +1430,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
let markupForSuborganizationRow = '';
|
let markupForSuborganizationRow = '';
|
||||||
|
|
||||||
if (portfolioValue) {
|
if (this.portfolioValue) {
|
||||||
markupForSuborganizationRow = `
|
markupForSuborganizationRow = `
|
||||||
<td>
|
<td>
|
||||||
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
||||||
|
@ -1271,271 +1476,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Do not scroll on first page load
|
// Do not scroll on first page load
|
||||||
if (scroll)
|
if (scroll)
|
||||||
ScrollToElement('class', 'domains');
|
ScrollToElement('class', 'domains');
|
||||||
scrollToTable = true;
|
this.scrollToTable = true;
|
||||||
|
|
||||||
// update pagination
|
// update pagination
|
||||||
updatePagination(
|
this.updatePagination(
|
||||||
'domain',
|
'domain',
|
||||||
'#domains-pagination',
|
'#domains-pagination',
|
||||||
'#domains-pagination .usa-pagination__counter',
|
'#domains-pagination .usa-pagination__counter',
|
||||||
'#domains',
|
'#domains',
|
||||||
loadDomains,
|
|
||||||
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 domains:', error));
|
.catch(error => console.error('Error fetching domains:', 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';
|
|
||||||
}
|
|
||||||
// load the results with the updated sort
|
|
||||||
loadDomains(1, sortBy, order);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
loadDomains(1, 'id', 'asc');
|
|
||||||
resetHeaders();
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
class DomainRequestsTable extends LoadTableBase {
|
||||||
if (this.checked) {
|
|
||||||
currentStatus.push(checkboxValue);
|
constructor() {
|
||||||
} else {
|
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');
|
||||||
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 loadDomains with updated status
|
|
||||||
loadDomains(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() {
|
|
||||||
domainsSearchInput.value = '';
|
|
||||||
currentSearchTerm = '';
|
|
||||||
hideElement(resetSearchButton);
|
|
||||||
loadDomains(1, 'id', 'asc');
|
|
||||||
resetHeaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resetSearchButton) {
|
|
||||||
resetSearchButton.addEventListener('click', function() {
|
|
||||||
resetSearch();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFilters() {
|
|
||||||
currentStatus = [];
|
|
||||||
statusCheckboxes.forEach(checkbox => {
|
|
||||||
checkbox.checked = false;
|
|
||||||
});
|
|
||||||
hideElement(resetFiltersButton);
|
|
||||||
|
|
||||||
// Disable the auto scroll
|
|
||||||
scrollToTable = false;
|
|
||||||
|
|
||||||
loadDomains(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
|
|
||||||
statusIndicator.hideElement();
|
|
||||||
if (currentStatus.length)
|
|
||||||
statusIndicator.innerHTML = '(' + currentStatus.length + ')';
|
|
||||||
statusIndicator.showElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
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"]');
|
|
||||||
|
|
||||||
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
|
||||||
closeFilters();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
loadDomains(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 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.
|
* 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, 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;
|
||||||
|
@ -1546,11 +1526,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch json of page of requests, given params
|
// add searchParams
|
||||||
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`
|
let searchParams = new URLSearchParams(
|
||||||
|
{
|
||||||
|
"page": page,
|
||||||
|
"sort_by": sortBy,
|
||||||
|
"order": order,
|
||||||
|
"status": status,
|
||||||
|
"search_term": searchTerm
|
||||||
|
}
|
||||||
|
);
|
||||||
if (portfolio)
|
if (portfolio)
|
||||||
url += `&portfolio=${portfolio}`
|
searchParams.append("portfolio", portfolio)
|
||||||
|
|
||||||
|
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
@ -1560,7 +1549,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');
|
||||||
|
@ -1613,7 +1602,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>
|
||||||
|
@ -1708,10 +1697,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"
|
||||||
|
@ -1794,8 +1783,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
|
||||||
|
@ -1803,90 +1792,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';
|
|
||||||
}
|
|
||||||
loadDomainRequests(1, sortBy, order);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
domainRequestsSearchSubmit.addEventListener('click', function(e) {
|
// Get csrf token
|
||||||
e.preventDefault();
|
const csrfToken = getCsrfToken();
|
||||||
currentSearchTerm = domainRequestsSearchInput.value;
|
// Create FormData object and append the CSRF token
|
||||||
// If the search is blank, we match the resetSearch functionality
|
const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`;
|
||||||
if (currentSearchTerm) {
|
|
||||||
showElement(resetSearchButton);
|
|
||||||
} else {
|
|
||||||
hideElement(resetSearchButton);
|
|
||||||
}
|
|
||||||
loadDomainRequests(1, 'id', 'asc');
|
|
||||||
resetHeaders();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset UI and accessibility
|
fetch(`/domain-request/${domainRequestPk}/delete`, {
|
||||||
function resetHeaders() {
|
method: 'POST',
|
||||||
tableHeaders.forEach(header => {
|
headers: {
|
||||||
// unset sort UI in headers
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
unsetHeader(header);
|
'X-CSRFToken': csrfToken,
|
||||||
});
|
},
|
||||||
// Reset the announcement region
|
body: formData
|
||||||
tableAnnouncementRegion.innerHTML = '';
|
})
|
||||||
|
.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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearch() {
|
|
||||||
domainRequestsSearchInput.value = '';
|
|
||||||
currentSearchTerm = '';
|
|
||||||
hideElement(resetSearchButton);
|
|
||||||
loadDomainRequests(1, 'id', 'asc');
|
|
||||||
resetHeaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resetSearchButton) {
|
/**
|
||||||
resetSearchButton.addEventListener('click', function() {
|
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||||
resetSearch();
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function closeMoreActionMenu(accordionThatIsOpen) {
|
/**
|
||||||
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
|
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||||
accordionThatIsOpen.click();
|
* 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1898,6 +1892,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) => {
|
||||||
|
@ -1909,12 +1909,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
@ -23,13 +23,21 @@
|
||||||
</svg>
|
</svg>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
|
{% if portfolio %}
|
||||||
|
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name or creator</label>
|
||||||
|
{% else %}
|
||||||
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
||||||
|
{% endif %}
|
||||||
<input
|
<input
|
||||||
class="usa-input"
|
class="usa-input"
|
||||||
id="domain-requests__search-field"
|
id="domain-requests__search-field"
|
||||||
type="search"
|
type="search"
|
||||||
name="search"
|
name="search"
|
||||||
|
{% if portfolio %}
|
||||||
|
placeholder="Search by domain name or creator"
|
||||||
|
{% else %}
|
||||||
placeholder="Search by domain name"
|
placeholder="Search by domain name"
|
||||||
|
{% endif %}
|
||||||
/>
|
/>
|
||||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
||||||
<img
|
<img
|
||||||
|
@ -42,6 +50,125 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if portfolio %}
|
||||||
|
<div class="display-flex flex-align-center">
|
||||||
|
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
||||||
|
<div class="usa-accordion usa-accordion--select margin-right-2">
|
||||||
|
<div class="usa-accordion__heading">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--small padding--8-8-9 usa-button--outline usa-button--filter usa-accordion__button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="filter-status"
|
||||||
|
>
|
||||||
|
<span class="filter-indicator text-bold display-none"></span> Status
|
||||||
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="filter-status" class="usa-accordion__content usa-prose shadow-1">
|
||||||
|
<h2>Status</h2>
|
||||||
|
<fieldset class="usa-fieldset margin-top-0">
|
||||||
|
<legend class="usa-legend">Select to apply <span class="sr-only">status</span> filter</legend>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-started"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="started"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-started"
|
||||||
|
>Started</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-submitted"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="submitted"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-submitted"
|
||||||
|
>Submitted</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-in-review"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="in review"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-in-review"
|
||||||
|
>In review</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-action-needed"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="action needed"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-action-needed"
|
||||||
|
>Action needed</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-rejected"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="rejected"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-rejected"
|
||||||
|
>Rejected</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-withdrawn"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="withdrawn"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-withdrawn"
|
||||||
|
>Withdrawn</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="usa-checkbox">
|
||||||
|
<input
|
||||||
|
class="usa-checkbox__input"
|
||||||
|
id="filter-status-ineligible"
|
||||||
|
type="checkbox"
|
||||||
|
name="filter-status"
|
||||||
|
value="ineligible"
|
||||||
|
/>
|
||||||
|
<label class="usa-checkbox__label" for="filter-status-ineligible"
|
||||||
|
>Ineligible</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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
|
||||||
|
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
|
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="filter-status"
|
aria-controls="filter-status"
|
||||||
>
|
>
|
||||||
<span class="domain__filter-indicator text-bold display-none"></span> Status
|
<span class="filter-indicator text-bold display-none"></span> Status
|
||||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -458,3 +458,81 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
# Ensure no approved requests are included
|
# Ensure no approved requests are included
|
||||||
for domain_request in data["domain_requests"]:
|
for domain_request in data["domain_requests"]:
|
||||||
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
self.assertNotEqual(domain_request["status"], DomainRequest.DomainRequestStatus.APPROVED)
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
"""Tests our search functionality. We expect that search filters on creator only when we are in a portfolio"""
|
||||||
|
# Test search for domain name
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "lamb"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 1)
|
||||||
|
|
||||||
|
requested_domain = data["domain_requests"][0]["requested_domain"]
|
||||||
|
self.assertEqual(requested_domain, "lamb-chops.gov")
|
||||||
|
|
||||||
|
# Test search for 'New domain request'
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "new domain"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(any(req["requested_domain"] is None for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test search with portfolio (including creator search)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
with override_flag("organization_feature", active=True), override_flag("organization_requests", active=True):
|
||||||
|
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
)
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_domain_requests_json"), {"search_term": "info", "portfolio": self.portfolio.id}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(any(req["creator"].startswith("info") for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test search without portfolio (should not search on creator)
|
||||||
|
with override_flag("organization_feature", active=False), override_flag("organization_requests", active=False):
|
||||||
|
user_perm.delete()
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"search_term": "info"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 0)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_status_filter(self):
|
||||||
|
"""Test that status filtering works properly"""
|
||||||
|
# Test a single status
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"status": "started"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||||
|
|
||||||
|
# Test an invalid status
|
||||||
|
response = self.app.get(reverse("get_domain_requests_json"), {"status": "approved"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertEqual(len(data["domain_requests"]), 0)
|
||||||
|
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_combined_filtering_and_sorting(self):
|
||||||
|
"""Test that combining filters and sorting works properly"""
|
||||||
|
user_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.app.get(
|
||||||
|
reverse("get_domain_requests_json"),
|
||||||
|
{"search_term": "beef", "status": "started", "portfolio": self.portfolio.id},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
self.assertTrue(all("beef" in req["requested_domain"] for req in data["domain_requests"]))
|
||||||
|
self.assertTrue(all(req["status"] == "Started" for req in data["domain_requests"]))
|
||||||
|
created_at_dates = [req["created_at"] for req in data["domain_requests"]]
|
||||||
|
self.assertEqual(created_at_dates, sorted(created_at_dates, reverse=True))
|
||||||
|
user_perm.delete()
|
||||||
|
|
|
@ -20,6 +20,7 @@ def get_domain_requests_json(request):
|
||||||
unfiltered_total = objects.count()
|
unfiltered_total = objects.count()
|
||||||
|
|
||||||
objects = apply_search(objects, request)
|
objects = apply_search(objects, request)
|
||||||
|
objects = apply_status_filter(objects, request)
|
||||||
objects = apply_sorting(objects, request)
|
objects = apply_sorting(objects, request)
|
||||||
|
|
||||||
paginator = Paginator(objects, 10)
|
paginator = Paginator(objects, 10)
|
||||||
|
@ -63,6 +64,7 @@ def get_domain_request_ids_from_request(request):
|
||||||
|
|
||||||
def apply_search(queryset, request):
|
def apply_search(queryset, request):
|
||||||
search_term = request.GET.get("search_term")
|
search_term = request.GET.get("search_term")
|
||||||
|
is_portfolio = request.GET.get("portfolio")
|
||||||
|
|
||||||
if search_term:
|
if search_term:
|
||||||
search_term_lower = search_term.lower()
|
search_term_lower = search_term.lower()
|
||||||
|
@ -75,11 +77,34 @@ def apply_search(queryset, request):
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
||||||
)
|
)
|
||||||
|
elif is_portfolio:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(requested_domain__name__icontains=search_term)
|
||||||
|
| Q(creator__first_name__icontains=search_term)
|
||||||
|
| Q(creator__last_name__icontains=search_term)
|
||||||
|
| Q(creator__email__icontains=search_term)
|
||||||
|
)
|
||||||
|
# For non org users
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_status_filter(queryset, request):
|
||||||
|
status_param = request.GET.get("status")
|
||||||
|
if status_param:
|
||||||
|
status_list = status_param.split(",")
|
||||||
|
statuses = [status for status in status_list if status in DomainRequest.DomainRequestStatus.values]
|
||||||
|
# Construct Q objects for statuses that can be queried through ORM
|
||||||
|
status_query = Q()
|
||||||
|
if statuses:
|
||||||
|
status_query |= Q(status__in=statuses)
|
||||||
|
# Apply the combined query
|
||||||
|
queryset = queryset.filter(status_query)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def apply_sorting(queryset, request):
|
def apply_sorting(queryset, request):
|
||||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue