Skip to content

Commit

Permalink
Merge pull request #3491 from cisagov/rb/3281-members-design-review
Browse files Browse the repository at this point in the history
#3281: DESIGN REVIEW - Members management - [RB]
  • Loading branch information
rachidatecs authored Feb 21, 2025
2 parents a70eee4 + 6525a91 commit 636f217
Show file tree
Hide file tree
Showing 34 changed files with 500 additions and 299 deletions.
11 changes: 11 additions & 0 deletions src/registrar/assets/src/js/getgov/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,14 @@ export function submitForm(form_id) {
console.error("Form '" + form_id + "' not found.");
}
}

/**
* Helper function to strip HTML tags
* THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
*/
export function unsafeStripHtmlTags(input) {
const tempDiv = document.createElement("div");
// NOTE: THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
tempDiv.innerHTML = input;
return tempDiv.textContent || tempDiv.innerText || "";
}
28 changes: 16 additions & 12 deletions src/registrar/assets/src/js/getgov/portfolio-member-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function initPortfolioNewMemberPageToggle() {
const unique_id = `${member_type}-${member_id}`;

let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`);
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`, "usa-icon--large");

// This easter egg is only for fixtures that dont have names as we are displaying their emails
// All prod users will have emails linked to their account
Expand Down Expand Up @@ -100,8 +100,8 @@ export function initAddNewMemberPageListeners() {
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);

permissionSections.forEach(section => {
// Find the <h3> element text
const sectionTitle = section.textContent;
// Find the <h3> element text, strip out the '*'
const sectionTitle = section.textContent.trim().replace(/\*$/, "") + ": ";

// Find the associated radio buttons container (next fieldset)
const fieldset = section.nextElementSibling;
Expand All @@ -128,25 +128,29 @@ export function initAddNewMemberPageListeners() {
});
} else {
// for admin users, the permissions are always the same
appendPermissionInContainer('Domains', 'Viewer', permissionDetailsContainer);
appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer);
appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer);
appendPermissionInContainer('Domains: ', 'Viewer', permissionDetailsContainer);
appendPermissionInContainer('Domain requests: ', 'Creator', permissionDetailsContainer);
appendPermissionInContainer('Members: ', 'Manager', permissionDetailsContainer);
}
}

function appendPermissionInContainer(sectionTitle, permissionDisplay, permissionContainer) {
// Create new elements for the content
const titleElement = document.createElement("h4");
const elementContainer = document.createElement("p");
elementContainer.classList.add("margin-top-0", "margin-bottom-1");

const titleElement = document.createElement("strong");
titleElement.textContent = sectionTitle;
titleElement.classList.add("text-primary", "margin-bottom-0");
titleElement.classList.add("text-primary-darker");

const permissionElement = document.createElement("p");
const permissionElement = document.createElement("span");
permissionElement.textContent = permissionDisplay;
permissionElement.classList.add("margin-top-0");

// Append to the content container
permissionContainer.appendChild(titleElement);
permissionContainer.appendChild(permissionElement);
elementContainer.appendChild(titleElement);
elementContainer.appendChild(permissionElement);

permissionContainer.appendChild(elementContainer);
}

