diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 613318cfe..08b9ea803 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 += "
";
+
+ // Display up to 6 domains with their URLs
+ for (let i = 0; i < num_domains && i < 6; i++) {
+ 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).
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.
@@ -1932,7 +2123,9 @@ 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';
let existingExtraActionsHeader = document.querySelector('.extra-actions-header');
@@ -1949,15 +2142,15 @@ class MembersTable extends LoadTableBase {
}
data.members.forEach(member => {
+ const member_id = member.source + member.id;
const member_name = member.name;
const member_email = member.email;
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 = '';
let kebob = '';
if (hasEditPermission) {
@@ -2080,27 +2273,7 @@ class MembersTable extends LoadTableBase {
`
}
- // 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;
@@ -2112,14 +2285,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 = `