mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 10:07:04 +02:00
merge main
This commit is contained in:
commit
be43bbc8b7
18 changed files with 438 additions and 84 deletions
|
@ -637,7 +637,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
None,
|
||||
{"fields": ("username", "password", "status", "verification_type")},
|
||||
),
|
||||
("Personal info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||
("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||
(
|
||||
"Permissions",
|
||||
{
|
||||
|
@ -668,7 +668,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
)
|
||||
},
|
||||
),
|
||||
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||
("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||
(
|
||||
"Permissions",
|
||||
{
|
||||
|
@ -692,7 +692,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
# NOT all fields are readonly for admin, otherwise we would have
|
||||
# set this at the permissions level. The exception is 'status'
|
||||
analyst_readonly_fields = [
|
||||
"Personal Info",
|
||||
"User profile",
|
||||
"first_name",
|
||||
"middle_name",
|
||||
"last_name",
|
||||
|
|
|
@ -46,7 +46,7 @@ function ScrollToElement(attributeName, attributeValue) {
|
|||
} else if (attributeName === 'id') {
|
||||
targetEl = document.getElementById(attributeValue);
|
||||
} else {
|
||||
console.log('Error: unknown attribute name provided.');
|
||||
console.error('Error: unknown attribute name provided.');
|
||||
return; // Exit the function if an invalid attributeName is provided
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,50 @@ function makeVisible(el) {
|
|||
el.style.visibility = "visible";
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles expand_more / expand_more svgs in buttons or anchors
|
||||
* @param {Element} element - DOM element
|
||||
*/
|
||||
function toggleCaret(element) {
|
||||
// Get a reference to the use element inside the button
|
||||
const useElement = element.querySelector('use');
|
||||
// Check if the span element text is 'Hide'
|
||||
if (useElement.getAttribute('xlink:href') === '/public/img/sprite.svg#expand_more') {
|
||||
// Update the xlink:href attribute to expand_more
|
||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
||||
} else {
|
||||
// Update the xlink:href attribute to expand_less
|
||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that scrolls to an element
|
||||
* @param {string} attributeName - The string "class" or "id"
|
||||
* @param {string} attributeValue - The class or id name
|
||||
*/
|
||||
function ScrollToElement(attributeName, attributeValue) {
|
||||
let targetEl = null;
|
||||
|
||||
if (attributeName === 'class') {
|
||||
targetEl = document.getElementsByClassName(attributeValue)[0];
|
||||
} else if (attributeName === 'id') {
|
||||
targetEl = document.getElementById(attributeValue);
|
||||
} else {
|
||||
console.error('Error: unknown attribute name provided.');
|
||||
return; // Exit the function if an invalid attributeName is provided
|
||||
}
|
||||
|
||||
if (targetEl) {
|
||||
const rect = targetEl.getBoundingClientRect();
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
window.scrollTo({
|
||||
top: rect.top + scrollTop,
|
||||
behavior: 'smooth' // Optional: for smooth scrolling
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates and returns a live region element. */
|
||||
function createLiveRegion(id) {
|
||||
const liveRegion = document.createElement("div");
|
||||
|
@ -927,7 +971,7 @@ function unloadModals() {
|
|||
* @param {string} itemName - The name displayed in the counter
|
||||
* @param {string} paginationSelector - CSS selector for the pagination container.
|
||||
* @param {string} counterSelector - CSS selector for the pagination counter.
|
||||
* @param {string} headerAnchor - CSS selector for the header element to anchor the links to.
|
||||
* @param {string} linkAnchor - CSS selector for the header element to anchor the links to.
|
||||
* @param {Function} loadPageFunction - Function to call when a page link is clicked.
|
||||
* @param {number} currentPage - The current page number (starting with 1).
|
||||
* @param {number} numPages - The total number of pages.
|
||||
|
@ -936,7 +980,7 @@ function unloadModals() {
|
|||
* @param {number} totalItems - The total number of items.
|
||||
* @param {string} searchTerm - The search term
|
||||
*/
|
||||
function updatePagination(itemName, paginationSelector, counterSelector, headerAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
|
||||
function updatePagination(itemName, paginationSelector, counterSelector, linkAnchor, loadPageFunction, currentPage, numPages, hasPrevious, hasNext, totalItems, searchTerm) {
|
||||
const paginationContainer = document.querySelector(paginationSelector);
|
||||
const paginationCounter = document.querySelector(counterSelector);
|
||||
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
||||
|
@ -955,7 +999,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
|||
const prevPageItem = document.createElement('li');
|
||||
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||
prevPageItem.innerHTML = `
|
||||
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
||||
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__previous-page" aria-label="Previous page">
|
||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||
<use xlink:href="/public/img/sprite.svg#navigate_before"></use>
|
||||
</svg>
|
||||
|
@ -974,7 +1018,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
|||
const pageItem = document.createElement('li');
|
||||
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
||||
pageItem.innerHTML = `
|
||||
<a href="${headerAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||
<a href="${linkAnchor}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||
`;
|
||||
if (page === currentPage) {
|
||||
pageItem.querySelector('a').classList.add('usa-current');
|
||||
|
@ -1020,7 +1064,7 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
|||
const nextPageItem = document.createElement('li');
|
||||
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||
nextPageItem.innerHTML = `
|
||||
<a href="${headerAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
||||
<a href="${linkAnchor}" class="usa-pagination__link usa-pagination__next-page" aria-label="Next page">
|
||||
<span class="usa-pagination__link-text">Next</span>
|
||||
<svg class="usa-icon" aria-hidden="true" role="img">
|
||||
<use xlink:href="/public/img/sprite.svg#navigate_next"></use>
|
||||
|
@ -1039,20 +1083,14 @@ function updatePagination(itemName, paginationSelector, counterSelector, headerA
|
|||
* A helper that toggles content/ no content/ no search results
|
||||
*
|
||||
*/
|
||||
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm) => {
|
||||
const updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
||||
const { unfiltered_total, total } = data;
|
||||
|
||||
if (searchTermHolder)
|
||||
searchTermHolder.innerHTML = '';
|
||||
|
||||
if (unfiltered_total) {
|
||||
if (total) {
|
||||
showElement(dataWrapper);
|
||||
hideElement(noSearchResultsWrapper);
|
||||
hideElement(noDataWrapper);
|
||||
} else {
|
||||
if (searchTermHolder)
|
||||
searchTermHolder.innerHTML = currentSearchTerm;
|
||||
hideElement(dataWrapper);
|
||||
showElement(noSearchResultsWrapper);
|
||||
hideElement(noDataWrapper);
|
||||
|
@ -1090,14 +1128,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
let currentOrder = 'asc';
|
||||
const noDomainsWrapper = document.querySelector('.domains__no-data');
|
||||
const noSearchResultsWrapper = document.querySelector('.domains__no-search-results');
|
||||
let hasLoaded = false;
|
||||
let currentSearchTerm = ''
|
||||
let scrollToTable = false;
|
||||
let currentStatus = [];
|
||||
let currentSearchTerm = '';
|
||||
const domainsSearchInput = document.getElementById('domains__search-field');
|
||||
const domainsSearchSubmit = document.getElementById('domains__search-field-submit');
|
||||
const tableHeaders = document.querySelectorAll('.domains__table th[data-sortable]');
|
||||
const tableAnnouncementRegion = document.querySelector('.domains__table-wrapper .usa-table__announcement-region');
|
||||
const searchTermHolder = document.querySelector('.domains__search-term');
|
||||
const resetButton = document.querySelector('.domains__reset-button');
|
||||
const resetSearchButton = document.querySelector('.domains__reset-search');
|
||||
const resetFiltersButton = document.querySelector('.domains__reset-filters');
|
||||
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
|
||||
const statusIndicator = document.querySelector('.domain__filter-indicator');
|
||||
const statusToggle = document.querySelector('.usa-button--filter');
|
||||
|
||||
/**
|
||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||
|
@ -1105,21 +1147,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
* @param {*} page - the page number of the results (starts with 1)
|
||||
* @param {*} sortBy - the sort column option
|
||||
* @param {*} order - the sort order {asc, desc}
|
||||
* @param {*} loaded - control for the scrollToElement functionality
|
||||
* @param {*} scroll - control for the scrollToElement functionality
|
||||
* @param {*} searchTerm - the search term
|
||||
*/
|
||||
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
|
||||
//fetch json of page of domains, given page # and sort
|
||||
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
||||
function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm) {
|
||||
// fetch json of page of domains, given params
|
||||
fetch(`/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.log('Error in AJAX call: ' + data.error);
|
||||
console.error('Error in AJAX call: ' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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, searchTermHolder, currentSearchTerm);
|
||||
updateDisplay(data, domainsWrapper, noDomainsWrapper, noSearchResultsWrapper, currentSearchTerm);
|
||||
|
||||
// identify the DOM element where the domain list will be inserted into the DOM
|
||||
const domainList = document.querySelector('.domains__table tbody');
|
||||
|
@ -1132,7 +1174,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
||||
const actionUrl = domain.action_url;
|
||||
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<th scope="row" role="rowheader" data-label="Domain name">
|
||||
|
@ -1148,7 +1189,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
data-position="top"
|
||||
title="${domain.get_state_help_text}"
|
||||
focusable="true"
|
||||
aria-label="Status Information"
|
||||
aria-label="${domain.get_state_help_text}"
|
||||
role="tooltip"
|
||||
>
|
||||
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
||||
|
@ -1169,16 +1210,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
initializeTooltips();
|
||||
|
||||
// Do not scroll on first page load
|
||||
if (loaded)
|
||||
ScrollToElement('id', 'domains-header');
|
||||
hasLoaded = true;
|
||||
if (scroll)
|
||||
ScrollToElement('class', 'domains');
|
||||
scrollToTable = true;
|
||||
|
||||
// update pagination
|
||||
updatePagination(
|
||||
'domain',
|
||||
'#domains-pagination',
|
||||
'#domains-pagination .usa-pagination__counter',
|
||||
'#domains-header',
|
||||
'#domains',
|
||||
loadDomains,
|
||||
data.page,
|
||||
data.num_pages,
|
||||
|
@ -1214,13 +1255,51 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
currentSearchTerm = domainsSearchInput.value;
|
||||
// If the search is blank, we match the resetSearch functionality
|
||||
if (currentSearchTerm) {
|
||||
showElement(resetButton);
|
||||
showElement(resetSearchButton);
|
||||
} else {
|
||||
hideElement(resetButton);
|
||||
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
|
||||
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 loadDomains with updated status
|
||||
loadDomains(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
updateStatusIndicator();
|
||||
});
|
||||
});
|
||||
|
||||
// Reset UI and accessibility
|
||||
function resetHeaders() {
|
||||
|
@ -1235,18 +1314,78 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
function resetSearch() {
|
||||
domainsSearchInput.value = '';
|
||||
currentSearchTerm = '';
|
||||
hideElement(resetButton);
|
||||
loadDomains(1, 'id', 'asc', hasLoaded, '');
|
||||
hideElement(resetSearchButton);
|
||||
loadDomains(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
}
|
||||
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', function() {
|
||||
if (resetSearchButton) {
|
||||
resetSearchButton.addEventListener('click', function() {
|
||||
resetSearch();
|
||||
});
|
||||
}
|
||||
|
||||
// Load the first page initially
|
||||
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 accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionIsOpen && !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 accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionIsOpen && !accordion.contains(event.target)) {
|
||||
closeFilters();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadDomains(1);
|
||||
}
|
||||
});
|
||||
|
@ -1279,14 +1418,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
let currentOrder = 'asc';
|
||||
const noDomainRequestsWrapper = document.querySelector('.domain-requests__no-data');
|
||||
const noSearchResultsWrapper = document.querySelector('.domain-requests__no-search-results');
|
||||
let hasLoaded = false;
|
||||
let currentSearchTerm = ''
|
||||
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 searchTermHolder = document.querySelector('.domain-requests__search-term');
|
||||
const resetButton = document.querySelector('.domain-requests__reset-button');
|
||||
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -1316,7 +1454,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
// Update data and UI
|
||||
loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, hasLoaded, currentSearchTerm);
|
||||
loadDomainRequests(pageToDisplay, currentSortBy, currentOrder, scrollToTable, currentSearchTerm);
|
||||
})
|
||||
.catch(error => console.error('Error fetching domain requests:', error));
|
||||
}
|
||||
|
@ -1332,21 +1470,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
* @param {*} page - the page number of the results (starts with 1)
|
||||
* @param {*} sortBy - the sort column option
|
||||
* @param {*} order - the sort order {asc, desc}
|
||||
* @param {*} loaded - control for the scrollToElement functionality
|
||||
* @param {*} scroll - control for the scrollToElement functionality
|
||||
* @param {*} searchTerm - the search term
|
||||
*/
|
||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, loaded = hasLoaded, searchTerm = currentSearchTerm) {
|
||||
//fetch json of page of domain requests, given page # and sort
|
||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) {
|
||||
// fetch json of page of domain requests, given params
|
||||
fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.log('Error in AJAX call: ' + data.error);
|
||||
console.error('Error in AJAX call: ' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
||||
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, searchTermHolder, currentSearchTerm);
|
||||
updateDisplay(data, domainRequestsWrapper, noDomainRequestsWrapper, noSearchResultsWrapper, currentSearchTerm);
|
||||
|
||||
// identify the DOM element where the domain request list will be inserted into the DOM
|
||||
const tbody = document.querySelector('.domain-requests__table tbody');
|
||||
|
@ -1533,16 +1671,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
|
||||
// Do not scroll on first page load
|
||||
if (loaded)
|
||||
ScrollToElement('id', 'domain-requests-header');
|
||||
hasLoaded = true;
|
||||
if (scroll)
|
||||
ScrollToElement('class', 'domain-requests');
|
||||
scrollToTable = true;
|
||||
|
||||
// update the pagination after the domain requests list is updated
|
||||
updatePagination(
|
||||
'domain request',
|
||||
'#domain-requests-pagination',
|
||||
'#domain-requests-pagination .usa-pagination__counter',
|
||||
'#domain-requests-header',
|
||||
'#domain-requests',
|
||||
loadDomainRequests,
|
||||
data.page,
|
||||
data.num_pages,
|
||||
|
@ -1577,13 +1715,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
currentSearchTerm = domainRequestsSearchInput.value;
|
||||
// If the search is blank, we match the resetSearch functionality
|
||||
if (currentSearchTerm) {
|
||||
showElement(resetButton);
|
||||
showElement(resetSearchButton);
|
||||
} else {
|
||||
hideElement(resetButton);
|
||||
hideElement(resetSearchButton);
|
||||
}
|
||||
loadDomainRequests(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
})
|
||||
});
|
||||
|
||||
// Reset UI and accessibility
|
||||
function resetHeaders() {
|
||||
|
@ -1598,24 +1736,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
function resetSearch() {
|
||||
domainRequestsSearchInput.value = '';
|
||||
currentSearchTerm = '';
|
||||
hideElement(resetButton);
|
||||
loadDomainRequests(1, 'id', 'asc', hasLoaded, '');
|
||||
hideElement(resetSearchButton);
|
||||
loadDomainRequests(1, 'id', 'asc');
|
||||
resetHeaders();
|
||||
}
|
||||
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', function() {
|
||||
if (resetSearchButton) {
|
||||
resetSearchButton.addEventListener('click', function() {
|
||||
resetSearch();
|
||||
});
|
||||
}
|
||||
|
||||
// Load the first page initially
|
||||
// Initial load
|
||||
loadDomainRequests(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An IIFE that displays confirmation modal on the user profile page
|
||||
*/
|
||||
|
|
33
src/registrar/assets/sass/_theme/_accordions.scss
Normal file
33
src/registrar/assets/sass/_theme/_accordions.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
.usa-accordion--select {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
position: relative;
|
||||
.usa-accordion__button[aria-expanded=false],
|
||||
.usa-accordion__button[aria-expanded=false]:hover,
|
||||
.usa-accordion__button[aria-expanded=true],
|
||||
.usa-accordion__button[aria-expanded=true]:hover {
|
||||
background-image: none;
|
||||
}
|
||||
.usa-accordion__content {
|
||||
// Note, width is determined by a custom width class on one of the children
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 33.88px;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
border: solid 1px color('base-lighter');
|
||||
padding: units(2) units(2) units(3) units(2);
|
||||
width: max-content;
|
||||
}
|
||||
h2 {
|
||||
font-size: size('body', 'sm');
|
||||
}
|
||||
.usa-button {
|
||||
width: 100%;
|
||||
}
|
||||
.margin-top-0 {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
|
@ -83,6 +83,10 @@ body {
|
|||
padding: 0 units(2) units(3);
|
||||
margin-top: units(3);
|
||||
|
||||
&.margin-top-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: color('primary-dark');
|
||||
margin-top: units(2);
|
||||
|
@ -96,6 +100,10 @@ body {
|
|||
@include at-media(mobile-lg) {
|
||||
margin-top: units(5);
|
||||
|
||||
&.margin-top-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -211,3 +219,7 @@ abbr[title] {
|
|||
.usa-logo button.usa-button--unstyled.disabled-button:hover{
|
||||
color: #{$dhs-dark-gray-85};
|
||||
}
|
||||
|
||||
.padding--8-8-9 {
|
||||
padding: 8px 8px 9px !important;
|
||||
}
|
||||
|
|
|
@ -161,3 +161,19 @@ a.usa-button--unstyled:visited {
|
|||
margin-left: units(2);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-button--filter {
|
||||
width: auto;
|
||||
// For mobile stacking
|
||||
margin-bottom: units(1);
|
||||
border: solid 1px color('base-light') !important;
|
||||
padding: units(1);
|
||||
color: color('primary-darker') !important;
|
||||
font-weight: font-weight('normal');
|
||||
font-size: size('ui', 'xs');
|
||||
box-shadow: none;
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,6 @@
|
|||
}
|
||||
|
||||
td .no-click-outline-and-cursor-help {
|
||||
outline: none;
|
||||
cursor: help;
|
||||
use {
|
||||
// USWDS has weird interactions with SVGs regarding tooltips,
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
@forward "typography";
|
||||
@forward "links";
|
||||
@forward "lists";
|
||||
@forward "accordions";
|
||||
@forward "buttons";
|
||||
@forward "pagination";
|
||||
@forward "forms";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% load static %}
|
||||
|
||||
<section class="section--outlined domain-requests">
|
||||
<section class="section--outlined domain-requests" id="domain-requests">
|
||||
<div class="grid-row">
|
||||
{% if portfolio is None %}
|
||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||
|
@ -11,10 +11,10 @@
|
|||
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-button display-none" type="button">
|
||||
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-search display-none" type="button">
|
||||
Reset
|
||||
</button>
|
||||
<label class="usa-sr-only" for="domain-requests__search-field">Search</label>
|
||||
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domain-requests__search-field"
|
||||
|
@ -33,7 +33,7 @@
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domain-requests__table-wrapper display-none">
|
||||
<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">
|
||||
<caption class="sr-only">Your domain requests</caption>
|
||||
<thead>
|
||||
|
@ -58,7 +58,7 @@
|
|||
<p>You haven't requested any domains.</p>
|
||||
</div>
|
||||
<div class="domain-requests__no-search-results display-none">
|
||||
<p>No results found for "<span class="domain-requests__search-term"></span>"</p>
|
||||
<p>No results found</p>
|
||||
</div>
|
||||
</section>
|
||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% load static %}
|
||||
|
||||
<section class="section--outlined domains">
|
||||
<section class="section--outlined domains{% if portfolio is not None %} margin-top-0{% endif %}" id="domains">
|
||||
<div class="grid-row">
|
||||
{% if portfolio is None %}
|
||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||
|
@ -11,10 +11,10 @@
|
|||
<section aria-label="Domains search component" class="flex-6 margin-y-2">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-button display-none" type="button">
|
||||
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-search display-none" type="button">
|
||||
Reset
|
||||
</button>
|
||||
<label class="usa-sr-only" for="domains__search-field">Search</label>
|
||||
<label class="usa-sr-only" for="domains__search-field">Search by domain name</label>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domains__search-field"
|
||||
|
@ -33,7 +33,102 @@
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domains__table-wrapper display-none">
|
||||
{% if portfolio %}
|
||||
<div class="display-flex flex-align-center margin-top-1">
|
||||
<span class="margin-right-2 margin-top-neg-1 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="domain__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-dns-needed"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="unknown"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-dns-needed"
|
||||
>DNS Needed</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-ready"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="ready"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-ready"
|
||||
>Ready</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-on-hold"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="on hold"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-on-hold"
|
||||
>On hold</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-expired"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="expired"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-expired"
|
||||
>Expired</label
|
||||
>
|
||||
</div>
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
id="filter-status-deleted"
|
||||
type="checkbox"
|
||||
name="filter-status"
|
||||
value="deleted"
|
||||
/>
|
||||
<label class="usa-checkbox__label" for="filter-status-deleted"
|
||||
>Deleted</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 domains__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="domains__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 domains__table">
|
||||
<caption class="sr-only">Your registered domains</caption>
|
||||
<thead>
|
||||
|
@ -70,7 +165,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<div class="domains__no-search-results display-none">
|
||||
<p>No results found for "<span class="domains__search-term"></span>"</p>
|
||||
<p>No results found</p>
|
||||
</div>
|
||||
</section>
|
||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
{% load static %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
<h1>Domains</h1>
|
||||
<h1 id="domains-header">Domains</h1>
|
||||
{% include "includes/domains_table.html" with portfolio=portfolio %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% load static %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
<h1>Domain requests</h1>
|
||||
<h1 id="domain-requests-header">Domain requests</h1>
|
||||
|
||||
{% comment %}
|
||||
IMPORTANT:
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="margin-bottom-4 tablet:margin-bottom-0">
|
||||
<nav aria-label="">
|
||||
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
|
||||
<ul class="usa-sidenav">
|
||||
<ul class="usa-sidenav usa-sidenav--portfolio">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'portfolio-domains' portfolio.id as url %}
|
||||
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
||||
|
|
|
@ -3545,7 +3545,7 @@ class TestMyUserAdmin(MockDb):
|
|||
)
|
||||
},
|
||||
),
|
||||
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||
("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
|
||||
("Permissions", {"fields": ("is_active", "groups")}),
|
||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
|
|
|
@ -735,7 +735,6 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
self.maxDiff = None
|
||||
expected_content = (
|
||||
# Header
|
||||
"Domain request,Submitted at,Status,Domain type,Federal type,"
|
||||
|
|
|
@ -970,7 +970,7 @@ class PortfoliosTests(TestWithUser, WebTest):
|
|||
# Assert that we're on the right page
|
||||
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
||||
|
||||
self.assertContains(portfolio_page, "<h1>Domains</h1>")
|
||||
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_no_redirect_when_org_flag_false(self):
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.urls import reverse
|
|||
from .test_views import TestWithUser
|
||||
from django_webtest import WebTest # type: ignore
|
||||
from django.utils.dateparse import parse_date
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
|
||||
|
||||
class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||
|
@ -11,9 +12,9 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
self.app.set_user(self.user.username)
|
||||
|
||||
# Create test domains
|
||||
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="active")
|
||||
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="inactive")
|
||||
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="active")
|
||||
self.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-01-01", state="unknown")
|
||||
self.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-02-01", state="dns needed")
|
||||
self.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready")
|
||||
|
||||
# Create UserDomainRoles
|
||||
UserDomainRole.objects.create(user=self.user, domain=self.domain1)
|
||||
|
@ -25,6 +26,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
UserDomainRole.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_domains_json_unauthenticated(self):
|
||||
"""for an unauthenticated user, test that the user is redirected for auth"""
|
||||
self.app.reset()
|
||||
|
@ -32,6 +34,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
response = self.client.get(reverse("get_domains_json"))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_domains_json_authenticated(self):
|
||||
"""Test that an authenticated user gets the list of 3 domains."""
|
||||
response = self.app.get(reverse("get_domains_json"))
|
||||
|
@ -102,6 +105,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
)
|
||||
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_domains_json_search(self):
|
||||
"""Test search."""
|
||||
# Define your URL variables as a dictionary
|
||||
|
@ -131,6 +135,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
domains[0],
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_pagination(self):
|
||||
"""Test that pagination is correct in the response"""
|
||||
response = self.app.get(reverse("get_domains_json"), {"page": 1})
|
||||
|
@ -143,6 +148,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
self.assertFalse(data["has_previous"])
|
||||
self.assertEqual(data["num_pages"], 1)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_sorting(self):
|
||||
"""test that sorting works properly in the response"""
|
||||
response = self.app.get(reverse("get_domains_json"), {"sort_by": "expiration_date", "order": "desc"})
|
||||
|
@ -161,6 +167,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
|
||||
self.assertEqual(expiration_dates, sorted(expiration_dates))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_sorting_by_state_display(self):
|
||||
"""test that the state_display sorting works properly"""
|
||||
response = self.app.get(reverse("get_domains_json"), {"sort_by": "state_display", "order": "asc"})
|
||||
|
@ -178,3 +185,21 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
|||
# Check if sorted by state_display in descending order
|
||||
states = [domain["state_display"] for domain in data["domains"]]
|
||||
self.assertEqual(states, sorted(states, reverse=True))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_state_filtering(self):
|
||||
"""Test that different states in request get expected responses."""
|
||||
|
||||
expected_values = [
|
||||
("unknown", 1),
|
||||
("ready", 0),
|
||||
("expired", 2),
|
||||
("ready,expired", 2),
|
||||
("unknown,expired", 3),
|
||||
]
|
||||
for state, num_domains in expected_values:
|
||||
with self.subTest(state=state, num_domains=num_domains):
|
||||
response = self.app.get(reverse("get_domains_json"), {"status": state})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
self.assertEqual(len(data["domains"]), num_domains)
|
||||
|
|
|
@ -20,11 +20,46 @@ def get_domains_json(request):
|
|||
# Handle sorting
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||
search_term = request.GET.get("search_term")
|
||||
|
||||
# Handle search term
|
||||
search_term = request.GET.get("search_term")
|
||||
if search_term:
|
||||
objects = objects.filter(Q(name__icontains=search_term))
|
||||
|
||||
# Handle state
|
||||
status_param = request.GET.get("status")
|
||||
if status_param:
|
||||
status_list = status_param.split(",")
|
||||
|
||||
# if unknown is in status_list, append 'dns needed' since both
|
||||
# unknown and dns needed display as DNS Needed, and both are
|
||||
# searchable via state parameter of 'unknown'
|
||||
if "unknown" in status_list:
|
||||
status_list.append("dns needed")
|
||||
|
||||
# Split the status list into normal states and custom states
|
||||
normal_states = [state for state in status_list if state in Domain.State.values]
|
||||
custom_states = [state for state in status_list if state == "expired"]
|
||||
|
||||
# Construct Q objects for normal states that can be queried through ORM
|
||||
state_query = Q()
|
||||
if normal_states:
|
||||
state_query |= Q(state__in=normal_states)
|
||||
|
||||
# Handle custom states in Python, as expired can not be queried through ORM
|
||||
if "expired" in custom_states:
|
||||
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
|
||||
state_query |= Q(id__in=expired_domain_ids)
|
||||
|
||||
# Apply the combined query
|
||||
objects = objects.filter(state_query)
|
||||
|
||||
# If there are filtered states, and expired is not one of them, domains with
|
||||
# state_display of 'Expired' must be removed
|
||||
if "expired" not in custom_states:
|
||||
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
|
||||
objects = objects.exclude(id__in=expired_domain_ids)
|
||||
|
||||
if sort_by == "state_display":
|
||||
# Fetch the objects and sort them in Python
|
||||
objects = list(objects) # Evaluate queryset to a list
|
||||
|
|
|
@ -68,6 +68,8 @@
|
|||
10038 OUTOFSCOPE http://app:8080/dns/dnssec
|
||||
10038 OUTOFSCOPE http://app:8080/dns/dnssec/dsdata
|
||||
10038 OUTOFSCOPE http://app:8080/org-name-address
|
||||
10038 OUTOFSCOPE http://app:8080/domain_requests/
|
||||
10038 OUTOFSCOPE http://app:8080/domains/
|
||||
# This URL always returns 404, so include it as well.
|
||||
10038 OUTOFSCOPE http://app:8080/todo
|
||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue