diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 000df1b04..9d1fec9bb 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -86,6 +86,218 @@ function makeVisible(el) { el.style.visibility = "visible"; } +// TODO: Write caption here +function addModal(member_email, member_id, num_domains, submit_delete_url, wrapper_element) { + console.log("We are in addModal") + let modalHeading = ''; + let modalDescription = ''; + + console.log("member_id is", member_id) + + if (num_domains === 0){ + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `They will no longer be able to access this organization. \n + This action cannot be undone.`; + } else if (num_domains === 1) { + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `${member_email} currently manages ${num_domains} domain in the organization. \n + Removing them from the organization will remove all of their domains. They will no longer be able to \n + access this organization. This action cannot be undone.`; + } else if (num_domains >= 1) { + modalHeading = `Are you sure you want to delete ${member_email}?`; + modalDescription = `${member_email} currently manages ${num_domains} domains in the organization. \n + Removing them from the organization will remove all of their domains. They will no longer be able to \n + access this organization. This action cannot be undone.`; + } + + const modalSubmit = ` + + ` + + const modal = document.createElement('div'); + modal.setAttribute('class', 'usa-modal'); + modal.setAttribute('id', `toggle-remove-member-${member_id}`); + modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); + modal.setAttribute('aria-describedby', 'Member will be removed'); + modal.setAttribute('data-force-action', ''); + + console.log("modal is", modal) + + modal.innerHTML = ` +
+
+ +
+ +
+ +
+ +
+ ` + if (wrapper_element) { + wrapper_element.appendChild(modal); + } else { + document.body.appendChild(modal); + } + + // wrapper_element.appendChild(modal); +} + +// TODO: Write caption here +function generateKebabHTML(unique_id, member_name, member_type) { + let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member"; + + const kebab = ` + + ${cancelInvitationButton} ${member_name} + + +
+
+ +
+ +
+ ` + return kebab +} + +function deleteMember(member_delete_url, pageToDisplay) { + // Debugging + console.log(member_delete_url); + + // Get csrf token + const csrfToken = getCsrfToken(); + // Create FormData object and append the CSRF token + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}`; + + fetch(`${member_delete_url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': csrfToken, + }, + body: formData + }) + .then(response => { + if (response.status === 200) { + response.json().then(data => { + if (data.success) { + addAlert("success", data.success); + } + memberTableInstance.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + }); + } else { + // If the response isn't 204, handle the error response + response.json().then(data => { + console.log("Member response not 200"); + if (data.error) { + // This should display the error given from backend for + // either only admin OR in progress requests + addAlert("error", data.error); + } else { + throw new Error(`Unexpected status: ${response.status}`); + } + }); + } + }) + .catch(error => { + console.error('Error deleting member:', error); + }); +} + + +/** + * Adds an alert message to the page with an alert class. + * + * @param {string} alertClass - {error, warning, info, success} + * @param {string} alertMessage - The text that will be displayed + * + */ +function addAlert(alertClass, alertMessage) { + let toggleableAlertDiv = document.getElementById("toggleable-alert"); + this.resetAlert(); + toggleableAlertDiv.classList.add(`usa-alert--${alertClass}`); + let alertParagraph = toggleableAlertDiv.querySelector(".usa-alert__text"); + alertParagraph.innerHTML = alertMessage + showElement(toggleableAlertDiv); +} + +/** + * Resets the reusable alert message + * + */ +function resetAlert() { + let toggleableAlertDiv = document.getElementById("toggleable-alert"); + toggleableAlertDiv.classList.remove('usa-alert--error'); + toggleableAlertDiv.classList.remove('usa-alert--success'); + hideElement(toggleableAlertDiv); +} + /** * Toggles expand_more / expand_more svgs in buttons or anchors * @param {Element} element - DOM element @@ -2064,208 +2276,6 @@ class MembersTable extends LoadTableBase { return permissionsHTML; } - addModal(member, member_id, num_domains, submit_delete_url) { - const member_email = member.email; - let modalHeading = ''; - let modalDescription = ''; - - if (num_domains === 0){ - modalHeading = `Are you sure you want to delete ${member_email}?`; - modalDescription = `They will no longer be able to access this organization. \n - This action cannot be undone.`; - } else if (num_domains === 1) { - modalHeading = `Are you sure you want to delete ${member_email}?`; - modalDescription = `${member_email} currently manages ${num_domains} domain in the organization. \n - Removing them from the organization will remove all of their domains. They will no longer be able to \n - access this organization. This action cannot be undone.`; - } else if (num_domains >= 1) { - modalHeading = `Are you sure you want to delete ${member_email}?`; - modalDescription = `${member_email} currently manages ${num_domains} domains in the organization. \n - Removing them from the organization will remove all of their domains. They will no longer be able to \n - access this organization. This action cannot be undone.`; - } - - const modalSubmit = ` - - ` - - const modal = document.createElement('div'); - modal.setAttribute('class', 'usa-modal'); - modal.setAttribute('id', `toggle-remove-member-${member_id}`); - modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); - modal.setAttribute('aria-describedby', 'Member will be removed'); - modal.setAttribute('data-force-action', ''); - - modal.innerHTML = ` -
-
- -
- -
- -
- -
- ` - this.tableWrapper.appendChild(modal); - } - - generateKebabHTML(member_dom_id, member_name, last_active) { - let isMemberInvited = !last_active || last_active === 'Invited'; - let cancelInvitationButton = isMemberInvited ? "Cancel invitation" : "Remove member"; - - const kebab = ` - - ${cancelInvitationButton} ${member_name} - - -
-
- -
- -
- ` - return kebab - } - - deleteMember(member_delete_url, pageToDisplay) { - // Debugging - console.log(member_delete_url); - - // Get csrf token - const csrfToken = getCsrfToken(); - // Create FormData object and append the CSRF token - const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}`; - - fetch(`${member_delete_url}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - }, - body: formData - }) - .then(response => { - if (response.status === 200) { - response.json().then(data => { - if (data.success) { - this.addAlert("success", data.success); - } - this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); - }); - } else { - // If the response isn't 204, handle the error response - response.json().then(data => { - console.log("Member response not 200"); - if (data.error) { - // This should display the error given from backend for - // either only admin OR in progress requests - this.addAlert("error", data.error); - } else { - throw new Error(`Unexpected status: ${response.status}`); - } - }); - } - }) - .catch(error => { - console.error('Error deleting member:', error); - }); - } - - - /** - * Adds an alert message to the page with an alert class. - * - * @param {string} alertClass - {error, warning, info, success} - * @param {string} alertMessage - The text that will be displayed - * - */ - addAlert(alertClass, alertMessage) { - let toggleableAlertDiv = document.getElementById("toggleable-alert"); - this.resetAlert(); - toggleableAlertDiv.classList.add(`usa-alert--${alertClass}`); - let alertParagraph = toggleableAlertDiv.querySelector(".usa-alert__text"); - alertParagraph.innerHTML = alertMessage - showElement(toggleableAlertDiv); - } - - /** - * Resets the reusable alert message - * - */ - resetAlert() { - let toggleableAlertDiv = document.getElementById("toggleable-alert"); - toggleableAlertDiv.classList.remove('usa-alert--error'); - toggleableAlertDiv.classList.remove('usa-alert--success'); - hideElement(toggleableAlertDiv); - } - - /** * Loads rows in the members list, as well as updates pagination around the members list * based on the supplied attributes. @@ -2341,27 +2351,18 @@ class MembersTable extends LoadTableBase { } data.members.forEach(member => { - // org_member is based on either a UserPortfolioPermission or a PortfolioInvitation + // member is based on either a UserPortfolioPermission or a PortfolioInvitation // and also includes information from related domains; the 'id' of the org_member // is the id of the UserPorfolioPermission or PortfolioInvitation, it is not a user id - const member_dom_id = org_member.type + org_member.id; // unique string for use in dom, this is + // member.type is either invitedmember or member + const unique_id = member.type + member.id; // unique string for use in dom, this is // not the id of the associated user - const member_delete_url = org_member.action_url + "/delete"; - const member_name = org_member.name; // name of the associated user - const member_display = member.member_display; // display value (email/name) of the associated user - const member_permissions = member.permissions; - // The url, names, and num_domains relates specifically to the domain info that the member manages - const domain_urls = member.domain_urls; - const domain_names = member.domain_names; - const num_domains = domain_urls.length; + const member_delete_url = member.action_url + "/delete"; + const num_domains = member.domain_urls.length; const last_active = this.handleLastActive(member.last_active); - const kebabHTML = hasEditPermission ? this.generateKebabHTML(member_dom_id, member_name, last_active): ''; + const kebabHTML = hasEditPermission ? generateKebabHTML(unique_id, member.name, member.type): ''; - if (hasEditPermission) this.addModal(member, member_dom_id, num_domains, member_delete_url); - - const action_url = member.action_url; - const action_label = member.action_label; - const svg_icon = member.svg_icon; + if (hasEditPermission) addModal(member.email, unique_id, num_domains, member_delete_url, this.tableWrapper); const row = document.createElement('tr'); @@ -2370,8 +2371,8 @@ class MembersTable extends LoadTableBase { admin_tagHTML = `Admin` // 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); + let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.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 = ''; @@ -2381,7 +2382,7 @@ class MembersTable extends LoadTableBase { `; - showMoreRow.innerHTML = `
${domainsHTML} ${permissionsHTML}
`; + showMoreRow.innerHTML = `
${domainsHTML} ${permissionsHTML}
`; showMoreRow.classList.add('show-more-content'); showMoreRow.classList.add('display-none'); - showMoreRow.id = member_dom_id; + showMoreRow.id = unique_id; } row.innerHTML = ` - - ${member_display} ${admin_tagHTML} ${showMoreButton} + + ${member.member_display} ${admin_tagHTML} ${showMoreButton} - + ${last_active.display_value} - - + + - ${action_label} ${member_name} + ${member.action_label} ${member.name} ${hasEditPermission ? ''+kebabHTML+'' : ''} @@ -2441,13 +2442,7 @@ class MembersTable extends LoadTableBase { pageToDisplay--; } - this.deleteMember(pk, pageToDisplay); - - // Pass member_delete_url in to delete - // TODO: Use the PK to call a separate function that triggers a new backend AJAX call - // to delete their UserDomainRoles only for this portfolio + remove their UserPortfolioPermissions - //alert('modal submit') - + deleteMember(pk, pageToDisplay); }); }); @@ -2991,3 +2986,72 @@ document.addEventListener('DOMContentLoaded', function() { } } })(); + +document.addEventListener("DOMContentLoaded", () => { + (function portfolioMemberToggle() { + console.log("IN PORTFOLIOMEMBERTOGGLE") + const wrapperDeleteAction = document.getElementById("wrapper-delete-action") + console.log("!!!", wrapperDeleteAction) + if (wrapperDeleteAction) { + const member_type = wrapperDeleteAction.getAttribute("data-member-type"); + const member_id = wrapperDeleteAction.getAttribute("data-member-id"); + const num_domains = wrapperDeleteAction.getAttribute("data-num-domains"); + const member_name = wrapperDeleteAction.getAttribute("data-member-name"); + const member_email = wrapperDeleteAction.getAttribute("data-member-email"); + const member_delete_url = `${member_type}-${member_id}/delete`; + const unique_id = `${member_type}-${member_id}`; + + wrapperDeleteAction.innerHTML = generateKebabHTML(unique_id, member_name, member_type); + console.log("WE GENERATED THE KEBAB HERE") + + // Select the button and the menu we just inserted + const kebabButton = wrapperDeleteAction.querySelector(`#button-toggle-more-actions-${unique_id}`); + const kebabMenu = wrapperDeleteAction.querySelector(`#more-actions-${unique_id}`); + console.log("BEFORE LISTENER") + + kebabButton.addEventListener('click', () => { + const isExpanded = kebabButton.getAttribute('aria-expanded') === 'true'; + kebabButton.setAttribute('aria-expanded', !isExpanded); + console.log("IN LISTENER") + kebabMenu.style.display = isExpanded ? 'none' : 'block'; + console.log("Menu is now", isExpanded ? "hidden" : "visible"); + }); + + // Handles clicks outside the kebab menu + document.addEventListener('click', (event) => { + const isClickInsideButton = kebabButton.contains(event.target); + const isClickInsideMenu = kebabMenu.contains(event.target); + + if (!isClickInsideButton && !isClickInsideMenu) { + kebabButton.setAttribute('aria-expanded', 'false'); + kebabMenu.style.display = 'none'; + console.log("Menu is hidden"); + } + }); + console.log("AFTER LISTENER") + + addModal(member_email, member_id, num_domains, member_delete_url, wrapperDeleteAction); + + 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'); + 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--; + } + + deleteMember(pk, pageToDisplay); + }); + }); + } + })(); +}); \ No newline at end of file diff --git a/src/registrar/templates/portfolio_member.html b/src/registrar/templates/portfolio_member.html index c0611f854..880964d89 100644 --- a/src/registrar/templates/portfolio_member.html +++ b/src/registrar/templates/portfolio_member.html @@ -1,7 +1,11 @@ {% extends 'portfolio_base.html' %} {% load static field_helpers%} -{% block title %}Organization member {% endblock %} +{% block title %}Organization member + +{% endblock %} {% load static %} @@ -33,57 +37,26 @@ {% if has_edit_members_portfolio_permission %} {% if member %} - - Remove member - - {% else %} - - Cancel invitation - - {% endif %} - -
-
- -
-