This commit is contained in:
Rachid Mrad 2024-09-05 00:13:18 -04:00
parent cc7f588dad
commit be8a618791
No known key found for this signature in database
12 changed files with 212 additions and 22 deletions

View file

@ -1168,7 +1168,6 @@ document.addEventListener('DOMContentLoaded', function() {
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
const statusIndicator = document.querySelector('.domain__filter-indicator');
const statusToggle = document.querySelector('.usa-button--filter');
const noPortfolioFlag = document.getElementById('no-portfolio-js-flag');
const portfolioElement = document.getElementById('portfolio-js-value');
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
@ -1226,7 +1225,7 @@ document.addEventListener('DOMContentLoaded', function() {
let markupForSuborganizationRow = '';
if (!noPortfolioFlag) {
if (portfolioValue) {
markupForSuborganizationRow = `
<td>
<span class="${suborganization ? 'ellipsis ellipsis--30 vertical-align-middle' : ''}" aria-label="${suborganization}" title="${suborganization}">${suborganization}</span>
@ -1485,6 +1484,8 @@ document.addEventListener('DOMContentLoaded', function() {
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
const portfolioElement = document.getElementById('portfolio-js-value');
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
/**
* 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.
@ -1533,7 +1534,7 @@ document.addEventListener('DOMContentLoaded', function() {
* @param {*} scroll - control for the scrollToElement functionality
* @param {*} searchTerm - the search term
*/
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) {
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
// fetch json of page of domain requests, given params
let baseUrl = document.getElementById("get_domain_requests_json_url");
if (!baseUrl) {
@ -1545,7 +1546,12 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
fetch(`${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
// fetch json of page of requests, given params
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`
if (portfolio)
url += `&portfolio=${portfolio}`
fetch(url)
.then(response => response.json())
.then(data => {
if (data.error) {
@ -1601,10 +1607,11 @@ document.addEventListener('DOMContentLoaded', function() {
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>`;
// Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed
// Delete markup will either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page)
// Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed
let modalTrigger = '';
// If the request is deletable, create modal body and insert it
// If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages
if (request.is_deletable) {
let modalHeading = '';
let modalDescription = '';
@ -1692,8 +1699,45 @@ document.addEventListener('DOMContentLoaded', function() {
`
domainRequestsSectionWrapper.appendChild(modal);
// Request is deletable, modal and modalTrigger are built. Now test is portfolio requests page and enhace the modalTrigger markup
if (portfolioValue) {
modalTrigger = `
<div class="usa-accordion usa-accordion--more-actions margin-right-2">
<div class="usa-accordion__heading">
<button
type="button"
class="usa-button usa-button--unstyled usa-accordion__button usa-button--more-actions"
aria-expanded="false"
aria-controls="more-actions-${request.id}"
>
<svg class="usa-icon top-2px" 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-${request.id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0" hidden>
<h2>More options</h2>
<a
role="button"
id="button-toggle-delete-domain-alert-${request.id}"
href="#toggle-delete-domain-alert-${request.id}"
class="usa-button--unstyled text-no-underline late-loading-modal-trigger"
aria-controls="toggle-delete-domain-alert-${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>
</div>
</div>
`
}
}
const row = document.createElement('tr');
row.innerHTML = `
<th scope="row" role="rowheader" data-label="Domain name">
@ -1817,6 +1861,36 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
function closeMoreActionMenu(accordionIsOpen) {
if (accordionIsOpen.getAttribute("aria-expanded") === "true") {
accordionIsOpen.click();
}
}
document.addEventListener('focusin', function(event) {
const accordions = document.querySelectorAll('.usa-accordion--more-actions');
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
openAccordions.forEach((openAccordionButton) => {
const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); // Find the corresponding accordion
if (accordion && !accordion.contains(event.target)) {
closeMoreActionMenu(openAccordionButton); // Close the accordion if the focus is outside
}
});
});
document.addEventListener('click', function(event) {
const accordions = document.querySelectorAll('.usa-accordion--more-actions');
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
openAccordions.forEach((openAccordionButton) => {
const accordion = openAccordionButton.closest('.usa-accordion--more-actions'); // Find the corresponding accordion
if (accordion && !accordion.contains(event.target)) {
closeMoreActionMenu(openAccordionButton); // Close the accordion if the click is outside
}
});
});
// Initial load
loadDomainRequests(1);
}

View file

@ -1,6 +1,7 @@
@use "uswds-core" as *;
.usa-accordion--select {
.usa-accordion--select,
.usa-accordion--more-actions {
display: inline-block;
width: auto;
position: relative;

View file

@ -192,3 +192,8 @@ abbr[title] {
max-width: 50ch;
}
}
// Boost this USWDS utility class for the accordions in the portfolio requests table
.left-auto {
left: auto!important;
}

View file

@ -89,14 +89,16 @@
.usa-nav__primary {
.usa-nav-link,
.usa-nav-link:hover,
.usa-nav-link:active {
.usa-nav-link:active,
button {
color: color('primary');
font-weight: font-weight('normal');
font-size: 16px;
}
.usa-current,
.usa-current:hover,
.usa-current:active {
.usa-current:active,
button.usa-current {
font-weight: font-weight('bold');
}
}

View file

@ -78,6 +78,11 @@ urlpatterns = [
views.PortfolioDomainRequestsView.as_view(),
name="domain-requests",
),
path(
"no-organization-requests/",
views.PortfolioNoDomainRequestsView.as_view(),
name="no-portfolio-requests",
),
path(
"organization/",
views.PortfolioOrganizationView.as_view(),

View file

@ -5,10 +5,13 @@
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
<section class="section--outlined domain-requests" id="domain-requests">
<div class="grid-row">
{% if not has_domain_requests_portfolio_permission %}
{% if not portfolio %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
</div>
{% else %}
<!-- Embedding the portfolio value in a data attribute -->
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
{% endif %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
@ -45,7 +48,7 @@
<thead>
<tr>
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Date submitted</th>
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Submitted</th>
<th data-sortable="status" scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
<!-- AJAX will conditionally add a th for delete actions -->

View file

@ -9,7 +9,6 @@
<div class="section--outlined__header margin-bottom-3 {% if not portfolio %} section--outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
{% if not portfolio %}
<h2 id="domains-header" class="display-inline-block">Domains</h2>
<span class="display-none" id="no-portfolio-js-flag"></span>
{% else %}
<!-- Embedding the portfolio value in a data attribute -->
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>

View file

@ -16,7 +16,7 @@
<li class="usa-nav__primary-item">
{% if has_domains_portfolio_permission %}
{% url 'domains' as url %}
{%else %}
{% else %}
{% url 'no-portfolio-domains' as url %}
{% endif %}
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
@ -29,14 +29,38 @@
</a>
</li>
{% if has_domain_requests_portfolio_permission %}
<li class="usa-nav__primary-item">
<li class="usa-nav__primary-item">
{% if has_domain_requests_portfolio_permission %}
{% url 'domain-requests' as url %}
<button
type="button"
class="usa-accordion__button usa-nav__link{% if 'request'|in_path:request.path %} usa-current{% endif %}"
aria-expanded="false"
aria-controls="basic-nav-section-two"
>
<span>Domain requests</span>
</button>
<ul id="basic-nav-section-two" class="usa-nav__submenu">
<li class="usa-nav__submenu-item">
<a href="{{ url }}"
><span>Domain requests</span></a
>
</li>
<li class="usa-nav__submenu-item">
<a href="{% url 'domain-request:' %}"
><span>Start a new domain request</span></a
>
</li>
</ul>
{% else %}
{% url 'no-portfolio-requests' as url %}
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
Domain requests
</a>
</li>
{% endif %}
{% endif %}
</li>
<li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
Members

View file

@ -0,0 +1,30 @@
{% extends 'portfolio_base.html' %}
{% load static %}
{% block title %} Domain Requests | {% endblock %}
{% block portfolio_content %}
<h1 id="domains-header">Current domain requests</h1>
<section class="section--outlined">
<div class="section--outlined__header margin-bottom-3">
<h2 id="domains-header" class="display-inline-block">You dont have access to domain requests.</h2>
{% if portfolio_administrators %}
<p>If you believe you should have access to a request, reach out to your organizations administrators.</p>
<p>Your organizations administrators:</p>
<ul class="margin-top-0">
{% for administrator in portfolio_administrators %}
{% if administrator.email %}
<li>{{ administrator.email }}</li>
{% else %}
<li>{{ administrator }}</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
<p><strong>No administrators were found on your organization.</strong></p>
<p>If you believe you should have access to a request, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.</p>
{% endif %}
</div>
</section>
{% endblock %}

View file

@ -12,9 +12,9 @@ def get_domain_requests_json(request):
"""Given the current request,
get all domain requests that are associated with the request user and exclude the APPROVED ones"""
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
status=DomainRequest.DomainRequestStatus.APPROVED
)
domain_request_ids = get_domain_requests_ids_from_request(request)
domain_requests = DomainRequest.objects.filter(id__in=domain_request_ids)
unfiltered_total = domain_requests.count()
# Handle sorting
@ -97,3 +97,21 @@ def get_domain_requests_json(request):
"unfiltered_total": unfiltered_total,
}
)
def get_domain_requests_ids_from_request(request):
"""Get domain request ids from request.
If portfolio specified, return domain request ids associated with portfolio.
Otherwise, return domain request ids associated with request.user.
"""
portfolio = request.GET.get("portfolio")
if portfolio:
domain_requests = DomainRequest.objects.filter(portfolio=portfolio).exclude(
status=DomainRequest.DomainRequestStatus.APPROVED
)
else:
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
status=DomainRequest.DomainRequestStatus.APPROVED
)
return domain_requests.values_list("id", flat=True)

View file

@ -42,12 +42,41 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
"""Some users have access to the underlying portfolio, but not any domains.
"""Some users have access to the underlying portfolio, but not any domains.
This is a custom view which explains that to the user - and denotes who to contact.
"""
model = Portfolio
template_name = "no_portfolio_domains.html"
template_name = "portfolio_no_domains.html"
def get(self, request):
return render(request, self.template_name, context=self.get_context_data())
def get_context_data(self, **kwargs):
"""Add additional context data to the template."""
# We can override the base class. This view only needs this item.
context = {}
portfolio = self.request.session.get("portfolio")
if portfolio:
admin_ids = UserPortfolioPermission.objects.filter(
portfolio=portfolio,
roles__overlap=[
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
],
).values_list("user__id", flat=True)
admin_users = User.objects.filter(id__in=admin_ids)
context["portfolio_administrators"] = admin_users
return context
class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, View):
"""Some users have access to the underlying portfolio, but not any domain requests.
This is a custom view which explains that to the user - and denotes who to contact.
"""
model = Portfolio
template_name = "portfolio_no_domain_requests.html"
def get(self, request):
return render(request, self.template_name, context=self.get_context_data())