mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-03 00:12:16 +02:00
280 lines
11 KiB
JavaScript
280 lines
11 KiB
JavaScript
import { hideElement, showElement, getCsrfToken } from './helpers.js';
|
|
import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js';
|
|
|
|
import { BaseTable, addModal, generateKebabHTML } from './table-base.js';
|
|
|
|
|
|
const utcDateString = (dateString) => {
|
|
const date = new Date(dateString);
|
|
const utcYear = date.getUTCFullYear();
|
|
const utcMonth = date.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' });
|
|
const utcDay = date.getUTCDate().toString().padStart(2, '0');
|
|
let utcHours = date.getUTCHours();
|
|
const utcMinutes = date.getUTCMinutes().toString().padStart(2, '0');
|
|
|
|
const ampm = utcHours >= 12 ? 'PM' : 'AM';
|
|
utcHours = utcHours % 12 || 12; // Convert to 12-hour format, '0' hours should be '12'
|
|
|
|
return `${utcMonth} ${utcDay}, ${utcYear}, ${utcHours}:${utcMinutes} ${ampm} UTC`;
|
|
};
|
|
|
|
|
|
export class DomainRequestsTable extends BaseTable {
|
|
|
|
constructor() {
|
|
super('domain-request');
|
|
this.displayName = "domain request";
|
|
}
|
|
|
|
getBaseUrl() {
|
|
return document.getElementById("get_domain_requests_json_url");
|
|
}
|
|
|
|
toggleExportButton(requests) {
|
|
const exportButton = document.getElementById('export-csv');
|
|
if (exportButton) {
|
|
if (requests.length > 0) {
|
|
showElement(exportButton);
|
|
} else {
|
|
hideElement(exportButton);
|
|
}
|
|
}
|
|
}
|
|
|
|
getDataObjects(data) {
|
|
return data.domain_requests;
|
|
}
|
|
unloadModals() {
|
|
uswdsUnloadModals();
|
|
}
|
|
customizeTable(data) {
|
|
|
|
// Manage "export as CSV" visibility for domain requests
|
|
this.toggleExportButton(data.domain_requests);
|
|
|
|
let 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 width-5');
|
|
delheader.innerHTML = `
|
|
<span class="usa-sr-only">Delete Action</span>`;
|
|
let tableHeaderRow = this.tableWrapper.querySelector('thead tr');
|
|
tableHeaderRow.appendChild(delheader);
|
|
}
|
|
}
|
|
return { 'needsAdditionalColumn': needsDeleteColumn };
|
|
}
|
|
|
|
addRow(dataObject, tbody, customTableOptions) {
|
|
const request = dataObject;
|
|
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) {
|
|
// 1st path: Just a modal trigger in any screen size for non-org users
|
|
modalTrigger = `
|
|
<a
|
|
role="button"
|
|
id="button-toggle-delete-domain-${request.id}"
|
|
href="#toggle-delete-domain-${request.id}"
|
|
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger line-height-sans-5"
|
|
aria-controls="toggle-delete-domain-${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>`
|
|
|
|
// Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly
|
|
if (this.portfolioValue) {
|
|
|
|
// 2nd path: Just a modal trigger on mobile for org users or kebab + accordion with nested modal trigger on desktop for org users
|
|
modalTrigger = generateKebabHTML('delete-domain', request.id, 'Delete', domainName);
|
|
}
|
|
}
|
|
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<th scope="row" role="rowheader" data-label="Domain name">
|
|
${domainName}
|
|
</th>
|
|
<td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
|
|
${submissionDate}
|
|
</td>
|
|
${markupCreatorRow}
|
|
<td data-label="Status">
|
|
${request.status}
|
|
</td>
|
|
<td>
|
|
<a href="${actionUrl}">
|
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
<use xlink:href="/public/img/sprite.svg#${request.svg_icon}"></use>
|
|
</svg>
|
|
${actionLabel} <span class="usa-sr-only">${request.requested_domain ? request.requested_domain : 'New domain request'}</span>
|
|
</a>
|
|
</td>
|
|
${customTableOptions.needsAdditionalColumn ? '<td>'+modalTrigger+'</td>' : ''}
|
|
`;
|
|
tbody.appendChild(row);
|
|
if (request.is_deletable) DomainRequestsTable.addDomainRequestsModal(request.requested_domain, request.id, request.created_at, tbody);
|
|
}
|
|
|
|
loadModals(page, total, unfiltered_total) {
|
|
// initialize modals immediately after the DOM content is updated
|
|
uswdsInitializeModals();
|
|
|
|
// 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');
|
|
// Workaround: 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 = page;
|
|
if (total == 1 && unfiltered_total > 1) {
|
|
pageToDisplay--;
|
|
}
|
|
|
|
this.deleteDomainRequest(pk, pageToDisplay);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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.currentStatus, this.currentSearchTerm);
|
|
})
|
|
.catch(error => console.error('Error fetching domain requests:', error));
|
|
}
|
|
|
|
/**
|
|
* Modal that displays when deleting a domain request
|
|
* @param {string} requested_domain - The requested domain URL
|
|
* @param {string} id - The request's ID
|
|
* @param {string}} created_at - When the request was created at
|
|
* @param {HTMLElement} wrapper_element - The element to which the modal is appended
|
|
*/
|
|
static addDomainRequestsModal(requested_domain, id, created_at, wrapper_element) {
|
|
// 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 (requested_domain) {
|
|
modalHeading = `Are you sure you want to delete ${requested_domain}?`;
|
|
modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.';
|
|
} else {
|
|
if (created_at) {
|
|
modalHeading = 'Are you sure you want to delete this domain request?';
|
|
modalDescription = `This will remove the domain request (created ${utcDateString(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.';
|
|
}
|
|
}
|
|
|
|
const modalSubmit = `
|
|
<button type="button"
|
|
class="usa-button usa-button--secondary usa-modal__submit"
|
|
data-pk = ${id}
|
|
name="delete-domain-request">Yes, delete request</button>
|
|
`
|
|
|
|
addModal(`toggle-delete-domain-${id}`, 'Are you sure you want to continue?', 'Domain will be removed', modalHeading, modalDescription, modalSubmit, wrapper_element, true);
|
|
|
|
}
|
|
}
|
|
|
|
export function initDomainRequestsTable() {
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const domainRequestsSectionWrapper = document.getElementById('domain-requests');
|
|
if (domainRequestsSectionWrapper) {
|
|
const domainRequestsTable = new DomainRequestsTable();
|
|
if (domainRequestsTable.tableWrapper) {
|
|
domainRequestsTable.loadTable(1);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('focusin', function(event) {
|
|
closeOpenAccordions(event);
|
|
});
|
|
|
|
document.addEventListener('click', function(event) {
|
|
closeOpenAccordions(event);
|
|
});
|
|
|
|
function closeMoreActionMenu(accordionThatIsOpen) {
|
|
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
|
|
accordionThatIsOpen.click();
|
|
}
|
|
}
|
|
|
|
function closeOpenAccordions(event) {
|
|
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
|
|
openAccordions.forEach((openAccordionButton) => {
|
|
// Find the corresponding accordion
|
|
const accordion = openAccordionButton.closest('.usa-accordion--more-actions');
|
|
if (accordion && !accordion.contains(event.target)) {
|
|
// Close the accordion if the click is outside
|
|
closeMoreActionMenu(openAccordionButton);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|