/*
Expand Down
10 changes: 5 additions & 5 deletions src/registrar/assets/src/js/getgov/table-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ export function addModal(id, ariaLabelledby, ariaDescribedby, modalHeading, moda
* @param {string} modal_button_text - The action button's text
* @param {string} screen_reader_text - A screen reader helper
*/
export function generateKebabHTML(action, unique_id, modal_button_text, screen_reader_text) {
export function generateKebabHTML(action, unique_id, modal_button_text, screen_reader_text, icon_class) {
const generateModalButton = (mobileOnly = false) => `
<a
role="button"
id="button-trigger-${action}-${unique_id}"
href="#toggle-${action}-${unique_id}"
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary ${mobileOnly ? 'visible-mobile-flex' : ''}"
class="usa-button usa-button--unstyled text-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary ${mobileOnly ? 'visible-mobile-flex' : ''}"
aria-controls="toggle-${action}-${unique_id}"
data-open-modal
>
Expand All @@ -99,7 +99,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
// Main kebab structure
const kebab = `
${generateModalButton(true)} <!-- Mobile button -->
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
<div class="usa-accordion usa-accordion--more-actions margin-right-2 margin-top-3px hidden-mobile-flex">
<div class="usa-accordion__heading">
<button
type="button"
Expand All @@ -108,12 +108,12 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
aria-controls="more-actions-${unique_id}"
aria-label="${screen_reader_text}"
>
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
<svg class="usa-icon${icon_class ? " " + icon_class : ""}" 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-${unique_id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-neg-1" hidden>
<div id="more-actions-${unique_id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0${icon_class === 'usa-icon--large' ? ' top-28px' : ''}" hidden>
<h2>More options</h2>
${generateModalButton()} <!-- Desktop button -->
</div>
Expand Down
9 changes: 5 additions & 4 deletions src/registrar/assets/src/js/getgov/table-domain-requests.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { hideElement, showElement, getCsrfToken } from './helpers.js';
import { hideElement, showElement, getCsrfToken, unsafeStripHtmlTags } from './helpers.js';
import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js';

import { BaseTable, addModal, generateKebabHTML } from './table-base.js';
Expand Down Expand Up @@ -98,9 +98,10 @@ export class DomainRequestsTable extends BaseTable {

// 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) {

// NOTE: THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
const sanitizedDomainName = unsafeStripHtmlTags(domainName);
// 2nd path (org model): 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);
modalTrigger = generateKebabHTML('delete-domain', request.id, 'Delete', sanitizedDomainName);
}
}

Expand All @@ -117,7 +118,7 @@ export class DomainRequestsTable extends BaseTable {
${request.status}
</td>
<td class="width--action-column">
<div class="tablet:display-flex tablet:flex-row flex-wrap">
<div class="tablet:display-flex tablet:flex-row">
<a href="${actionUrl}" ${customTableOptions.hasAdditionalActions ? "class='margin-right-2'" : ''}>
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="/public/img/sprite.svg#${request.svg_icon}"></use>
Expand Down
98 changes: 50 additions & 48 deletions src/registrar/assets/src/js/getgov/table-edit-member-domains.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ export class EditMemberDomainsTable extends BaseTable {
this.readonlyModeContainer = document.getElementById('domain-assignments-readonly-view');
this.reviewButton = document.getElementById('review-domain-assignments');
this.backButton = document.getElementById('back-to-edit-domain-assignments');
this.saveButton = document.getElementById('save-domain-assignments');
this.initializeDomainAssignments();
this.saveButton = document.getElementById('save-domain-assignments');
}
async init() {
await this.initializeDomainAssignments();
this.initCancelEditDomainAssignmentButton();
this.initEventListeners();
return this;
}
getBaseUrl() {
return document.getElementById("get_member_domains_json_url");
Expand Down Expand Up @@ -134,27 +137,33 @@ export class EditMemberDomainsTable extends BaseTable {
* member. It populates both initialDomainAssignments and initialDomainAssignmentsOnlyMember.
* It is called once per page load, but not called with subsequent table changes.
*/
initializeDomainAssignments() {
async initializeDomainAssignments() {
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
if (!baseUrlValue) return;
let searchParams = this.getDomainAssignmentSearchParams(this.portfolioValue);
let url = baseUrlValue + "?" + searchParams.toString();
fetch(url)
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Error in AJAX call: ' + data.error);
if (!baseUrlValue) {
console.error("Base URL not found");
return;
}

try {
let searchParams = this.getDomainAssignmentSearchParams(this.portfolioValue);
let url = baseUrlValue + "?" + searchParams.toString();

let response = await fetch(url);
let data = await response.json();

if (data.error) {
console.error("Error in AJAX call:", data.error);
return;
}

let dataObjects = this.getDataObjects(data);
// Map the id attributes of dataObjects to this.initialDomainAssignments
this.initialDomainAssignments = dataObjects.map(obj => obj.id);
this.initialDomainAssignmentsOnlyMember = dataObjects
.filter(obj => obj.member_is_only_manager)
.map(obj => obj.id);
})
.catch(error => console.error('Error fetching domain assignments:', error));
.filter(obj => obj.member_is_only_manager)
.map(obj => obj.id);
} catch (error) {
console.error("Error fetching domain assignments:", error);
}
}
/**
* Initializes listeners on checkboxes in the table. Checkbox listeners are used
Expand Down Expand Up @@ -232,8 +241,6 @@ export class EditMemberDomainsTable extends BaseTable {
}

updateReadonlyDisplay() {
let totalAssignedDomains = this.getCheckedDomains().length;

// Create unassigned domains list
const unassignedDomainsList = document.createElement('ul');
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
Expand All @@ -260,35 +267,30 @@ export class EditMemberDomainsTable extends BaseTable {
// Clear existing content
domainAssignmentSummary.innerHTML = '';

// Append unassigned domains section
if (this.removedDomains.length) {
const unassignedHeader = document.createElement('h3');
unassignedHeader.classList.add('margin-bottom-05', 'h4');
unassignedHeader.textContent = 'Unassigned domains';
domainAssignmentSummary.appendChild(unassignedHeader);
domainAssignmentSummary.appendChild(unassignedDomainsList);
}

// Append assigned domains section
if (this.addedDomains.length) {
const assignedHeader = document.createElement('h3');
// Make this h3 look like a h4
assignedHeader.classList.add('margin-bottom-05', 'h4');
assignedHeader.textContent = 'Assigned domains';
assignedHeader.textContent = `New assignments (${this.addedDomains.length})`;
domainAssignmentSummary.appendChild(assignedHeader);
domainAssignmentSummary.appendChild(assignedDomainsList);
}

// Append total assigned domains section
const totalHeader = document.createElement('h3');
// Make this h3 look like a h4
totalHeader.classList.add('margin-bottom-05', 'h4');
totalHeader.textContent = 'Total assigned domains';
domainAssignmentSummary.appendChild(totalHeader);
const totalCount = document.createElement('p');
totalCount.classList.add('margin-y-0');
totalCount.textContent = totalAssignedDomains;
domainAssignmentSummary.appendChild(totalCount);
// Append unassigned domains section
if (this.removedDomains.length) {
const unassignedHeader = document.createElement('h3');
unassignedHeader.classList.add('margin-bottom-05', 'h4');
unassignedHeader.textContent =`Removed assignments (${this.removedDomains.length})`;
domainAssignmentSummary.appendChild(unassignedHeader);
domainAssignmentSummary.appendChild(unassignedDomainsList);
}

if (!this.addedDomains.length && !this.removedDomains.length) {
const noChangesParagraph = document.createElement('p');
noChangesParagraph.textContent = "No changes were detected. Click the “Back” button to edit this member’s domain assignments.";
domainAssignmentSummary.appendChild(noChangesParagraph);
}
}

showReadonlyMode() {
Expand Down Expand Up @@ -355,14 +357,14 @@ export class EditMemberDomainsTable extends BaseTable {
}

export function initEditMemberDomainsTable() {
document.addEventListener('DOMContentLoaded', function() {
const isEditMemberDomainsPage = document.getElementById("edit-member-domains");
if (isEditMemberDomainsPage) {
const editMemberDomainsTable = new EditMemberDomainsTable();
if (editMemberDomainsTable.tableWrapper) {
// Initial load
editMemberDomainsTable.loadTable(1);
}
}
});
document.addEventListener('DOMContentLoaded', async function() {
const isEditMemberDomainsPage = document.getElementById("edit-member-domains");
if (!isEditMemberDomainsPage) return; // Exit if not on the right page

const editMemberDomainsTable = await new EditMemberDomainsTable().init();

if (editMemberDomainsTable.tableWrapper) {
editMemberDomainsTable.loadTable(1); // Initial load
}
});
}
Loading

0 comments on commit 636f217

Please sign in to comment.