diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index ee7d3eb05..6c1c81aae 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1874,6 +1874,197 @@ class MembersTable extends LoadTableBase { constructor() { super('.members__table', '.members__table-wrapper', '#members__search-field', '#members__search-field-submit', '.members__reset-search', '.members__reset-filters', '.members__no-data', '.members__no-search-results'); } + + /** + * 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) { + // 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 += "
"; + domainsHTML += "

Domains assigned

"; + domainsHTML += `

This member is assigned to ${num_domains} domains:

`; + domainsHTML += ""; + + // If there are more than 6 domains, display a "View assigned domains" link + if (num_domains >= 6) { + domainsHTML += "

View assigned domains

"; + } + + domainsHTML += "
"; + } + + 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 += "

Domains: Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; + } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) { + permissionsHTML += "

Domains: Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; + } + + // Check request-related permissions + if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) { + permissionsHTML += "

Domain requests: Can view all organization domain requests. Can create domain requests and modify their own requests.

"; + } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) { + permissionsHTML += "

Domain requests (view-only): Can view all organization domain requests. Can't create or modify any domain requests.

"; + } + + // Check member-related permissions + if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) { + permissionsHTML += "

Members: Can manage members including inviting new members, removing current members, and assigning domains to members.

"; + } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) { + permissionsHTML += "

Members (view-only): Can view all organizational members. Can't manage any members.

"; + } + + // If no specific permissions are assigned, display a message indicating no additional permissions + if (!permissionsHTML) { + permissionsHTML += "

No additional permissions: There are no additional permissions for this member.

"; + } + + // Add a permissions header and wrap the entire output in a container + permissionsHTML = "

Additional permissions for this member

" + permissionsHTML + "
"; + + return permissionsHTML; + } + /** * Loads rows in the members list, as well as updates pagination around the members list * based on the supplied attributes. @@ -1927,39 +2118,20 @@ class MembersTable extends LoadTableBase { const memberList = document.querySelector('.members__table 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 options = { year: 'numeric', month: 'short', day: 'numeric' }; + const member_permissions = member.permissions; + const domain_urls = member.domain_urls; + const domain_names = member.domain_names; + const num_domains = domain_urls.length; - // Handle last_active values - let last_active = member.last_active; - let last_active_formatted = ''; - let last_active_sort_value = ''; - - // Handle 'Invited' or null/empty values differently from valid dates - if (last_active && last_active !== invited) { - try { - // Try to parse the last_active as a valid date - last_active = new Date(last_active); - if (!isNaN(last_active)) { - last_active_formatted = last_active.toLocaleDateString('en-US', options); - last_active_sort_value = last_active.getTime(); // For sorting purposes - } else { - last_active_formatted='Invalid date' - } - } catch (e) { - console.error(`Error parsing date: ${last_active}. Error: ${e}`); - last_active_formatted='Invalid date' - } - } else { - // Handle 'Invited' or null - last_active = invited; - last_active_formatted = invited; - last_active_sort_value = invited; // Keep 'Invited' as a sortable string - } + const last_active = this.handleLastActive(member.last_active); const action_url = member.action_url; const action_label = member.action_label; @@ -1971,14 +2143,42 @@ class MembersTable extends LoadTableBase { if (member.is_admin) admin_tagHTML = `Admin` + // generate html blocks for domains and permissions for the member + let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls); + 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 = ` + + `; + + showMoreRow.innerHTML = `
${domainsHTML} ${permissionsHTML}
`; + showMoreRow.classList.add('show-more-content'); + showMoreRow.classList.add('display-none'); + showMoreRow.id = member_id; + } + row.innerHTML = ` - - ${member_display} ${admin_tagHTML} + + ${member_display} ${admin_tagHTML} ${showMoreButton} - - ${last_active_formatted} + + ${last_active.display_value} - +