mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 18:56:15 +02:00
formatting
This commit is contained in:
parent
4dec61990c
commit
7d92949e46
14 changed files with 1554 additions and 1610 deletions
|
@ -1,122 +1,115 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
import { hideElement, showElement } from './helpers.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An IIFE that changes the default clear behavior on comboboxes to the input field.
|
|
||||||
* We want the search bar to act soley as a search bar.
|
|
||||||
*/
|
|
||||||
export function loadInitialValuesForComboBoxes() {
|
export function loadInitialValuesForComboBoxes() {
|
||||||
var overrideDefaultClearButton = true;
|
var overrideDefaultClearButton = true;
|
||||||
var isTyping = false;
|
var isTyping = false;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
document.addEventListener('DOMContentLoaded', (event) => {
|
||||||
handleAllComboBoxElements();
|
handleAllComboBoxElements();
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleAllComboBoxElements() {
|
function handleAllComboBoxElements() {
|
||||||
const comboBoxElements = document.querySelectorAll(".usa-combo-box");
|
const comboBoxElements = document.querySelectorAll(".usa-combo-box");
|
||||||
comboBoxElements.forEach(comboBox => {
|
comboBoxElements.forEach(comboBox => {
|
||||||
const input = comboBox.querySelector("input");
|
const input = comboBox.querySelector("input");
|
||||||
const select = comboBox.querySelector("select");
|
const select = comboBox.querySelector("select");
|
||||||
if (!input || !select) {
|
if (!input || !select) {
|
||||||
console.warn("No combobox element found");
|
console.warn("No combobox element found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Set the initial value of the combobox
|
// Set the initial value of the combobox
|
||||||
let initialValue = select.getAttribute("data-default-value");
|
let initialValue = select.getAttribute("data-default-value");
|
||||||
let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
|
let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
|
||||||
if (!clearInputButton) {
|
if (!clearInputButton) {
|
||||||
console.warn("No clear element found");
|
console.warn("No clear element found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the default clear button behavior such that it no longer clears the input,
|
// Override the default clear button behavior such that it no longer clears the input,
|
||||||
// it just resets to the data-initial-value.
|
// it just resets to the data-initial-value.
|
||||||
|
|
||||||
// Due to the nature of how uswds works, this is slightly hacky.
|
// Due to the nature of how uswds works, this is slightly hacky.
|
||||||
|
|
||||||
// Use a MutationObserver to watch for changes in the dropdown list
|
// Use a MutationObserver to watch for changes in the dropdown list
|
||||||
const dropdownList = comboBox.querySelector(`#${input.id}--list`);
|
const dropdownList = comboBox.querySelector(`#${input.id}--list`);
|
||||||
const observer = new MutationObserver(function(mutations) {
|
const observer = new MutationObserver(function(mutations) {
|
||||||
mutations.forEach(function(mutation) {
|
mutations.forEach(function(mutation) {
|
||||||
if (mutation.type === "childList") {
|
if (mutation.type === "childList") {
|
||||||
addBlankOption(clearInputButton, dropdownList, initialValue);
|
addBlankOption(clearInputButton, dropdownList, initialValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure the observer to watch for changes in the dropdown list
|
||||||
|
const config = { childList: true, subtree: true };
|
||||||
|
observer.observe(dropdownList, config);
|
||||||
|
|
||||||
|
// Input event listener to detect typing
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
isTyping = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Blur event listener to reset typing state
|
||||||
|
input.addEventListener("blur", () => {
|
||||||
|
isTyping = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide the reset button when there is nothing to reset.
|
||||||
|
// Do this once on init, then everytime a change occurs.
|
||||||
|
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
||||||
|
select.addEventListener("change", () => {
|
||||||
|
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change the default input behaviour - have it reset to the data default instead
|
||||||
|
clearInputButton.addEventListener("click", (e) => {
|
||||||
|
if (overrideDefaultClearButton && initialValue) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
input.click();
|
||||||
|
// Find the dropdown option with the desired value
|
||||||
|
const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option");
|
||||||
|
if (dropdownOptions) {
|
||||||
|
dropdownOptions.forEach(option => {
|
||||||
|
if (option.getAttribute("data-value") === initialValue) {
|
||||||
|
// Simulate a click event on the dropdown option
|
||||||
|
option.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Configure the observer to watch for changes in the dropdown list
|
|
||||||
const config = { childList: true, subtree: true };
|
|
||||||
observer.observe(dropdownList, config);
|
|
||||||
|
|
||||||
// Input event listener to detect typing
|
|
||||||
input.addEventListener("input", () => {
|
|
||||||
isTyping = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Blur event listener to reset typing state
|
|
||||||
input.addEventListener("blur", () => {
|
|
||||||
isTyping = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hide the reset button when there is nothing to reset.
|
|
||||||
// Do this once on init, then everytime a change occurs.
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
select.addEventListener("change", () => {
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change the default input behaviour - have it reset to the data default instead
|
|
||||||
clearInputButton.addEventListener("click", (e) => {
|
|
||||||
if (overrideDefaultClearButton && initialValue) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
input.click();
|
|
||||||
// Find the dropdown option with the desired value
|
|
||||||
const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option");
|
|
||||||
if (dropdownOptions) {
|
|
||||||
dropdownOptions.forEach(option => {
|
|
||||||
if (option.getAttribute("data-value") === initialValue) {
|
|
||||||
// Simulate a click event on the dropdown option
|
|
||||||
option.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateClearButtonVisibility(select, initialValue, clearInputButton) {
|
function updateClearButtonVisibility(select, initialValue, clearInputButton) {
|
||||||
if (select.value === initialValue) {
|
if (select.value === initialValue) {
|
||||||
hideElement(clearInputButton);
|
hideElement(clearInputButton);
|
||||||
}else {
|
}else {
|
||||||
showElement(clearInputButton)
|
showElement(clearInputButton)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBlankOption(clearInputButton, dropdownList, initialValue) {
|
|
||||||
if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) {
|
|
||||||
const blankOption = document.createElement("li");
|
|
||||||
blankOption.setAttribute("role", "option");
|
|
||||||
blankOption.setAttribute("data-value", "");
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option");
|
|
||||||
if (!initialValue){
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option--selected")
|
|
||||||
}
|
|
||||||
blankOption.textContent = "⎯";
|
|
||||||
|
|
||||||
dropdownList.insertBefore(blankOption, dropdownList.firstChild);
|
|
||||||
blankOption.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
overrideDefaultClearButton = false;
|
|
||||||
// Trigger the default clear behavior
|
|
||||||
clearInputButton.click();
|
|
||||||
overrideDefaultClearButton = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addBlankOption(clearInputButton, dropdownList, initialValue) {
|
||||||
|
if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) {
|
||||||
|
const blankOption = document.createElement("li");
|
||||||
|
blankOption.setAttribute("role", "option");
|
||||||
|
blankOption.setAttribute("data-value", "");
|
||||||
|
blankOption.classList.add("usa-combo-box__list-option");
|
||||||
|
if (!initialValue){
|
||||||
|
blankOption.classList.add("usa-combo-box__list-option--selected")
|
||||||
|
}
|
||||||
|
blankOption.textContent = "⎯";
|
||||||
|
|
||||||
|
dropdownList.insertBefore(blankOption, dropdownList.firstChild);
|
||||||
|
blankOption.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
overrideDefaultClearButton = false;
|
||||||
|
// Trigger the default clear behavior
|
||||||
|
clearInputButton.click();
|
||||||
|
overrideDefaultClearButton = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
|
||||||
// Event handlers.
|
|
||||||
|
|
||||||
var DEFAULT_ERROR = "Please check this field for errors.";
|
var DEFAULT_ERROR = "Please check this field for errors.";
|
||||||
|
|
||||||
var INFORMATIVE = "info";
|
|
||||||
var WARNING = "warning";
|
|
||||||
var ERROR = "error";
|
var ERROR = "error";
|
||||||
var SUCCESS = "success";
|
var SUCCESS = "success";
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
import { hookupYesNoListener, hookupRadioTogglerListener } from './radios.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
|
||||||
import { hookupYesNoListener, hookupRadioTogglerListener } from './helpers-radios.js';
|
|
||||||
import { getCsrfToken } from './get-csrf-token.js';
|
|
||||||
import { initDomainValidators } from './domain-validators.js';
|
import { initDomainValidators } from './domain-validators.js';
|
||||||
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
||||||
import { initializeUrbanizationToggle } from './urbanization.js';
|
import { initializeUrbanizationToggle } from './urbanization.js';
|
||||||
|
@ -43,6 +40,3 @@ initDomainsTable();
|
||||||
initDomainRequestsTable();
|
initDomainRequestsTable();
|
||||||
initMembersTable();
|
initMembersTable();
|
||||||
initMemberDomainsTable();
|
initMemberDomainsTable();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
import { hideElement, showElement } from './helpers.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
|
||||||
/** An IIFE that intializes the requesting entity page.
|
/** An IIFE that intializes the requesting entity page.
|
||||||
* This page has a radio button that dynamically toggles some fields
|
* This page has a radio button that dynamically toggles some fields
|
||||||
* Within that, the dropdown also toggles some additional form elements.
|
* Within that, the dropdown also toggles some additional form elements.
|
||||||
|
@ -20,27 +20,27 @@ export function handleRequestingEntityFieldset() {
|
||||||
var requestingNewSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`);
|
var requestingNewSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`);
|
||||||
|
|
||||||
function toggleSuborganization(radio=null) {
|
function toggleSuborganization(radio=null) {
|
||||||
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
||||||
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
||||||
requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False";
|
requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False";
|
||||||
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add fake "other" option to sub_organization select
|
// Add fake "other" option to sub_organization select
|
||||||
if (select && !Array.from(select.options).some(option => option.value === "other")) {
|
if (select && !Array.from(select.options).some(option => option.value === "other")) {
|
||||||
select.add(new Option("Other (enter your organization manually)", "other"));
|
select.add(new Option("Other (enter your organization manually)", "other"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestingNewSuborganization.value === "True") {
|
if (requestingNewSuborganization.value === "True") {
|
||||||
select.value = "other";
|
select.value = "other";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listener to is_suborganization radio buttons, and run for initial display
|
// Add event listener to is_suborganization radio buttons, and run for initial display
|
||||||
toggleSuborganization();
|
toggleSuborganization();
|
||||||
radios.forEach(radio => {
|
radios.forEach(radio => {
|
||||||
radio.addEventListener("click", () => toggleSuborganization(radio));
|
radio.addEventListener("click", () => toggleSuborganization(radio));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener to the suborg dropdown to show/hide the suborg details section
|
// Add event listener to the suborg dropdown to show/hide the suborg details section
|
||||||
select.addEventListener("change", () => toggleSuborganization());
|
select.addEventListener("change", () => toggleSuborganization());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,359 +1,358 @@
|
||||||
import { hideElement, showElement, toggleCaret } from './helpers.js';
|
import { hideElement, showElement, toggleCaret } from './helpers.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
|
||||||
|
|
||||||
export class LoadTableBase {
|
export class LoadTableBase {
|
||||||
constructor(sectionSelector) {
|
constructor(sectionSelector) {
|
||||||
this.tableWrapper = document.getElementById(`${sectionSelector}__table-wrapper`);
|
this.tableWrapper = document.getElementById(`${sectionSelector}__table-wrapper`);
|
||||||
this.tableHeaders = document.querySelectorAll(`#${sectionSelector} th[data-sortable]`);
|
this.tableHeaders = document.querySelectorAll(`#${sectionSelector} th[data-sortable]`);
|
||||||
this.currentSortBy = 'id';
|
this.currentSortBy = 'id';
|
||||||
this.currentOrder = 'asc';
|
this.currentOrder = 'asc';
|
||||||
this.currentStatus = [];
|
this.currentStatus = [];
|
||||||
this.currentSearchTerm = '';
|
this.currentSearchTerm = '';
|
||||||
this.scrollToTable = false;
|
this.scrollToTable = false;
|
||||||
this.searchInput = document.getElementById(`${sectionSelector}__search-field`);
|
this.searchInput = document.getElementById(`${sectionSelector}__search-field`);
|
||||||
this.searchSubmit = document.getElementById(`${sectionSelector}__search-field-submit`);
|
this.searchSubmit = document.getElementById(`${sectionSelector}__search-field-submit`);
|
||||||
this.tableAnnouncementRegion = document.getElementById(`${sectionSelector}__usa-table__announcement-region`);
|
this.tableAnnouncementRegion = document.getElementById(`${sectionSelector}__usa-table__announcement-region`);
|
||||||
this.resetSearchButton = document.getElementById(`${sectionSelector}__reset-search`);
|
this.resetSearchButton = document.getElementById(`${sectionSelector}__reset-search`);
|
||||||
this.resetFiltersButton = document.getElementById(`${sectionSelector}__reset-filters`);
|
this.resetFiltersButton = document.getElementById(`${sectionSelector}__reset-filters`);
|
||||||
this.statusCheckboxes = document.querySelectorAll(`.${sectionSelector} input[name="filter-status"]`);
|
this.statusCheckboxes = document.querySelectorAll(`.${sectionSelector} input[name="filter-status"]`);
|
||||||
this.statusIndicator = document.getElementById(`${sectionSelector}__filter-indicator`);
|
this.statusIndicator = document.getElementById(`${sectionSelector}__filter-indicator`);
|
||||||
this.statusToggle = document.getElementById(`${sectionSelector}__usa-button--filter`);
|
this.statusToggle = document.getElementById(`${sectionSelector}__usa-button--filter`);
|
||||||
this.noTableWrapper = document.getElementById(`${sectionSelector}__no-data`);
|
this.noTableWrapper = document.getElementById(`${sectionSelector}__no-data`);
|
||||||
this.noSearchResultsWrapper = document.getElementById(`${sectionSelector}__no-search-results`);
|
this.noSearchResultsWrapper = document.getElementById(`${sectionSelector}__no-search-results`);
|
||||||
this.portfolioElement = document.getElementById('portfolio-js-value');
|
this.portfolioElement = document.getElementById('portfolio-js-value');
|
||||||
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
|
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
|
||||||
this.initializeTableHeaders();
|
this.initializeTableHeaders();
|
||||||
this.initializeSearchHandler();
|
this.initializeSearchHandler();
|
||||||
this.initializeStatusToggleHandler();
|
this.initializeStatusToggleHandler();
|
||||||
this.initializeFilterCheckboxes();
|
this.initializeFilterCheckboxes();
|
||||||
this.initializeResetSearchButton();
|
this.initializeResetSearchButton();
|
||||||
this.initializeResetFiltersButton();
|
this.initializeResetFiltersButton();
|
||||||
this.initializeAccordionAccessibilityListeners();
|
this.initializeAccordionAccessibilityListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generalized function to update pagination for a list.
|
||||||
|
* @param {string} itemName - The name displayed in the counter
|
||||||
|
* @param {string} paginationSelector - CSS selector for the pagination container.
|
||||||
|
* @param {string} counterSelector - CSS selector for the pagination counter.
|
||||||
|
* @param {string} tableSelector - CSS selector for the header element to anchor the links to.
|
||||||
|
* @param {number} currentPage - The current page number (starting with 1).
|
||||||
|
* @param {number} numPages - The total number of pages.
|
||||||
|
* @param {boolean} hasPrevious - Whether there is a page before the current page.
|
||||||
|
* @param {boolean} hasNext - Whether there is a page after the current page.
|
||||||
|
* @param {number} total - The total number of items.
|
||||||
|
*/
|
||||||
|
updatePagination(
|
||||||
|
itemName,
|
||||||
|
paginationSelector,
|
||||||
|
counterSelector,
|
||||||
|
parentTableSelector,
|
||||||
|
currentPage,
|
||||||
|
numPages,
|
||||||
|
hasPrevious,
|
||||||
|
hasNext,
|
||||||
|
totalItems,
|
||||||
|
) {
|
||||||
|
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
||||||
|
const counterSelectorEl = document.querySelector(counterSelector);
|
||||||
|
const paginationSelectorEl = document.querySelector(paginationSelector);
|
||||||
|
counterSelectorEl.innerHTML = '';
|
||||||
|
paginationButtons.innerHTML = '';
|
||||||
|
|
||||||
|
// Buttons should only be displayed if there are more than one pages of results
|
||||||
|
paginationButtons.classList.toggle('display-none', numPages <= 1);
|
||||||
|
|
||||||
|
// Counter should only be displayed if there is more than 1 item
|
||||||
|
paginationSelectorEl.classList.toggle('display-none', totalItems < 1);
|
||||||
|
|
||||||
|
counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`;
|
||||||
|
|
||||||
|
if (hasPrevious) {
|
||||||
|
const prevPageItem = document.createElement('li');
|
||||||
|
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
|
prevPageItem.innerHTML = `
|
||||||
|
<a href="${parentTableSelector}" 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>
|
||||||
|
<span class="usa-pagination__link-text">Previous</span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
prevPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.loadTable(currentPage - 1);
|
||||||
|
});
|
||||||
|
paginationButtons.appendChild(prevPageItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Add first page and ellipsis if necessary
|
||||||
* Generalized function to update pagination for a list.
|
if (currentPage > 2) {
|
||||||
* @param {string} itemName - The name displayed in the counter
|
paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage));
|
||||||
* @param {string} paginationSelector - CSS selector for the pagination container.
|
if (currentPage > 3) {
|
||||||
* @param {string} counterSelector - CSS selector for the pagination counter.
|
const ellipsis = document.createElement('li');
|
||||||
* @param {string} tableSelector - CSS selector for the header element to anchor the links to.
|
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
||||||
* @param {number} currentPage - The current page number (starting with 1).
|
ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
|
||||||
* @param {number} numPages - The total number of pages.
|
ellipsis.innerHTML = '<span>…</span>';
|
||||||
* @param {boolean} hasPrevious - Whether there is a page before the current page.
|
paginationButtons.appendChild(ellipsis);
|
||||||
* @param {boolean} hasNext - Whether there is a page after the current page.
|
|
||||||
* @param {number} total - The total number of items.
|
|
||||||
*/
|
|
||||||
updatePagination(
|
|
||||||
itemName,
|
|
||||||
paginationSelector,
|
|
||||||
counterSelector,
|
|
||||||
parentTableSelector,
|
|
||||||
currentPage,
|
|
||||||
numPages,
|
|
||||||
hasPrevious,
|
|
||||||
hasNext,
|
|
||||||
totalItems,
|
|
||||||
) {
|
|
||||||
const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`);
|
|
||||||
const counterSelectorEl = document.querySelector(counterSelector);
|
|
||||||
const paginationSelectorEl = document.querySelector(paginationSelector);
|
|
||||||
counterSelectorEl.innerHTML = '';
|
|
||||||
paginationButtons.innerHTML = '';
|
|
||||||
|
|
||||||
// Buttons should only be displayed if there are more than one pages of results
|
|
||||||
paginationButtons.classList.toggle('display-none', numPages <= 1);
|
|
||||||
|
|
||||||
// Counter should only be displayed if there is more than 1 item
|
|
||||||
paginationSelectorEl.classList.toggle('display-none', totalItems < 1);
|
|
||||||
|
|
||||||
counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`;
|
|
||||||
|
|
||||||
if (hasPrevious) {
|
|
||||||
const prevPageItem = document.createElement('li');
|
|
||||||
prevPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
|
||||||
prevPageItem.innerHTML = `
|
|
||||||
<a href="${parentTableSelector}" 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>
|
|
||||||
<span class="usa-pagination__link-text">Previous</span>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
prevPageItem.querySelector('a').addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.loadTable(currentPage - 1);
|
|
||||||
});
|
|
||||||
paginationButtons.appendChild(prevPageItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add first page and ellipsis if necessary
|
|
||||||
if (currentPage > 2) {
|
|
||||||
paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage));
|
|
||||||
if (currentPage > 3) {
|
|
||||||
const ellipsis = document.createElement('li');
|
|
||||||
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
|
||||||
ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
|
|
||||||
ellipsis.innerHTML = '<span>…</span>';
|
|
||||||
paginationButtons.appendChild(ellipsis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add pages around the current page
|
|
||||||
for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) {
|
|
||||||
paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add last page and ellipsis if necessary
|
|
||||||
if (currentPage < numPages - 1) {
|
|
||||||
if (currentPage < numPages - 2) {
|
|
||||||
const ellipsis = document.createElement('li');
|
|
||||||
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
|
||||||
ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
|
|
||||||
ellipsis.innerHTML = '<span>…</span>';
|
|
||||||
paginationButtons.appendChild(ellipsis);
|
|
||||||
}
|
|
||||||
paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNext) {
|
|
||||||
const nextPageItem = document.createElement('li');
|
|
||||||
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
|
||||||
nextPageItem.innerHTML = `
|
|
||||||
<a href="${parentTableSelector}" 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>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
nextPageItem.querySelector('a').addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.loadTable(currentPage + 1);
|
|
||||||
});
|
|
||||||
paginationButtons.appendChild(nextPageItem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Add pages around the current page
|
||||||
* A helper that toggles content/ no content/ no search results
|
for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) {
|
||||||
*
|
paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage));
|
||||||
*/
|
}
|
||||||
updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
|
||||||
const { unfiltered_total, total } = data;
|
// Add last page and ellipsis if necessary
|
||||||
if (unfiltered_total) {
|
if (currentPage < numPages - 1) {
|
||||||
if (total) {
|
if (currentPage < numPages - 2) {
|
||||||
showElement(dataWrapper);
|
const ellipsis = document.createElement('li');
|
||||||
hideElement(noSearchResultsWrapper);
|
ellipsis.className = 'usa-pagination__item usa-pagination__overflow';
|
||||||
hideElement(noDataWrapper);
|
ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages');
|
||||||
} else {
|
ellipsis.innerHTML = '<span>…</span>';
|
||||||
hideElement(dataWrapper);
|
paginationButtons.appendChild(ellipsis);
|
||||||
showElement(noSearchResultsWrapper);
|
}
|
||||||
hideElement(noDataWrapper);
|
paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasNext) {
|
||||||
|
const nextPageItem = document.createElement('li');
|
||||||
|
nextPageItem.className = 'usa-pagination__item usa-pagination__arrow';
|
||||||
|
nextPageItem.innerHTML = `
|
||||||
|
<a href="${parentTableSelector}" 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>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
nextPageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.loadTable(currentPage + 1);
|
||||||
|
});
|
||||||
|
paginationButtons.appendChild(nextPageItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper that toggles content/ no content/ no search results
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
|
||||||
|
const { unfiltered_total, total } = data;
|
||||||
|
if (unfiltered_total) {
|
||||||
|
if (total) {
|
||||||
|
showElement(dataWrapper);
|
||||||
|
hideElement(noSearchResultsWrapper);
|
||||||
|
hideElement(noDataWrapper);
|
||||||
} else {
|
} else {
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
showElement(noSearchResultsWrapper);
|
||||||
showElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
}
|
}
|
||||||
};
|
} else {
|
||||||
|
hideElement(dataWrapper);
|
||||||
// Helper function to create a page item
|
hideElement(noSearchResultsWrapper);
|
||||||
createPageItem(page, parentTableSelector, currentPage) {
|
showElement(noDataWrapper);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
// Helper function to create a page item
|
||||||
* A helper that resets sortable table headers
|
createPageItem(page, parentTableSelector, currentPage) {
|
||||||
*
|
const pageItem = document.createElement('li');
|
||||||
*/
|
pageItem.className = 'usa-pagination__item usa-pagination__page-no';
|
||||||
unsetHeader = (header) => {
|
pageItem.innerHTML = `
|
||||||
header.removeAttribute('aria-sort');
|
<a href="${parentTableSelector}" class="usa-pagination__button" aria-label="Page ${page}">${page}</a>
|
||||||
let headerName = header.innerText;
|
`;
|
||||||
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
|
if (page === currentPage) {
|
||||||
const headerButtonLabel = `Click to sort by ascending order.`;
|
pageItem.querySelector('a').classList.add('usa-current');
|
||||||
header.setAttribute("aria-label", headerLabel);
|
pageItem.querySelector('a').setAttribute('aria-current', 'page');
|
||||||
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Abstract method (to be implemented in the child class)
|
|
||||||
loadTable(page, sortBy, order) {
|
|
||||||
throw new Error('loadData() must be implemented in a subclass');
|
|
||||||
}
|
}
|
||||||
|
pageItem.querySelector('a').addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.loadTable(page);
|
||||||
|
});
|
||||||
|
return pageItem;
|
||||||
|
}
|
||||||
|
|
||||||
// Add event listeners to table headers for sorting
|
/**
|
||||||
initializeTableHeaders() {
|
* A helper that resets sortable table headers
|
||||||
this.tableHeaders.forEach(header => {
|
*
|
||||||
header.addEventListener('click', () => {
|
*/
|
||||||
const sortBy = header.getAttribute('data-sortable');
|
unsetHeader = (header) => {
|
||||||
let order = 'asc';
|
header.removeAttribute('aria-sort');
|
||||||
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
let headerName = header.innerText;
|
||||||
// is selecting the same column to sort in descending order
|
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
|
||||||
if (sortBy === this.currentSortBy) {
|
const headerButtonLabel = `Click to sort by ascending order.`;
|
||||||
order = this.currentOrder === 'asc' ? 'desc' : 'asc';
|
header.setAttribute("aria-label", headerLabel);
|
||||||
}
|
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
|
||||||
// load the results with the updated sort
|
};
|
||||||
this.loadTable(1, sortBy, order);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeSearchHandler() {
|
// Abstract method (to be implemented in the child class)
|
||||||
this.searchSubmit.addEventListener('click', (e) => {
|
loadTable(page, sortBy, order) {
|
||||||
e.preventDefault();
|
throw new Error('loadData() must be implemented in a subclass');
|
||||||
this.currentSearchTerm = this.searchInput.value;
|
}
|
||||||
// If the search is blank, we match the resetSearch functionality
|
|
||||||
if (this.currentSearchTerm) {
|
// Add event listeners to table headers for sorting
|
||||||
showElement(this.resetSearchButton);
|
initializeTableHeaders() {
|
||||||
} else {
|
this.tableHeaders.forEach(header => {
|
||||||
hideElement(this.resetSearchButton);
|
header.addEventListener('click', () => {
|
||||||
|
const sortBy = header.getAttribute('data-sortable');
|
||||||
|
let order = 'asc';
|
||||||
|
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||||
|
// is selecting the same column to sort in descending order
|
||||||
|
if (sortBy === this.currentSortBy) {
|
||||||
|
order = this.currentOrder === 'asc' ? 'desc' : 'asc';
|
||||||
}
|
}
|
||||||
this.loadTable(1, 'id', 'asc');
|
// load the results with the updated sort
|
||||||
this.resetHeaders();
|
this.loadTable(1, sortBy, order);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initializeStatusToggleHandler() {
|
initializeSearchHandler() {
|
||||||
if (this.statusToggle) {
|
this.searchSubmit.addEventListener('click', (e) => {
|
||||||
this.statusToggle.addEventListener('click', () => {
|
e.preventDefault();
|
||||||
toggleCaret(this.statusToggle);
|
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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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.loadTable(1, 'id', 'asc');
|
||||||
this.resetHeaders();
|
this.resetHeaders();
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initializeResetSearchButton() {
|
initializeStatusToggleHandler() {
|
||||||
if (this.resetSearchButton) {
|
if (this.statusToggle) {
|
||||||
this.resetSearchButton.addEventListener('click', () => {
|
this.statusToggle.addEventListener('click', () => {
|
||||||
this.resetSearch();
|
toggleCaret(this.statusToggle);
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
import { hideElement, showElement, scrollToElement } from './helpers.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
import { initializeModals, unloadModals } from './helpers-uswds.js';
|
||||||
import { getCsrfToken } from './get-csrf-token.js';
|
import { getCsrfToken } from './helpers-csrf-token.js';
|
||||||
|
|
||||||
import { LoadTableBase } from './table-base.js';
|
import { LoadTableBase } from './table-base.js';
|
||||||
|
|
||||||
|
@ -22,381 +22,375 @@ const utcDateString = (dateString) => {
|
||||||
|
|
||||||
export class DomainRequestsTable extends LoadTableBase {
|
export class DomainRequestsTable extends LoadTableBase {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('domain-requests');
|
super('domain-requests');
|
||||||
}
|
|
||||||
|
|
||||||
toggleExportButton(requests) {
|
|
||||||
const exportButton = document.getElementById('export-csv');
|
|
||||||
if (exportButton) {
|
|
||||||
if (requests.length > 0) {
|
|
||||||
showElement(exportButton);
|
|
||||||
} else {
|
|
||||||
hideElement(exportButton);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
toggleExportButton(requests) {
|
||||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
const exportButton = document.getElementById('export-csv');
|
||||||
* based on the supplied attributes.
|
if (exportButton) {
|
||||||
* @param {*} page - the page number of the results (starts with 1)
|
if (requests.length > 0) {
|
||||||
* @param {*} sortBy - the sort column option
|
showElement(exportButton);
|
||||||
* @param {*} order - the sort order {asc, desc}
|
} else {
|
||||||
* @param {*} scroll - control for the scrollToElement functionality
|
hideElement(exportButton);
|
||||||
* @param {*} status - control for the status filter
|
|
||||||
* @param {*} searchTerm - the search term
|
|
||||||
* @param {*} portfolio - the portfolio id
|
|
||||||
*/
|
|
||||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) {
|
|
||||||
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
|
||||||
|
|
||||||
if (!baseUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let baseUrlValue = baseUrl.innerHTML;
|
|
||||||
if (!baseUrlValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add searchParams
|
|
||||||
let searchParams = new URLSearchParams(
|
|
||||||
{
|
|
||||||
"page": page,
|
|
||||||
"sort_by": sortBy,
|
|
||||||
"order": order,
|
|
||||||
"status": status,
|
|
||||||
"search_term": searchTerm
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
if (portfolio)
|
}
|
||||||
searchParams.append("portfolio", portfolio)
|
|
||||||
|
|
||||||
let url = `${baseUrlValue}?${searchParams.toString()}`
|
/**
|
||||||
fetch(url)
|
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||||
.then(response => response.json())
|
* based on the supplied attributes.
|
||||||
.then(data => {
|
* @param {*} page - the page number of the results (starts with 1)
|
||||||
if (data.error) {
|
* @param {*} sortBy - the sort column option
|
||||||
console.error('Error in AJAX call: ' + data.error);
|
* @param {*} order - the sort order {asc, desc}
|
||||||
return;
|
* @param {*} scroll - control for the scrollToElement functionality
|
||||||
|
* @param {*} status - control for the status filter
|
||||||
|
* @param {*} searchTerm - the search term
|
||||||
|
* @param {*} portfolio - the portfolio id
|
||||||
|
*/
|
||||||
|
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||||
|
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseUrlValue = baseUrl.innerHTML;
|
||||||
|
if (!baseUrlValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add searchParams
|
||||||
|
let searchParams = new URLSearchParams(
|
||||||
|
{
|
||||||
|
"page": page,
|
||||||
|
"sort_by": sortBy,
|
||||||
|
"order": order,
|
||||||
|
"status": status,
|
||||||
|
"search_term": searchTerm
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (portfolio)
|
||||||
|
searchParams.append("portfolio", portfolio)
|
||||||
|
|
||||||
|
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error in AJAX call: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage "export as CSV" visibility for domain requests
|
||||||
|
this.toggleExportButton(data.domain_requests);
|
||||||
|
|
||||||
|
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
||||||
|
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||||
|
|
||||||
|
// identify the DOM element where the domain request list will be inserted into the DOM
|
||||||
|
const tbody = document.querySelector('#domain-requests tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
// Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases
|
||||||
|
// We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete,
|
||||||
|
// which will cause bad delete requests to be sent.
|
||||||
|
const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]');
|
||||||
|
preExistingModalPlaceholders.forEach(element => {
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove any existing modal elements from the DOM so they can be properly re-initialized
|
||||||
|
// after the DOM content changes and there are new delete modal buttons added
|
||||||
|
unloadModals();
|
||||||
|
|
||||||
|
let needsDeleteColumn = false;
|
||||||
|
|
||||||
|
needsDeleteColumn = data.domain_requests.some(request => request.is_deletable);
|
||||||
|
|
||||||
|
// Remove existing delete th and td if they exist
|
||||||
|
let existingDeleteTh = document.querySelector('.delete-header');
|
||||||
|
if (!needsDeleteColumn) {
|
||||||
|
if (existingDeleteTh)
|
||||||
|
existingDeleteTh.remove();
|
||||||
|
} else {
|
||||||
|
if (!existingDeleteTh) {
|
||||||
|
const delheader = document.createElement('th');
|
||||||
|
delheader.setAttribute('scope', 'col');
|
||||||
|
delheader.setAttribute('role', 'columnheader');
|
||||||
|
delheader.setAttribute('class', 'delete-header');
|
||||||
|
delheader.innerHTML = `
|
||||||
|
<span class="usa-sr-only">Delete Action</span>`;
|
||||||
|
let tableHeaderRow = document.querySelector('#domain-requests thead tr');
|
||||||
|
tableHeaderRow.appendChild(delheader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.domain_requests.forEach(request => {
|
||||||
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||||
|
const domainName = request.requested_domain ? request.requested_domain : `New domain request <br><span class="text-base font-body-xs">(${utcDateString(request.created_at)})</span>`;
|
||||||
|
const actionUrl = request.action_url;
|
||||||
|
const actionLabel = request.action_label;
|
||||||
|
const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
||||||
|
|
||||||
|
// The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page)
|
||||||
|
// If the request is not deletable, use the following (hidden) span for ANDI screenreaders to indicate this state to the end user
|
||||||
|
let modalTrigger = `
|
||||||
|
<span class="usa-sr-only">Domain request cannot be deleted now. Edit the request for more information.</span>`;
|
||||||
|
|
||||||
|
let markupCreatorRow = '';
|
||||||
|
|
||||||
|
if (this.portfolioValue) {
|
||||||
|
markupCreatorRow = `
|
||||||
|
<td>
|
||||||
|
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
|
||||||
|
</td>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manage "export as CSV" visibility for domain requests
|
if (request.is_deletable) {
|
||||||
this.toggleExportButton(data.domain_requests);
|
// If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages
|
||||||
|
let modalHeading = '';
|
||||||
|
let modalDescription = '';
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
if (request.requested_domain) {
|
||||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
modalHeading = `Are you sure you want to delete ${request.requested_domain}?`;
|
||||||
|
modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.';
|
||||||
// identify the DOM element where the domain request list will be inserted into the DOM
|
} else {
|
||||||
const tbody = document.querySelector('#domain-requests tbody');
|
if (request.created_at) {
|
||||||
tbody.innerHTML = '';
|
modalHeading = 'Are you sure you want to delete this domain request?';
|
||||||
|
modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`;
|
||||||
// Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases
|
|
||||||
// We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete,
|
|
||||||
// which will cause bad delete requests to be sent.
|
|
||||||
const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]');
|
|
||||||
preExistingModalPlaceholders.forEach(element => {
|
|
||||||
element.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
// remove any existing modal elements from the DOM so they can be properly re-initialized
|
|
||||||
// after the DOM content changes and there are new delete modal buttons added
|
|
||||||
unloadModals();
|
|
||||||
|
|
||||||
let needsDeleteColumn = false;
|
|
||||||
|
|
||||||
needsDeleteColumn = data.domain_requests.some(request => request.is_deletable);
|
|
||||||
|
|
||||||
// Remove existing delete th and td if they exist
|
|
||||||
let existingDeleteTh = document.querySelector('.delete-header');
|
|
||||||
if (!needsDeleteColumn) {
|
|
||||||
if (existingDeleteTh)
|
|
||||||
existingDeleteTh.remove();
|
|
||||||
} else {
|
|
||||||
if (!existingDeleteTh) {
|
|
||||||
const delheader = document.createElement('th');
|
|
||||||
delheader.setAttribute('scope', 'col');
|
|
||||||
delheader.setAttribute('role', 'columnheader');
|
|
||||||
delheader.setAttribute('class', 'delete-header');
|
|
||||||
delheader.innerHTML = `
|
|
||||||
<span class="usa-sr-only">Delete Action</span>`;
|
|
||||||
let tableHeaderRow = document.querySelector('#domain-requests thead tr');
|
|
||||||
tableHeaderRow.appendChild(delheader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data.domain_requests.forEach(request => {
|
|
||||||
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
|
||||||
const domainName = request.requested_domain ? request.requested_domain : `New domain request <br><span class="text-base font-body-xs">(${utcDateString(request.created_at)})</span>`;
|
|
||||||
const actionUrl = request.action_url;
|
|
||||||
const actionLabel = request.action_label;
|
|
||||||
const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
|
||||||
|
|
||||||
// The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page)
|
|
||||||
// If the request is not deletable, use the following (hidden) span for ANDI screenreaders to indicate this state to the end user
|
|
||||||
let modalTrigger = `
|
|
||||||
<span class="usa-sr-only">Domain request cannot be deleted now. Edit the request for more information.</span>`;
|
|
||||||
|
|
||||||
let markupCreatorRow = '';
|
|
||||||
|
|
||||||
if (this.portfolioValue) {
|
|
||||||
markupCreatorRow = `
|
|
||||||
<td>
|
|
||||||
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
|
|
||||||
</td>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.is_deletable) {
|
|
||||||
// If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages
|
|
||||||
let modalHeading = '';
|
|
||||||
let modalDescription = '';
|
|
||||||
|
|
||||||
if (request.requested_domain) {
|
|
||||||
modalHeading = `Are you sure you want to delete ${request.requested_domain}?`;
|
|
||||||
modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.';
|
|
||||||
} else {
|
} else {
|
||||||
if (request.created_at) {
|
modalHeading = 'Are you sure you want to delete New domain request?';
|
||||||
modalHeading = 'Are you sure you want to delete this domain request?';
|
modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.';
|
||||||
modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`;
|
|
||||||
} else {
|
|
||||||
modalHeading = 'Are you sure you want to delete New domain request?';
|
|
||||||
modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
modalTrigger = `
|
modalTrigger = `
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
id="button-toggle-delete-domain-alert-${request.id}"
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
href="#toggle-delete-domain-alert-${request.id}"
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger line-height-sans-5"
|
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger line-height-sans-5"
|
||||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
data-open-modal
|
data-open-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
|
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||||
|
</a>`
|
||||||
|
|
||||||
|
const modalSubmit = `
|
||||||
|
<button type="button"
|
||||||
|
class="usa-button usa-button--secondary usa-modal__submit"
|
||||||
|
data-pk = ${request.id}
|
||||||
|
name="delete-domain-request">Yes, delete request</button>
|
||||||
|
`
|
||||||
|
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.setAttribute('class', 'usa-modal');
|
||||||
|
modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`);
|
||||||
|
modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?');
|
||||||
|
modal.setAttribute('aria-describedby', 'Domain will be removed');
|
||||||
|
modal.setAttribute('data-force-action', '');
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
${modalHeading}
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p id="modal-1-description">
|
||||||
|
${modalDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
${modalSubmit}
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
>
|
>
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
<use xlink:href="/public/img/sprite.svg#close"></use>
|
||||||
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
</svg>
|
||||||
</a>`
|
</button>
|
||||||
|
</div>
|
||||||
const modalSubmit = `
|
|
||||||
<button type="button"
|
|
||||||
class="usa-button usa-button--secondary usa-modal__submit"
|
|
||||||
data-pk = ${request.id}
|
|
||||||
name="delete-domain-request">Yes, delete request</button>
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const modal = document.createElement('div');
|
this.tableWrapper.appendChild(modal);
|
||||||
modal.setAttribute('class', 'usa-modal');
|
|
||||||
modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`);
|
|
||||||
modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?');
|
|
||||||
modal.setAttribute('aria-describedby', 'Domain will be removed');
|
|
||||||
modal.setAttribute('data-force-action', '');
|
|
||||||
|
|
||||||
modal.innerHTML = `
|
// 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
|
||||||
<div class="usa-modal__content">
|
if (this.portfolioValue) {
|
||||||
<div class="usa-modal__main">
|
modalTrigger = `
|
||||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
<a
|
||||||
${modalHeading}
|
role="button"
|
||||||
</h2>
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
<div class="usa-prose">
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
<p id="modal-1-description">
|
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 visible-mobile-flex line-height-sans-5"
|
||||||
${modalDescription}
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
</p>
|
data-open-modal
|
||||||
</div>
|
>
|
||||||
<div class="usa-modal__footer">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<ul class="usa-button-group">
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
<li class="usa-button-group__item">
|
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||||
${modalSubmit}
|
</a>
|
||||||
</li>
|
|
||||||
<li class="usa-button-group__item">
|
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
||||||
<button
|
<div class="usa-accordion__heading">
|
||||||
type="button"
|
|
||||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
|
||||||
data-close-modal
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button usa-modal__close"
|
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
||||||
aria-label="Close this window"
|
aria-expanded="false"
|
||||||
data-close-modal
|
aria-controls="more-actions-${request.id}"
|
||||||
>
|
>
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="/public/img/sprite.svg#close"></use>
|
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
<div id="more-actions-${request.id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0" hidden>
|
||||||
|
<h2>More options</h2>
|
||||||
this.tableWrapper.appendChild(modal);
|
<a
|
||||||
|
role="button"
|
||||||
// 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
|
id="button-toggle-delete-domain-alert-${request.id}"
|
||||||
if (this.portfolioValue) {
|
href="#toggle-delete-domain-alert-${request.id}"
|
||||||
modalTrigger = `
|
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5"
|
||||||
<a
|
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||||
role="button"
|
data-open-modal
|
||||||
id="button-toggle-delete-domain-alert-${request.id}"
|
>
|
||||||
href="#toggle-delete-domain-alert-${request.id}"
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 visible-mobile-flex line-height-sans-5"
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||||
data-open-modal
|
</a>
|
||||||
>
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
|
||||||
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
|
||||||
<div class="usa-accordion__heading">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-controls="more-actions-${request.id}"
|
|
||||||
>
|
|
||||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="more-actions-${request.id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0" hidden>
|
|
||||||
<h2>More options</h2>
|
|
||||||
<a
|
|
||||||
role="button"
|
|
||||||
id="button-toggle-delete-domain-alert-${request.id}"
|
|
||||||
href="#toggle-delete-domain-alert-${request.id}"
|
|
||||||
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5"
|
|
||||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
|
||||||
data-open-modal
|
|
||||||
>
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
|
||||||
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
</div>
|
||||||
}
|
`
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<th scope="row" role="rowheader" data-label="Domain name">
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
${domainName}
|
${domainName}
|
||||||
</th>
|
</th>
|
||||||
<td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
|
<td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
|
||||||
${submissionDate}
|
${submissionDate}
|
||||||
</td>
|
</td>
|
||||||
${markupCreatorRow}
|
${markupCreatorRow}
|
||||||
<td data-label="Status">
|
<td data-label="Status">
|
||||||
${request.status}
|
${request.status}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="${actionUrl}">
|
<a href="${actionUrl}">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="/public/img/sprite.svg#${request.svg_icon}"></use>
|
<use xlink:href="/public/img/sprite.svg#${request.svg_icon}"></use>
|
||||||
</svg>
|
</svg>
|
||||||
${actionLabel} <span class="usa-sr-only">${request.requested_domain ? request.requested_domain : 'New domain request'}</span>
|
${actionLabel} <span class="usa-sr-only">${request.requested_domain ? request.requested_domain : 'New domain request'}</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
${needsDeleteColumn ? '<td>'+modalTrigger+'</td>' : ''}
|
${needsDeleteColumn ? '<td>'+modalTrigger+'</td>' : ''}
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize modals immediately after the DOM content is updated
|
||||||
|
initializeModals();
|
||||||
|
|
||||||
|
// Now the DOM and modals are ready, add listeners to the submit buttons
|
||||||
|
const modals = document.querySelectorAll('.usa-modal__content');
|
||||||
|
|
||||||
|
modals.forEach(modal => {
|
||||||
|
const submitButton = modal.querySelector('.usa-modal__submit');
|
||||||
|
const closeButton = modal.querySelector('.usa-modal__close');
|
||||||
|
submitButton.addEventListener('click', () => {
|
||||||
|
let pk = submitButton.getAttribute('data-pk');
|
||||||
|
// Close the modal to remove the USWDS UI local classes
|
||||||
|
closeButton.click();
|
||||||
|
// If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page
|
||||||
|
let pageToDisplay = data.page;
|
||||||
|
if (data.total == 1 && data.unfiltered_total > 1) {
|
||||||
|
pageToDisplay--;
|
||||||
|
}
|
||||||
|
this.deleteDomainRequest(pk, pageToDisplay);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// initialize modals immediately after the DOM content is updated
|
// Do not scroll on first page load
|
||||||
initializeModals();
|
if (scroll)
|
||||||
|
scrollToElement('class', 'domain-requests');
|
||||||
|
this.scrollToTable = true;
|
||||||
|
|
||||||
// Now the DOM and modals are ready, add listeners to the submit buttons
|
// update the pagination after the domain requests list is updated
|
||||||
const modals = document.querySelectorAll('.usa-modal__content');
|
this.updatePagination(
|
||||||
|
'domain request',
|
||||||
modals.forEach(modal => {
|
'#domain-requests-pagination',
|
||||||
const submitButton = modal.querySelector('.usa-modal__submit');
|
'#domain-requests-pagination .usa-pagination__counter',
|
||||||
const closeButton = modal.querySelector('.usa-modal__close');
|
'#domain-requests',
|
||||||
submitButton.addEventListener('click', () => {
|
data.page,
|
||||||
let pk = submitButton.getAttribute('data-pk');
|
data.num_pages,
|
||||||
// Close the modal to remove the USWDS UI local classes
|
data.has_previous,
|
||||||
closeButton.click();
|
data.has_next,
|
||||||
// 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
|
data.total,
|
||||||
let pageToDisplay = data.page;
|
);
|
||||||
if (data.total == 1 && data.unfiltered_total > 1) {
|
this.currentSortBy = sortBy;
|
||||||
pageToDisplay--;
|
this.currentOrder = order;
|
||||||
}
|
this.currentSearchTerm = searchTerm;
|
||||||
this.deleteDomainRequest(pk, pageToDisplay);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Do not scroll on first page load
|
|
||||||
if (scroll)
|
|
||||||
scrollToElement('class', 'domain-requests');
|
|
||||||
this.scrollToTable = true;
|
|
||||||
|
|
||||||
// update the pagination after the domain requests list is updated
|
|
||||||
this.updatePagination(
|
|
||||||
'domain request',
|
|
||||||
'#domain-requests-pagination',
|
|
||||||
'#domain-requests-pagination .usa-pagination__counter',
|
|
||||||
'#domain-requests',
|
|
||||||
data.page,
|
|
||||||
data.num_pages,
|
|
||||||
data.has_previous,
|
|
||||||
data.has_next,
|
|
||||||
data.total,
|
|
||||||
);
|
|
||||||
this.currentSortBy = sortBy;
|
|
||||||
this.currentOrder = order;
|
|
||||||
this.currentSearchTerm = searchTerm;
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error fetching domain requests:', error));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
|
|
||||||
* @param {*} domainRequestPk - the identifier for the request that we're deleting
|
|
||||||
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
|
|
||||||
*/
|
|
||||||
deleteDomainRequest(domainRequestPk, pageToDisplay) {
|
|
||||||
// Use to debug uswds modal issues
|
|
||||||
//console.log('deleteDomainRequest')
|
|
||||||
|
|
||||||
// Get csrf token
|
|
||||||
const csrfToken = getCsrfToken();
|
|
||||||
// Create FormData object and append the CSRF token
|
|
||||||
const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`;
|
|
||||||
|
|
||||||
fetch(`/domain-request/${domainRequestPk}/delete`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
// Update data and UI
|
|
||||||
this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm);
|
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching domain requests:', error));
|
.catch(error => console.error('Error fetching domain requests:', error));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
* 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.
|
||||||
* initializes the domain requests list and associated functionality.
|
* @param {*} domainRequestPk - the identifier for the request that we're deleting
|
||||||
*
|
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
|
||||||
*/
|
*/
|
||||||
|
deleteDomainRequest(domainRequestPk, pageToDisplay) {
|
||||||
|
// Use to debug uswds modal issues
|
||||||
|
//console.log('deleteDomainRequest')
|
||||||
|
|
||||||
|
// Get csrf token
|
||||||
|
const csrfToken = getCsrfToken();
|
||||||
|
// Create FormData object and append the CSRF token
|
||||||
|
const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`;
|
||||||
|
|
||||||
|
fetch(`/domain-request/${domainRequestPk}/delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
// Update data and UI
|
||||||
|
this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching domain requests:', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function initDomainRequestsTable() {
|
export function initDomainRequestsTable() {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const domainRequestsSectionWrapper = document.getElementById('domain-requests');
|
const domainRequestsSectionWrapper = document.getElementById('domain-requests');
|
||||||
|
|
|
@ -1,152 +1,146 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
import { scrollToElement } from './helpers.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
import { initializeTooltips } from './helpers-uswds.js';
|
||||||
|
|
||||||
import { LoadTableBase } from './table-base.js';
|
import { LoadTableBase } from './table-base.js';
|
||||||
|
|
||||||
export class DomainsTable extends LoadTableBase {
|
export class DomainsTable extends LoadTableBase {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('domains');
|
super('domains');
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
|
||||||
* based on the supplied attributes.
|
|
||||||
* @param {*} page - the page number of the results (starts with 1)
|
|
||||||
* @param {*} sortBy - the sort column option
|
|
||||||
* @param {*} order - the sort order {asc, desc}
|
|
||||||
* @param {*} scroll - control for the scrollToElement functionality
|
|
||||||
* @param {*} status - control for the status filter
|
|
||||||
* @param {*} searchTerm - the search term
|
|
||||||
* @param {*} portfolio - the portfolio id
|
|
||||||
*/
|
|
||||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
|
||||||
|
|
||||||
// fetch json of page of domais, given params
|
|
||||||
let baseUrl = document.getElementById("get_domains_json_url");
|
|
||||||
if (!baseUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let baseUrlValue = baseUrl.innerHTML;
|
|
||||||
if (!baseUrlValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch json of page of domains, given params
|
|
||||||
let searchParams = new URLSearchParams(
|
|
||||||
{
|
|
||||||
"page": page,
|
|
||||||
"sort_by": sortBy,
|
|
||||||
"order": order,
|
|
||||||
"status": status,
|
|
||||||
"search_term": searchTerm
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (portfolio)
|
|
||||||
searchParams.append("portfolio", portfolio)
|
|
||||||
|
|
||||||
let url = `${baseUrlValue}?${searchParams.toString()}`
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
console.error('Error in AJAX call: ' + data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results
|
|
||||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
|
||||||
|
|
||||||
// identify the DOM element where the domain list will be inserted into the DOM
|
|
||||||
const domainList = document.querySelector('#domains tbody');
|
|
||||||
domainList.innerHTML = '';
|
|
||||||
|
|
||||||
data.domains.forEach(domain => {
|
|
||||||
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
|
||||||
const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null;
|
|
||||||
const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : '';
|
|
||||||
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
|
||||||
const actionUrl = domain.action_url;
|
|
||||||
const suborganization = domain.domain_info__sub_organization ? domain.domain_info__sub_organization : '⎯';
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
let markupForSuborganizationRow = '';
|
|
||||||
|
|
||||||
if (this.portfolioValue) {
|
|
||||||
markupForSuborganizationRow = `
|
|
||||||
<td>
|
|
||||||
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
|
||||||
</td>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<th scope="row" role="rowheader" data-label="Domain name">
|
|
||||||
${domain.name}
|
|
||||||
</th>
|
|
||||||
<td data-sort-value="${expirationDateSortValue}" data-label="Expires">
|
|
||||||
${expirationDateFormatted}
|
|
||||||
</td>
|
|
||||||
<td data-label="Status">
|
|
||||||
${domain.state_display}
|
|
||||||
<svg
|
|
||||||
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
|
|
||||||
data-position="top"
|
|
||||||
title="${domain.get_state_help_text}"
|
|
||||||
focusable="true"
|
|
||||||
aria-label="${domain.get_state_help_text}"
|
|
||||||
role="tooltip"
|
|
||||||
>
|
|
||||||
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
|
||||||
</svg>
|
|
||||||
</td>
|
|
||||||
${markupForSuborganizationRow}
|
|
||||||
<td>
|
|
||||||
<a href="${actionUrl}">
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#${domain.svg_icon}"></use>
|
|
||||||
</svg>
|
|
||||||
${domain.action_label} <span class="usa-sr-only">${domain.name}</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
domainList.appendChild(row);
|
|
||||||
});
|
|
||||||
// initialize tool tips immediately after the associated DOM elements are added
|
|
||||||
initializeTooltips();
|
|
||||||
|
|
||||||
// Do not scroll on first page load
|
|
||||||
if (scroll)
|
|
||||||
scrollToElement('class', 'domains');
|
|
||||||
this.scrollToTable = true;
|
|
||||||
|
|
||||||
// update pagination
|
|
||||||
this.updatePagination(
|
|
||||||
'domain',
|
|
||||||
'#domains-pagination',
|
|
||||||
'#domains-pagination .usa-pagination__counter',
|
|
||||||
'#domains',
|
|
||||||
data.page,
|
|
||||||
data.num_pages,
|
|
||||||
data.has_previous,
|
|
||||||
data.has_next,
|
|
||||||
data.total,
|
|
||||||
);
|
|
||||||
this.currentSortBy = sortBy;
|
|
||||||
this.currentOrder = order;
|
|
||||||
this.currentSearchTerm = searchTerm;
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error fetching domains:', error));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 domains list and associated functionality.
|
* based on the supplied attributes.
|
||||||
*
|
* @param {*} page - the page number of the results (starts with 1)
|
||||||
*/
|
* @param {*} sortBy - the sort column option
|
||||||
|
* @param {*} order - the sort order {asc, desc}
|
||||||
|
* @param {*} scroll - control for the scrollToElement functionality
|
||||||
|
* @param {*} status - control for the status filter
|
||||||
|
* @param {*} searchTerm - the search term
|
||||||
|
* @param {*} portfolio - the portfolio id
|
||||||
|
*/
|
||||||
|
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||||
|
|
||||||
|
// fetch json of page of domais, given params
|
||||||
|
let baseUrl = document.getElementById("get_domains_json_url");
|
||||||
|
if (!baseUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseUrlValue = baseUrl.innerHTML;
|
||||||
|
if (!baseUrlValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch json of page of domains, given params
|
||||||
|
let searchParams = new URLSearchParams(
|
||||||
|
{
|
||||||
|
"page": page,
|
||||||
|
"sort_by": sortBy,
|
||||||
|
"order": order,
|
||||||
|
"status": status,
|
||||||
|
"search_term": searchTerm
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (portfolio)
|
||||||
|
searchParams.append("portfolio", portfolio)
|
||||||
|
|
||||||
|
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error in AJAX call: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the display of proper messaging in the event that no domains exist in the list or search returns no results
|
||||||
|
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||||
|
|
||||||
|
// identify the DOM element where the domain list will be inserted into the DOM
|
||||||
|
const domainList = document.querySelector('#domains tbody');
|
||||||
|
domainList.innerHTML = '';
|
||||||
|
|
||||||
|
data.domains.forEach(domain => {
|
||||||
|
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
||||||
|
const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null;
|
||||||
|
const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : '';
|
||||||
|
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
|
||||||
|
const actionUrl = domain.action_url;
|
||||||
|
const suborganization = domain.domain_info__sub_organization ? domain.domain_info__sub_organization : '⎯';
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
let markupForSuborganizationRow = '';
|
||||||
|
|
||||||
|
if (this.portfolioValue) {
|
||||||
|
markupForSuborganizationRow = `
|
||||||
|
<td>
|
||||||
|
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
||||||
|
</td>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
|
${domain.name}
|
||||||
|
</th>
|
||||||
|
<td data-sort-value="${expirationDateSortValue}" data-label="Expires">
|
||||||
|
${expirationDateFormatted}
|
||||||
|
</td>
|
||||||
|
<td data-label="Status">
|
||||||
|
${domain.state_display}
|
||||||
|
<svg
|
||||||
|
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
|
||||||
|
data-position="top"
|
||||||
|
title="${domain.get_state_help_text}"
|
||||||
|
focusable="true"
|
||||||
|
aria-label="${domain.get_state_help_text}"
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
${markupForSuborganizationRow}
|
||||||
|
<td>
|
||||||
|
<a href="${actionUrl}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#${domain.svg_icon}"></use>
|
||||||
|
</svg>
|
||||||
|
${domain.action_label} <span class="usa-sr-only">${domain.name}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
domainList.appendChild(row);
|
||||||
|
});
|
||||||
|
// initialize tool tips immediately after the associated DOM elements are added
|
||||||
|
initializeTooltips();
|
||||||
|
|
||||||
|
// Do not scroll on first page load
|
||||||
|
if (scroll)
|
||||||
|
scrollToElement('class', 'domains');
|
||||||
|
this.scrollToTable = true;
|
||||||
|
|
||||||
|
// update pagination
|
||||||
|
this.updatePagination(
|
||||||
|
'domain',
|
||||||
|
'#domains-pagination',
|
||||||
|
'#domains-pagination .usa-pagination__counter',
|
||||||
|
'#domains',
|
||||||
|
data.page,
|
||||||
|
data.num_pages,
|
||||||
|
data.has_previous,
|
||||||
|
data.has_next,
|
||||||
|
data.total,
|
||||||
|
);
|
||||||
|
this.currentSortBy = sortBy;
|
||||||
|
this.currentOrder = order;
|
||||||
|
this.currentSearchTerm = searchTerm;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching domains:', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function initDomainsTable() {
|
export function initDomainsTable() {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const isDomainsPage = document.getElementById("domains")
|
const isDomainsPage = document.getElementById("domains")
|
||||||
|
@ -158,7 +152,4 @@ export function initDomainsTable() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,126 +1,125 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
||||||
import { getCsrfToken } from './get-csrf-token.js';
|
import { getCsrfToken } from './helpers-csrf-token.js';
|
||||||
|
|
||||||
import { LoadTableBase } from './table-base.js';
|
import { LoadTableBase } from './table-base.js';
|
||||||
|
|
||||||
export class MemberDomainsTable extends LoadTableBase {
|
export class MemberDomainsTable extends LoadTableBase {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('member-domains');
|
super('member-domains');
|
||||||
this.currentSortBy = 'name';
|
this.currentSortBy = 'name';
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Loads rows in the members list, as well as updates pagination around the members list
|
* Loads rows in the members list, as well as updates pagination around the members 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 {*} searchTerm - the search term
|
* @param {*} searchTerm - the search term
|
||||||
* @param {*} portfolio - the portfolio id
|
* @param {*} portfolio - the portfolio id
|
||||||
*/
|
*/
|
||||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||||
|
|
||||||
// --------- SEARCH
|
// --------- SEARCH
|
||||||
let searchParams = new URLSearchParams(
|
let searchParams = new URLSearchParams(
|
||||||
{
|
{
|
||||||
"page": page,
|
"page": page,
|
||||||
"sort_by": sortBy,
|
"sort_by": sortBy,
|
||||||
"order": order,
|
"order": order,
|
||||||
"search_term": searchTerm,
|
"search_term": searchTerm,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null;
|
||||||
|
let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null;
|
||||||
|
let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null;
|
||||||
|
|
||||||
|
if (portfolio)
|
||||||
|
searchParams.append("portfolio", portfolio)
|
||||||
|
if (emailValue)
|
||||||
|
searchParams.append("email", emailValue)
|
||||||
|
if (memberIdValue)
|
||||||
|
searchParams.append("member_id", memberIdValue)
|
||||||
|
if (memberOnly)
|
||||||
|
searchParams.append("member_only", memberOnly)
|
||||||
|
|
||||||
|
|
||||||
|
// --------- FETCH DATA
|
||||||
|
// fetch json of page of domais, given params
|
||||||
|
let baseUrl = document.getElementById("get_member_domains_json_url");
|
||||||
|
if (!baseUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseUrlValue = baseUrl.innerHTML;
|
||||||
|
if (!baseUrlValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error in AJAX call: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
|
||||||
|
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||||
|
|
||||||
|
// identify the DOM element where the domain list will be inserted into the DOM
|
||||||
|
const memberDomainsList = document.querySelector('#member-domains tbody');
|
||||||
|
memberDomainsList.innerHTML = '';
|
||||||
|
|
||||||
|
|
||||||
|
data.domains.forEach(domain => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td scope="row" data-label="Domain name">
|
||||||
|
${domain.name}
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
memberDomainsList.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not scroll on first page load
|
||||||
|
if (scroll)
|
||||||
|
scrollToElement('class', 'member-domains');
|
||||||
|
this.scrollToTable = true;
|
||||||
|
|
||||||
|
// update pagination
|
||||||
|
this.updatePagination(
|
||||||
|
'member domain',
|
||||||
|
'#member-domains-pagination',
|
||||||
|
'#member-domains-pagination .usa-pagination__counter',
|
||||||
|
'#member-domains',
|
||||||
|
data.page,
|
||||||
|
data.num_pages,
|
||||||
|
data.has_previous,
|
||||||
|
data.has_next,
|
||||||
|
data.total,
|
||||||
);
|
);
|
||||||
|
this.currentSortBy = sortBy;
|
||||||
let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null;
|
this.currentOrder = order;
|
||||||
let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null;
|
this.currentSearchTerm = searchTerm;
|
||||||
let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null;
|
})
|
||||||
|
.catch(error => console.error('Error fetching domains:', error));
|
||||||
if (portfolio)
|
|
||||||
searchParams.append("portfolio", portfolio)
|
|
||||||
if (emailValue)
|
|
||||||
searchParams.append("email", emailValue)
|
|
||||||
if (memberIdValue)
|
|
||||||
searchParams.append("member_id", memberIdValue)
|
|
||||||
if (memberOnly)
|
|
||||||
searchParams.append("member_only", memberOnly)
|
|
||||||
|
|
||||||
|
|
||||||
// --------- FETCH DATA
|
|
||||||
// fetch json of page of domais, given params
|
|
||||||
let baseUrl = document.getElementById("get_member_domains_json_url");
|
|
||||||
if (!baseUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let baseUrlValue = baseUrl.innerHTML;
|
|
||||||
if (!baseUrlValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
console.error('Error in AJAX call: ' + data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
|
|
||||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
|
||||||
|
|
||||||
// identify the DOM element where the domain list will be inserted into the DOM
|
|
||||||
const memberDomainsList = document.querySelector('#member-domains tbody');
|
|
||||||
memberDomainsList.innerHTML = '';
|
|
||||||
|
|
||||||
|
|
||||||
data.domains.forEach(domain => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td scope="row" data-label="Domain name">
|
|
||||||
${domain.name}
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
memberDomainsList.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Do not scroll on first page load
|
|
||||||
if (scroll)
|
|
||||||
scrollToElement('class', 'member-domains');
|
|
||||||
this.scrollToTable = true;
|
|
||||||
|
|
||||||
// update pagination
|
|
||||||
this.updatePagination(
|
|
||||||
'member domain',
|
|
||||||
'#member-domains-pagination',
|
|
||||||
'#member-domains-pagination .usa-pagination__counter',
|
|
||||||
'#member-domains',
|
|
||||||
data.page,
|
|
||||||
data.num_pages,
|
|
||||||
data.has_previous,
|
|
||||||
data.has_next,
|
|
||||||
data.total,
|
|
||||||
);
|
|
||||||
this.currentSortBy = sortBy;
|
|
||||||
this.currentOrder = order;
|
|
||||||
this.currentSearchTerm = searchTerm;
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error fetching domains:', error));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initMemberDomainsTable() {
|
||||||
export function initMemberDomainsTable() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
const isMemberDomainsPage = document.getElementById("member-domains")
|
||||||
const isMemberDomainsPage = document.getElementById("member-domains")
|
if (isMemberDomainsPage){
|
||||||
if (isMemberDomainsPage){
|
const memberDomainsTable = new MemberDomainsTable();
|
||||||
const memberDomainsTable = new MemberDomainsTable();
|
if (memberDomainsTable.tableWrapper) {
|
||||||
if (memberDomainsTable.tableWrapper) {
|
// Initial load
|
||||||
// Initial load
|
memberDomainsTable.loadTable(1);
|
||||||
memberDomainsTable.loadTable(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,367 +1,358 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
import { hideElement, showElement, scrollToElement } from './helpers.js';
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
|
||||||
import { getCsrfToken } from './get-csrf-token.js';
|
|
||||||
|
|
||||||
import { LoadTableBase } from './table-base.js';
|
import { LoadTableBase } from './table-base.js';
|
||||||
|
|
||||||
export class MembersTable extends LoadTableBase {
|
export class MembersTable extends LoadTableBase {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('members');
|
super('members');
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes "Show More" buttons on the page, enabling toggle functionality to show or hide content.
|
|
||||||
*
|
|
||||||
* The function finds elements with "Show More" buttons and sets up a click event listener to toggle the visibility
|
|
||||||
* of a corresponding content div. When clicked, the button updates its visual state (e.g., text/icon change),
|
|
||||||
* and the associated content is shown or hidden based on its current visibility status.
|
|
||||||
*
|
|
||||||
* @function initShowMoreButtons
|
|
||||||
*/
|
|
||||||
initShowMoreButtons() {
|
|
||||||
/**
|
|
||||||
* Toggles the visibility of a content section when the "Show More" button is clicked.
|
|
||||||
* Updates the button text/icon based on whether the content is shown or hidden.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} toggleButton - The button that toggles the content visibility.
|
|
||||||
* @param {HTMLElement} contentDiv - The content div whose visibility is toggled.
|
|
||||||
* @param {HTMLElement} buttonParentRow - The parent row element containing the button.
|
|
||||||
*/
|
|
||||||
function toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow) {
|
|
||||||
const spanElement = toggleButton.querySelector('span');
|
|
||||||
const useElement = toggleButton.querySelector('use');
|
|
||||||
if (contentDiv.classList.contains('display-none')) {
|
|
||||||
showElement(contentDiv);
|
|
||||||
spanElement.textContent = 'Close';
|
|
||||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
|
||||||
buttonParentRow.classList.add('hide-td-borders');
|
|
||||||
toggleButton.setAttribute('aria-label', 'Close additional information');
|
|
||||||
} else {
|
|
||||||
hideElement(contentDiv);
|
|
||||||
spanElement.textContent = 'Expand';
|
|
||||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
|
||||||
buttonParentRow.classList.remove('hide-td-borders');
|
|
||||||
toggleButton.setAttribute('aria-label', 'Expand for additional information');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let toggleButtons = document.querySelectorAll('.usa-button--show-more-button');
|
|
||||||
toggleButtons.forEach((toggleButton) => {
|
|
||||||
|
|
||||||
// get contentDiv for element specified in data-for attribute of toggleButton
|
|
||||||
let dataFor = toggleButton.dataset.for;
|
|
||||||
let contentDiv = document.getElementById(dataFor);
|
|
||||||
let buttonParentRow = toggleButton.parentElement.parentElement;
|
|
||||||
if (contentDiv && contentDiv.tagName.toLowerCase() === 'tr' && contentDiv.classList.contains('show-more-content') && buttonParentRow && buttonParentRow.tagName.toLowerCase() === 'tr') {
|
|
||||||
toggleButton.addEventListener('click', function() {
|
|
||||||
toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn('Found a toggle button with no associated toggleable content or parent row');
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a given `last_active` value into a display value and a numeric sort value.
|
|
||||||
* The input can be a UTC date, the strings "Invited", "Invalid date", or null/undefined.
|
|
||||||
*
|
|
||||||
* @param {string} last_active - UTC date string or special status like "Invited" or "Invalid date".
|
|
||||||
* @returns {Object} - An object containing `display_value` (formatted date or status string)
|
|
||||||
* and `sort_value` (numeric value for sorting).
|
|
||||||
*/
|
|
||||||
handleLastActive(last_active) {
|
|
||||||
const invited = 'Invited';
|
|
||||||
const invalid_date = 'Invalid date';
|
|
||||||
const options = { year: 'numeric', month: 'long', day: 'numeric' }; // Date display format
|
|
||||||
|
|
||||||
let display_value = invalid_date; // Default display value for invalid or null dates
|
|
||||||
let sort_value = -1; // Default sort value for invalid or null dates
|
|
||||||
|
|
||||||
if (last_active === invited) {
|
|
||||||
// Handle "Invited" status: special case with 0 sort value
|
|
||||||
display_value = invited;
|
|
||||||
sort_value = 0;
|
|
||||||
} else if (last_active && last_active !== invalid_date) {
|
|
||||||
// Parse and format valid UTC date strings
|
|
||||||
const parsedDate = new Date(last_active);
|
|
||||||
|
|
||||||
if (!isNaN(parsedDate.getTime())) {
|
|
||||||
// Valid date
|
|
||||||
display_value = parsedDate.toLocaleDateString('en-US', options);
|
|
||||||
sort_value = parsedDate.getTime(); // Use timestamp for sorting
|
|
||||||
} else {
|
|
||||||
console.error(`Error: Invalid date string provided: ${last_active}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { display_value, sort_value };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates HTML for the list of domains assigned to a member.
|
|
||||||
*
|
|
||||||
* @param {number} num_domains - The number of domains the member is assigned to.
|
|
||||||
* @param {Array} domain_names - An array of domain names.
|
|
||||||
* @param {Array} domain_urls - An array of corresponding domain URLs.
|
|
||||||
* @returns {string} - A string of HTML displaying the domains assigned to the member.
|
|
||||||
*/
|
|
||||||
generateDomainsHTML(num_domains, domain_names, domain_urls, action_url) {
|
|
||||||
// Initialize an empty string for the HTML
|
|
||||||
let domainsHTML = '';
|
|
||||||
|
|
||||||
// Only generate HTML if the member has one or more assigned domains
|
|
||||||
if (num_domains > 0) {
|
|
||||||
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
|
||||||
domainsHTML += "<h4 class='margin-y-0 text-primary'>Domains assigned</h4>";
|
|
||||||
domainsHTML += `<p class='margin-y-0'>This member is assigned to ${num_domains} domains:</p>`;
|
|
||||||
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
|
||||||
|
|
||||||
// Display up to 6 domains with their URLs
|
|
||||||
for (let i = 0; i < num_domains && i < 6; i++) {
|
|
||||||
domainsHTML += `<li><a href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
domainsHTML += "</ul>";
|
|
||||||
|
|
||||||
// If there are more than 6 domains, display a "View assigned domains" link
|
|
||||||
if (num_domains >= 6) {
|
|
||||||
domainsHTML += `<p><a href="${action_url}/domains">View assigned domains</a></p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
domainsHTML += "</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return domainsHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates an HTML string summarizing a user's additional permissions within a portfolio,
|
|
||||||
* based on the user's permissions and predefined permission choices.
|
|
||||||
*
|
|
||||||
* @param {Array} member_permissions - An array of permission strings that the member has.
|
|
||||||
* @param {Object} UserPortfolioPermissionChoices - An object containing predefined permission choice constants.
|
|
||||||
* Expected keys include:
|
|
||||||
* - VIEW_ALL_DOMAINS
|
|
||||||
* - VIEW_MANAGED_DOMAINS
|
|
||||||
* - EDIT_REQUESTS
|
|
||||||
* - VIEW_ALL_REQUESTS
|
|
||||||
* - EDIT_MEMBERS
|
|
||||||
* - VIEW_MEMBERS
|
|
||||||
*
|
|
||||||
* @returns {string} - A string of HTML representing the user's additional permissions.
|
|
||||||
* If the user has no specific permissions, it returns a default message
|
|
||||||
* indicating no additional permissions.
|
|
||||||
*
|
|
||||||
* Behavior:
|
|
||||||
* - The function checks the user's permissions (`member_permissions`) and generates
|
|
||||||
* corresponding HTML sections based on the permission choices defined in `UserPortfolioPermissionChoices`.
|
|
||||||
* - Permissions are categorized into domains, requests, and members:
|
|
||||||
* - Domains: Determines whether the user can view or manage all or assigned domains.
|
|
||||||
* - Requests: Differentiates between users who can edit requests, view all requests, or have no request privileges.
|
|
||||||
* - Members: Distinguishes between members who can manage or only view other members.
|
|
||||||
* - If no relevant permissions are found, the function returns a message stating that the user has no additional permissions.
|
|
||||||
* - The resulting HTML always includes a header "Additional permissions for this member" and appends the relevant permission descriptions.
|
|
||||||
*/
|
|
||||||
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
|
||||||
let permissionsHTML = '';
|
|
||||||
|
|
||||||
// Check domain-related permissions
|
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check request-related permissions
|
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests:</strong> Can view all organization domain requests. Can create domain requests and modify their own requests.</p>";
|
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests (view-only):</strong> Can view all organization domain requests. Can't create or modify any domain requests.</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check member-related permissions
|
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.</p>";
|
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no specific permissions are assigned, display a message indicating no additional permissions
|
|
||||||
if (!permissionsHTML) {
|
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><b>No additional permissions:</b> There are no additional permissions for this member.</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a permissions header and wrap the entire output in a container
|
|
||||||
permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0 text-primary'>Additional permissions for this member</h4>" + permissionsHTML + "</div>";
|
|
||||||
|
|
||||||
return permissionsHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads rows in the members list, as well as updates pagination around the members list
|
|
||||||
* based on the supplied attributes.
|
|
||||||
* @param {*} page - the page number of the results (starts with 1)
|
|
||||||
* @param {*} sortBy - the sort column option
|
|
||||||
* @param {*} order - the sort order {asc, desc}
|
|
||||||
* @param {*} scroll - control for the scrollToElement functionality
|
|
||||||
* @param {*} searchTerm - the search term
|
|
||||||
* @param {*} portfolio - the portfolio id
|
|
||||||
*/
|
|
||||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
|
||||||
|
|
||||||
// --------- SEARCH
|
|
||||||
let searchParams = new URLSearchParams(
|
|
||||||
{
|
|
||||||
"page": page,
|
|
||||||
"sort_by": sortBy,
|
|
||||||
"order": order,
|
|
||||||
"search_term": searchTerm
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (portfolio)
|
|
||||||
searchParams.append("portfolio", portfolio)
|
|
||||||
|
|
||||||
|
|
||||||
// --------- FETCH DATA
|
|
||||||
// fetch json of page of domains, given params
|
|
||||||
let baseUrl = document.getElementById("get_members_json_url");
|
|
||||||
if (!baseUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let baseUrlValue = baseUrl.innerHTML;
|
|
||||||
if (!baseUrlValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
console.error('Error in AJAX call: ' + data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
|
|
||||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
|
||||||
|
|
||||||
// identify the DOM element where the domain list will be inserted into the DOM
|
|
||||||
const memberList = document.querySelector('#members tbody');
|
|
||||||
memberList.innerHTML = '';
|
|
||||||
|
|
||||||
const UserPortfolioPermissionChoices = data.UserPortfolioPermissionChoices;
|
|
||||||
const invited = 'Invited';
|
|
||||||
const invalid_date = 'Invalid date';
|
|
||||||
|
|
||||||
data.members.forEach(member => {
|
|
||||||
const member_id = member.source + member.id;
|
|
||||||
const member_name = member.name;
|
|
||||||
const member_display = member.member_display;
|
|
||||||
const member_permissions = member.permissions;
|
|
||||||
const domain_urls = member.domain_urls;
|
|
||||||
const domain_names = member.domain_names;
|
|
||||||
const num_domains = domain_urls.length;
|
|
||||||
|
|
||||||
const last_active = this.handleLastActive(member.last_active);
|
|
||||||
|
|
||||||
const action_url = member.action_url;
|
|
||||||
const action_label = member.action_label;
|
|
||||||
const svg_icon = member.svg_icon;
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
let admin_tagHTML = ``;
|
|
||||||
if (member.is_admin)
|
|
||||||
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary">Admin</span>`
|
|
||||||
|
|
||||||
// generate html blocks for domains and permissions for the member
|
|
||||||
let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, action_url);
|
|
||||||
let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices);
|
|
||||||
|
|
||||||
// domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand
|
|
||||||
let showMoreButton = '';
|
|
||||||
const showMoreRow = document.createElement('tr');
|
|
||||||
if (domainsHTML || permissionsHTML) {
|
|
||||||
showMoreButton = `
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-1"
|
|
||||||
data-for=${member_id}
|
|
||||||
aria-label="Expand for additional information"
|
|
||||||
>
|
|
||||||
<span>Expand</span>
|
|
||||||
<svg class="usa-icon usa-icon--big" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
showMoreRow.innerHTML = `<td colspan='3' headers="header-member row-header-${member_id}" class="padding-top-0"><div class='grid-row'>${domainsHTML} ${permissionsHTML}</div></td>`;
|
|
||||||
showMoreRow.classList.add('show-more-content');
|
|
||||||
showMoreRow.classList.add('display-none');
|
|
||||||
showMoreRow.id = member_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<th role="rowheader" headers="header-member" data-label="member email" id='row-header-${member_id}'>
|
|
||||||
${member_display} ${admin_tagHTML} ${showMoreButton}
|
|
||||||
</th>
|
|
||||||
<td headers="header-last-active row-header-${member_id}" data-sort-value="${last_active.sort_value}" data-label="last_active">
|
|
||||||
${last_active.display_value}
|
|
||||||
</td>
|
|
||||||
<td headers="header-action row-header-${member_id}">
|
|
||||||
<a href="${action_url}">
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#${svg_icon}"></use>
|
|
||||||
</svg>
|
|
||||||
${action_label} <span class="usa-sr-only">${member_name}</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
`;
|
|
||||||
memberList.appendChild(row);
|
|
||||||
if (domainsHTML || permissionsHTML) {
|
|
||||||
memberList.appendChild(showMoreRow);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.initShowMoreButtons();
|
|
||||||
|
|
||||||
// Do not scroll on first page load
|
|
||||||
if (scroll)
|
|
||||||
scrollToElement('class', 'members');
|
|
||||||
this.scrollToTable = true;
|
|
||||||
|
|
||||||
// update pagination
|
|
||||||
this.updatePagination(
|
|
||||||
'member',
|
|
||||||
'#members-pagination',
|
|
||||||
'#members-pagination .usa-pagination__counter',
|
|
||||||
'#members',
|
|
||||||
data.page,
|
|
||||||
data.num_pages,
|
|
||||||
data.has_previous,
|
|
||||||
data.has_next,
|
|
||||||
data.total,
|
|
||||||
);
|
|
||||||
this.currentSortBy = sortBy;
|
|
||||||
this.currentOrder = order;
|
|
||||||
this.currentSearchTerm = searchTerm;
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error fetching members:', error));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes "Show More" buttons on the page, enabling toggle functionality to show or hide content.
|
||||||
|
*
|
||||||
|
* The function finds elements with "Show More" buttons and sets up a click event listener to toggle the visibility
|
||||||
|
* of a corresponding content div. When clicked, the button updates its visual state (e.g., text/icon change),
|
||||||
|
* and the associated content is shown or hidden based on its current visibility status.
|
||||||
|
*
|
||||||
|
* @function initShowMoreButtons
|
||||||
|
*/
|
||||||
|
initShowMoreButtons() {
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of a content section when the "Show More" button is clicked.
|
||||||
|
* Updates the button text/icon based on whether the content is shown or hidden.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} toggleButton - The button that toggles the content visibility.
|
||||||
|
* @param {HTMLElement} contentDiv - The content div whose visibility is toggled.
|
||||||
|
* @param {HTMLElement} buttonParentRow - The parent row element containing the button.
|
||||||
|
*/
|
||||||
|
function toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow) {
|
||||||
|
const spanElement = toggleButton.querySelector('span');
|
||||||
|
const useElement = toggleButton.querySelector('use');
|
||||||
|
if (contentDiv.classList.contains('display-none')) {
|
||||||
|
showElement(contentDiv);
|
||||||
|
spanElement.textContent = 'Close';
|
||||||
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
||||||
|
buttonParentRow.classList.add('hide-td-borders');
|
||||||
|
toggleButton.setAttribute('aria-label', 'Close additional information');
|
||||||
|
} else {
|
||||||
|
hideElement(contentDiv);
|
||||||
|
spanElement.textContent = 'Expand';
|
||||||
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
||||||
|
buttonParentRow.classList.remove('hide-td-borders');
|
||||||
|
toggleButton.setAttribute('aria-label', 'Expand for additional information');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toggleButtons = document.querySelectorAll('.usa-button--show-more-button');
|
||||||
|
toggleButtons.forEach((toggleButton) => {
|
||||||
|
|
||||||
|
// get contentDiv for element specified in data-for attribute of toggleButton
|
||||||
|
let dataFor = toggleButton.dataset.for;
|
||||||
|
let contentDiv = document.getElementById(dataFor);
|
||||||
|
let buttonParentRow = toggleButton.parentElement.parentElement;
|
||||||
|
if (contentDiv && contentDiv.tagName.toLowerCase() === 'tr' && contentDiv.classList.contains('show-more-content') && buttonParentRow && buttonParentRow.tagName.toLowerCase() === 'tr') {
|
||||||
|
toggleButton.addEventListener('click', function() {
|
||||||
|
toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Found a toggle button with no associated toggleable content or parent row');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
* Converts a given `last_active` value into a display value and a numeric sort value.
|
||||||
* initializes the members list and associated functionality.
|
* The input can be a UTC date, the strings "Invited", "Invalid date", or null/undefined.
|
||||||
*
|
*
|
||||||
*/
|
* @param {string} last_active - UTC date string or special status like "Invited" or "Invalid date".
|
||||||
|
* @returns {Object} - An object containing `display_value` (formatted date or status string)
|
||||||
|
* and `sort_value` (numeric value for sorting).
|
||||||
|
*/
|
||||||
|
handleLastActive(last_active) {
|
||||||
|
const invited = 'Invited';
|
||||||
|
const invalid_date = 'Invalid date';
|
||||||
|
const options = { year: 'numeric', month: 'long', day: 'numeric' }; // Date display format
|
||||||
|
|
||||||
|
let display_value = invalid_date; // Default display value for invalid or null dates
|
||||||
|
let sort_value = -1; // Default sort value for invalid or null dates
|
||||||
|
|
||||||
|
if (last_active === invited) {
|
||||||
|
// Handle "Invited" status: special case with 0 sort value
|
||||||
|
display_value = invited;
|
||||||
|
sort_value = 0;
|
||||||
|
} else if (last_active && last_active !== invalid_date) {
|
||||||
|
// Parse and format valid UTC date strings
|
||||||
|
const parsedDate = new Date(last_active);
|
||||||
|
|
||||||
|
if (!isNaN(parsedDate.getTime())) {
|
||||||
|
// Valid date
|
||||||
|
display_value = parsedDate.toLocaleDateString('en-US', options);
|
||||||
|
sort_value = parsedDate.getTime(); // Use timestamp for sorting
|
||||||
|
} else {
|
||||||
|
console.error(`Error: Invalid date string provided: ${last_active}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { display_value, sort_value };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates HTML for the list of domains assigned to a member.
|
||||||
|
*
|
||||||
|
* @param {number} num_domains - The number of domains the member is assigned to.
|
||||||
|
* @param {Array} domain_names - An array of domain names.
|
||||||
|
* @param {Array} domain_urls - An array of corresponding domain URLs.
|
||||||
|
* @returns {string} - A string of HTML displaying the domains assigned to the member.
|
||||||
|
*/
|
||||||
|
generateDomainsHTML(num_domains, domain_names, domain_urls, action_url) {
|
||||||
|
// Initialize an empty string for the HTML
|
||||||
|
let domainsHTML = '';
|
||||||
|
|
||||||
|
// Only generate HTML if the member has one or more assigned domains
|
||||||
|
if (num_domains > 0) {
|
||||||
|
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||||
|
domainsHTML += "<h4 class='margin-y-0 text-primary'>Domains assigned</h4>";
|
||||||
|
domainsHTML += `<p class='margin-y-0'>This member is assigned to ${num_domains} domains:</p>`;
|
||||||
|
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
||||||
|
|
||||||
|
// Display up to 6 domains with their URLs
|
||||||
|
for (let i = 0; i < num_domains && i < 6; i++) {
|
||||||
|
domainsHTML += `<li><a href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
domainsHTML += "</ul>";
|
||||||
|
|
||||||
|
// If there are more than 6 domains, display a "View assigned domains" link
|
||||||
|
if (num_domains >= 6) {
|
||||||
|
domainsHTML += `<p><a href="${action_url}/domains">View assigned domains</a></p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
domainsHTML += "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainsHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an HTML string summarizing a user's additional permissions within a portfolio,
|
||||||
|
* based on the user's permissions and predefined permission choices.
|
||||||
|
*
|
||||||
|
* @param {Array} member_permissions - An array of permission strings that the member has.
|
||||||
|
* @param {Object} UserPortfolioPermissionChoices - An object containing predefined permission choice constants.
|
||||||
|
* Expected keys include:
|
||||||
|
* - VIEW_ALL_DOMAINS
|
||||||
|
* - VIEW_MANAGED_DOMAINS
|
||||||
|
* - EDIT_REQUESTS
|
||||||
|
* - VIEW_ALL_REQUESTS
|
||||||
|
* - EDIT_MEMBERS
|
||||||
|
* - VIEW_MEMBERS
|
||||||
|
*
|
||||||
|
* @returns {string} - A string of HTML representing the user's additional permissions.
|
||||||
|
* If the user has no specific permissions, it returns a default message
|
||||||
|
* indicating no additional permissions.
|
||||||
|
*
|
||||||
|
* Behavior:
|
||||||
|
* - The function checks the user's permissions (`member_permissions`) and generates
|
||||||
|
* corresponding HTML sections based on the permission choices defined in `UserPortfolioPermissionChoices`.
|
||||||
|
* - Permissions are categorized into domains, requests, and members:
|
||||||
|
* - Domains: Determines whether the user can view or manage all or assigned domains.
|
||||||
|
* - Requests: Differentiates between users who can edit requests, view all requests, or have no request privileges.
|
||||||
|
* - Members: Distinguishes between members who can manage or only view other members.
|
||||||
|
* - If no relevant permissions are found, the function returns a message stating that the user has no additional permissions.
|
||||||
|
* - The resulting HTML always includes a header "Additional permissions for this member" and appends the relevant permission descriptions.
|
||||||
|
*/
|
||||||
|
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
||||||
|
let permissionsHTML = '';
|
||||||
|
|
||||||
|
// Check domain-related permissions
|
||||||
|
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
||||||
|
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
||||||
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
||||||
|
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check request-related permissions
|
||||||
|
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
||||||
|
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests:</strong> Can view all organization domain requests. Can create domain requests and modify their own requests.</p>";
|
||||||
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
||||||
|
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests (view-only):</strong> Can view all organization domain requests. Can't create or modify any domain requests.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check member-related permissions
|
||||||
|
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
||||||
|
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.</p>";
|
||||||
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
||||||
|
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific permissions are assigned, display a message indicating no additional permissions
|
||||||
|
if (!permissionsHTML) {
|
||||||
|
permissionsHTML += "<p class='margin-top-1 p--blockquote'><b>No additional permissions:</b> There are no additional permissions for this member.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a permissions header and wrap the entire output in a container
|
||||||
|
permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0 text-primary'>Additional permissions for this member</h4>" + permissionsHTML + "</div>";
|
||||||
|
|
||||||
|
return permissionsHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads rows in the members list, as well as updates pagination around the members list
|
||||||
|
* based on the supplied attributes.
|
||||||
|
* @param {*} page - the page number of the results (starts with 1)
|
||||||
|
* @param {*} sortBy - the sort column option
|
||||||
|
* @param {*} order - the sort order {asc, desc}
|
||||||
|
* @param {*} scroll - control for the scrollToElement functionality
|
||||||
|
* @param {*} searchTerm - the search term
|
||||||
|
* @param {*} portfolio - the portfolio id
|
||||||
|
*/
|
||||||
|
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||||
|
|
||||||
|
// --------- SEARCH
|
||||||
|
let searchParams = new URLSearchParams(
|
||||||
|
{
|
||||||
|
"page": page,
|
||||||
|
"sort_by": sortBy,
|
||||||
|
"order": order,
|
||||||
|
"search_term": searchTerm
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (portfolio)
|
||||||
|
searchParams.append("portfolio", portfolio)
|
||||||
|
|
||||||
|
|
||||||
|
// --------- FETCH DATA
|
||||||
|
// fetch json of page of domains, given params
|
||||||
|
let baseUrl = document.getElementById("get_members_json_url");
|
||||||
|
if (!baseUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseUrlValue = baseUrl.innerHTML;
|
||||||
|
if (!baseUrlValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error in AJAX call: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
|
||||||
|
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||||
|
|
||||||
|
// identify the DOM element where the domain list will be inserted into the DOM
|
||||||
|
const memberList = document.querySelector('#members tbody');
|
||||||
|
memberList.innerHTML = '';
|
||||||
|
|
||||||
|
const UserPortfolioPermissionChoices = data.UserPortfolioPermissionChoices;
|
||||||
|
const invited = 'Invited';
|
||||||
|
const invalid_date = 'Invalid date';
|
||||||
|
|
||||||
|
data.members.forEach(member => {
|
||||||
|
const member_id = member.source + member.id;
|
||||||
|
const member_name = member.name;
|
||||||
|
const member_display = member.member_display;
|
||||||
|
const member_permissions = member.permissions;
|
||||||
|
const domain_urls = member.domain_urls;
|
||||||
|
const domain_names = member.domain_names;
|
||||||
|
const num_domains = domain_urls.length;
|
||||||
|
|
||||||
|
const last_active = this.handleLastActive(member.last_active);
|
||||||
|
|
||||||
|
const action_url = member.action_url;
|
||||||
|
const action_label = member.action_label;
|
||||||
|
const svg_icon = member.svg_icon;
|
||||||
|
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
let admin_tagHTML = ``;
|
||||||
|
if (member.is_admin)
|
||||||
|
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary">Admin</span>`
|
||||||
|
|
||||||
|
// generate html blocks for domains and permissions for the member
|
||||||
|
let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, action_url);
|
||||||
|
let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices);
|
||||||
|
|
||||||
|
// domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand
|
||||||
|
let showMoreButton = '';
|
||||||
|
const showMoreRow = document.createElement('tr');
|
||||||
|
if (domainsHTML || permissionsHTML) {
|
||||||
|
showMoreButton = `
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-1"
|
||||||
|
data-for=${member_id}
|
||||||
|
aria-label="Expand for additional information"
|
||||||
|
>
|
||||||
|
<span>Expand</span>
|
||||||
|
<svg class="usa-icon usa-icon--big" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
showMoreRow.innerHTML = `<td colspan='3' headers="header-member row-header-${member_id}" class="padding-top-0"><div class='grid-row'>${domainsHTML} ${permissionsHTML}</div></td>`;
|
||||||
|
showMoreRow.classList.add('show-more-content');
|
||||||
|
showMoreRow.classList.add('display-none');
|
||||||
|
showMoreRow.id = member_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<th role="rowheader" headers="header-member" data-label="member email" id='row-header-${member_id}'>
|
||||||
|
${member_display} ${admin_tagHTML} ${showMoreButton}
|
||||||
|
</th>
|
||||||
|
<td headers="header-last-active row-header-${member_id}" data-sort-value="${last_active.sort_value}" data-label="last_active">
|
||||||
|
${last_active.display_value}
|
||||||
|
</td>
|
||||||
|
<td headers="header-action row-header-${member_id}">
|
||||||
|
<a href="${action_url}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#${svg_icon}"></use>
|
||||||
|
</svg>
|
||||||
|
${action_label} <span class="usa-sr-only">${member_name}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
memberList.appendChild(row);
|
||||||
|
if (domainsHTML || permissionsHTML) {
|
||||||
|
memberList.appendChild(showMoreRow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initShowMoreButtons();
|
||||||
|
|
||||||
|
// Do not scroll on first page load
|
||||||
|
if (scroll)
|
||||||
|
scrollToElement('class', 'members');
|
||||||
|
this.scrollToTable = true;
|
||||||
|
|
||||||
|
// update pagination
|
||||||
|
this.updatePagination(
|
||||||
|
'member',
|
||||||
|
'#members-pagination',
|
||||||
|
'#members-pagination .usa-pagination__counter',
|
||||||
|
'#members',
|
||||||
|
data.page,
|
||||||
|
data.num_pages,
|
||||||
|
data.has_previous,
|
||||||
|
data.has_next,
|
||||||
|
data.total,
|
||||||
|
);
|
||||||
|
this.currentSortBy = sortBy;
|
||||||
|
this.currentOrder = order;
|
||||||
|
this.currentSearchTerm = searchTerm;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching members:', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function initMembersTable() {
|
export function initMembersTable() {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const isMembersPage = document.getElementById("members")
|
const isMembersPage = document.getElementById("members")
|
||||||
|
|
|
@ -1,186 +1,175 @@
|
||||||
import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js';
|
|
||||||
import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js';
|
|
||||||
import { getCsrfToken } from './get-csrf-token.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An IIFE that displays confirmation modal on the user profile page
|
|
||||||
*/
|
|
||||||
export function userProfileListener() {
|
export function userProfileListener() {
|
||||||
|
const showConfirmationModalTrigger = document.querySelector('.show-confirmation-modal');
|
||||||
|
if (showConfirmationModalTrigger) {
|
||||||
|
showConfirmationModalTrigger.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const showConfirmationModalTrigger = document.querySelector('.show-confirmation-modal');
|
export function finishUserSetupListener() {
|
||||||
if (showConfirmationModalTrigger) {
|
|
||||||
showConfirmationModalTrigger.click();
|
function getInputField(fieldName){
|
||||||
|
return document.querySelector(`#id_${fieldName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows the hidden input field and hides the readonly one
|
||||||
|
function showInputFieldHideReadonlyField(fieldName, button) {
|
||||||
|
let inputField = getInputField(fieldName)
|
||||||
|
let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`)
|
||||||
|
|
||||||
|
readonlyField.classList.toggle('display-none');
|
||||||
|
inputField.classList.toggle('display-none');
|
||||||
|
|
||||||
|
// Toggle the bold style on the grid row
|
||||||
|
let gridRow = button.closest(".grid-col-2").closest(".grid-row")
|
||||||
|
if (gridRow){
|
||||||
|
gridRow.classList.toggle("bold-usa-label")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function handleFullNameField(fieldName = "full_name") {
|
||||||
* An IIFE that hooks up the edit buttons on the finish-user-setup page
|
// Remove the display-none class from the nearest parent div
|
||||||
*/
|
let nameFieldset = document.querySelector("#profile-name-group");
|
||||||
export function finishUserSetupListener() {
|
if (nameFieldset){
|
||||||
|
nameFieldset.classList.remove("display-none");
|
||||||
function getInputField(fieldName){
|
|
||||||
return document.querySelector(`#id_${fieldName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shows the hidden input field and hides the readonly one
|
// Hide the "full_name" field
|
||||||
function showInputFieldHideReadonlyField(fieldName, button) {
|
let inputField = getInputField(fieldName);
|
||||||
let inputField = getInputField(fieldName)
|
if (inputField) {
|
||||||
let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`)
|
inputFieldParentDiv = inputField.closest("div");
|
||||||
|
if (inputFieldParentDiv) {
|
||||||
readonlyField.classList.toggle('display-none');
|
inputFieldParentDiv.classList.add("display-none");
|
||||||
inputField.classList.toggle('display-none');
|
|
||||||
|
|
||||||
// Toggle the bold style on the grid row
|
|
||||||
let gridRow = button.closest(".grid-col-2").closest(".grid-row")
|
|
||||||
if (gridRow){
|
|
||||||
gridRow.classList.toggle("bold-usa-label")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleFullNameField(fieldName = "full_name") {
|
function handleEditButtonClick(fieldName, button){
|
||||||
// Remove the display-none class from the nearest parent div
|
button.addEventListener('click', function() {
|
||||||
let nameFieldset = document.querySelector("#profile-name-group");
|
// Lock the edit button while this operation occurs
|
||||||
if (nameFieldset){
|
button.disabled = true
|
||||||
nameFieldset.classList.remove("display-none");
|
|
||||||
|
if (fieldName == "full_name"){
|
||||||
|
handleFullNameField();
|
||||||
|
}else {
|
||||||
|
showInputFieldHideReadonlyField(fieldName, button);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the "full_name" field
|
// Hide the button itself
|
||||||
let inputField = getInputField(fieldName);
|
button.classList.add("display-none");
|
||||||
if (inputField) {
|
|
||||||
inputFieldParentDiv = inputField.closest("div");
|
|
||||||
if (inputFieldParentDiv) {
|
|
||||||
inputFieldParentDiv.classList.add("display-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditButtonClick(fieldName, button){
|
// Unlock after it completes
|
||||||
button.addEventListener('click', function() {
|
button.disabled = false
|
||||||
// Lock the edit button while this operation occurs
|
});
|
||||||
button.disabled = true
|
}
|
||||||
|
|
||||||
if (fieldName == "full_name"){
|
function setupListener(){
|
||||||
handleFullNameField();
|
|
||||||
}else {
|
|
||||||
showInputFieldHideReadonlyField(fieldName, button);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the button itself
|
|
||||||
button.classList.add("display-none");
|
|
||||||
|
|
||||||
// Unlock after it completes
|
|
||||||
button.disabled = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupListener(){
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) {
|
document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) {
|
||||||
// Get the "{field_name}" and "edit-button"
|
// Get the "{field_name}" and "edit-button"
|
||||||
let fieldIdParts = button.id.split("__")
|
let fieldIdParts = button.id.split("__")
|
||||||
if (fieldIdParts && fieldIdParts.length > 0){
|
if (fieldIdParts && fieldIdParts.length > 0){
|
||||||
let fieldName = fieldIdParts[0]
|
let fieldName = fieldIdParts[0]
|
||||||
|
|
||||||
// When the edit button is clicked, show the input field under it
|
// When the edit button is clicked, show the input field under it
|
||||||
handleEditButtonClick(fieldName, button);
|
handleEditButtonClick(fieldName, button);
|
||||||
|
|
||||||
let editableFormGroup = button.parentElement.parentElement.parentElement;
|
let editableFormGroup = button.parentElement.parentElement.parentElement;
|
||||||
if (editableFormGroup){
|
if (editableFormGroup){
|
||||||
let readonlyField = editableFormGroup.querySelector(".toggleable_input__readonly-field")
|
let readonlyField = editableFormGroup.querySelector(".toggleable_input__readonly-field")
|
||||||
let inputField = document.getElementById(`id_${fieldName}`);
|
let inputField = document.getElementById(`id_${fieldName}`);
|
||||||
if (!inputField || !readonlyField) {
|
if (!inputField || !readonlyField) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputFieldValue = inputField.value
|
let inputFieldValue = inputField.value
|
||||||
if (inputFieldValue || fieldName == "full_name"){
|
if (inputFieldValue || fieldName == "full_name"){
|
||||||
if (fieldName == "full_name"){
|
if (fieldName == "full_name"){
|
||||||
let firstName = document.querySelector("#id_first_name");
|
let firstName = document.querySelector("#id_first_name");
|
||||||
let middleName = document.querySelector("#id_middle_name");
|
let middleName = document.querySelector("#id_middle_name");
|
||||||
let lastName = document.querySelector("#id_last_name");
|
let lastName = document.querySelector("#id_last_name");
|
||||||
if (firstName && lastName && firstName.value && lastName.value) {
|
if (firstName && lastName && firstName.value && lastName.value) {
|
||||||
let values = [firstName.value, middleName.value, lastName.value]
|
let values = [firstName.value, middleName.value, lastName.value]
|
||||||
readonlyField.innerHTML = values.join(" ");
|
readonlyField.innerHTML = values.join(" ");
|
||||||
}else {
|
}else {
|
||||||
let fullNameField = document.querySelector('#full_name__edit-button-readonly');
|
let fullNameField = document.querySelector('#full_name__edit-button-readonly');
|
||||||
let svg = fullNameField.querySelector("svg use")
|
let svg = fullNameField.querySelector("svg use")
|
||||||
if (svg) {
|
if (svg) {
|
||||||
const currentHref = svg.getAttribute('xlink:href');
|
const currentHref = svg.getAttribute('xlink:href');
|
||||||
if (currentHref) {
|
if (currentHref) {
|
||||||
const parts = currentHref.split('#');
|
const parts = currentHref.split('#');
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
// Keep the path before '#' and replace the part after '#' with 'invalid'
|
// Keep the path before '#' and replace the part after '#' with 'invalid'
|
||||||
const newHref = parts[0] + '#error';
|
const newHref = parts[0] + '#error';
|
||||||
svg.setAttribute('xlink:href', newHref);
|
svg.setAttribute('xlink:href', newHref);
|
||||||
fullNameField.classList.add("toggleable_input__error")
|
fullNameField.classList.add("toggleable_input__error")
|
||||||
label = fullNameField.querySelector(".toggleable_input__readonly-field")
|
label = fullNameField.querySelector(".toggleable_input__readonly-field")
|
||||||
label.innerHTML = "Unknown";
|
label.innerHTML = "Unknown";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Technically, the full_name field is optional, but we want to display it as required.
|
|
||||||
// This style is applied to readonly fields (gray text). This just removes it, as
|
|
||||||
// this is difficult to achieve otherwise by modifying the .readonly property.
|
|
||||||
if (readonlyField.classList.contains("text-base")) {
|
|
||||||
readonlyField.classList.remove("text-base")
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
readonlyField.innerHTML = inputFieldValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Technically, the full_name field is optional, but we want to display it as required.
|
||||||
|
// This style is applied to readonly fields (gray text). This just removes it, as
|
||||||
|
// this is difficult to achieve otherwise by modifying the .readonly property.
|
||||||
|
if (readonlyField.classList.contains("text-base")) {
|
||||||
|
readonlyField.classList.remove("text-base")
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
readonlyField.innerHTML = inputFieldValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showInputOnErrorFields(){
|
function showInputOnErrorFields(){
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
// Get all input elements within the form
|
// Get all input elements within the form
|
||||||
let form = document.querySelector("#finish-profile-setup-form");
|
let form = document.querySelector("#finish-profile-setup-form");
|
||||||
let inputs = form ? form.querySelectorAll("input") : null;
|
let inputs = form ? form.querySelectorAll("input") : null;
|
||||||
if (!inputs) {
|
if (!inputs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullNameButtonClicked = false
|
||||||
|
inputs.forEach(function(input) {
|
||||||
|
let fieldName = input.name;
|
||||||
|
let errorMessage = document.querySelector(`#id_${fieldName}__error-message`);
|
||||||
|
|
||||||
|
// If no error message is found, do nothing
|
||||||
|
if (!fieldName || !errorMessage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullNameButtonClicked = false
|
let editButton = document.querySelector(`#${fieldName}__edit-button`);
|
||||||
inputs.forEach(function(input) {
|
if (editButton){
|
||||||
let fieldName = input.name;
|
// Show the input field of the field that errored out
|
||||||
let errorMessage = document.querySelector(`#id_${fieldName}__error-message`);
|
editButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
// If no error message is found, do nothing
|
// If either the full_name field errors out,
|
||||||
if (!fieldName || !errorMessage) {
|
// or if any of its associated fields do - show all name related fields.
|
||||||
return null;
|
let nameFields = ["first_name", "middle_name", "last_name"];
|
||||||
|
if (nameFields.includes(fieldName) && !fullNameButtonClicked){
|
||||||
|
// Click the full name button if any of its related fields error out
|
||||||
|
fullNameButton = document.querySelector("#full_name__edit-button");
|
||||||
|
if (fullNameButton) {
|
||||||
|
fullNameButton.click();
|
||||||
|
fullNameButtonClicked = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let editButton = document.querySelector(`#${fieldName}__edit-button`);
|
|
||||||
if (editButton){
|
|
||||||
// Show the input field of the field that errored out
|
|
||||||
editButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If either the full_name field errors out,
|
|
||||||
// or if any of its associated fields do - show all name related fields.
|
|
||||||
let nameFields = ["first_name", "middle_name", "last_name"];
|
|
||||||
if (nameFields.includes(fieldName) && !fullNameButtonClicked){
|
|
||||||
// Click the full name button if any of its related fields error out
|
|
||||||
fullNameButton = document.querySelector("#full_name__edit-button");
|
|
||||||
if (fullNameButton) {
|
|
||||||
fullNameButton.click();
|
|
||||||
fullNameButtonClicked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
|
||||||
setupListener();
|
setupListener();
|
||||||
|
|
||||||
// Show the input fields if an error exists
|
// Show the input fields if an error exists
|
||||||
showInputOnErrorFields();
|
showInputOnErrorFields();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue