diff --git a/src/registrar/assets/src/js/getgov/main.js b/src/registrar/assets/src/js/getgov/main.js
index 490874220..a496b76f3 100644
--- a/src/registrar/assets/src/js/getgov/main.js
+++ b/src/registrar/assets/src/js/getgov/main.js
@@ -11,7 +11,7 @@ import { initDomainRequestsTable } from './table-domain-requests.js';
import { initMembersTable } from './table-members.js';
import { initMemberDomainsTable } from './table-member-domains.js';
import { initEditMemberDomainsTable } from './table-edit-member-domains.js';
-import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js';
+import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPage } from './portfolio-member-page.js';
import { initDomainRequestForm } from './domain-request-form.js';
import { initDomainManagersPage } from './domain-managers.js';
import { initDomainDNSSEC } from './domain-dnssec.js';
@@ -56,8 +56,10 @@ initDomainDNSSEC();
initFormErrorHandling();
+// Init the portfolio member page
+initPortfolioMemberPage();
+
// Init the portfolio new member page
-initPortfolioMemberPageRadio();
initPortfolioNewMemberPageToggle();
initAddNewMemberPageListeners();
diff --git a/src/registrar/assets/src/js/getgov/portfolio-member-page.js b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
index 96961e5dc..faf2c7b18 100644
--- a/src/registrar/assets/src/js/getgov/portfolio-member-page.js
+++ b/src/registrar/assets/src/js/getgov/portfolio-member-page.js
@@ -193,10 +193,14 @@ export function initAddNewMemberPageListeners() {
}
// Initalize the radio for the member pages
-export function initPortfolioMemberPageRadio() {
+export function initPortfolioMemberPage() {
document.addEventListener("DOMContentLoaded", () => {
let memberForm = document.getElementById("member_form");
- let newMemberForm = document.getElementById("add_member_form")
+ let newMemberForm = document.getElementById("add_member_form");
+ let editSelfWarningModal = document.getElementById("toggle-member-permissions-edit-self");
+ let editSelfWarningModalConfirm = document.getElementById("member-permissions-edit-self");
+
+ // Init the radio
if (memberForm || newMemberForm) {
hookupRadioTogglerListener(
'role',
@@ -206,5 +210,36 @@ export function initPortfolioMemberPageRadio() {
}
);
}
+
+ // Init the "edit self" warning modal, which triggers when the user is trying to edit themselves.
+ // The dom will include these elements when this occurs.
+ // NOTE: This logic does not trigger when the user is the ONLY admin in the portfolio.
+ // This is because info alerts are used rather than modals in this case.
+ if (memberForm && editSelfWarningModal) {
+ // Only show the warning modal when the user is changing their ROLE.
+ var canSubmit = document.querySelector(`input[name="role"]:checked`)?.value != "organization_member";
+ let radioButtons = document.querySelectorAll(`input[name="role"]`);
+ radioButtons.forEach(function (radioButton) {
+ radioButton.addEventListener("change", function() {
+ let selectedValue = radioButton.checked ? radioButton.value : null;
+ canSubmit = selectedValue != "organization_member";
+ });
+ });
+
+ // Prevent form submission assuming org member is selected for role, and open the modal.
+ memberForm.addEventListener("submit", function(e) {
+ if (!canSubmit) {
+ e.preventDefault();
+ editSelfWarningModal.click();
+ }
+ });
+
+ // Hook the confirm button on the modal to form submission.
+ editSelfWarningModalConfirm.addEventListener("click", function() {
+ canSubmit = true;
+ memberForm.submit();
+ });
+ }
});
+
}
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index db1f58d88..43a391ae0 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -411,6 +411,27 @@ class PortfolioMemberForm(BasePortfolioMemberForm):
model = UserPortfolioPermission
fields = ["roles", "additional_permissions"]
+ def clean(self):
+ """
+ Override of clean to ensure that the user isn't removing themselves
+ if they're the only portfolio admin
+ """
+ super().clean()
+ role = self.cleaned_data.get("role")
+ if self.instance and hasattr(self.instance, "user") and hasattr(self.instance, "portfolio"):
+ if role and self.instance.user.is_only_admin_of_portfolio(self.instance.portfolio):
+ # This is how you associate a validation error to a particular field.
+ # The alternative is to do this in clean_role, but execution order matters.
+ raise forms.ValidationError(
+ {
+ "role": forms.ValidationError(
+ "You can't change your member access because you're "
+ "the only admin for this organization. "
+ "To change your access, you'll need to add another admin."
+ )
+ }
+ )
+
class PortfolioInvitedMemberForm(BasePortfolioMemberForm):
"""
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index d5476ab9a..c59ab376b 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -474,7 +474,7 @@ class User(AbstractUser):
admin_count = admins.count()
# Check if the current user is in the list of admins
- if admin_count == 1 and admins.first().user == self:
+ if admin_count == 1 and admins.first() and admins.first().user == self:
return True # The user is the only admin
# If there are other admins or the user is not the only one
diff --git a/src/registrar/templates/portfolio_member.html b/src/registrar/templates/portfolio_member.html
index 1b9ffd653..1afe96161 100644
--- a/src/registrar/templates/portfolio_member.html
+++ b/src/registrar/templates/portfolio_member.html
@@ -98,6 +98,18 @@ Organization member
{% if portfolio_permission %}
+ {% if member and member.id == request.user.id and is_only_admin %}
+
+
+
+ You're the only admin for this organization.
+ Organizations must have at least one admin.
+ To remove yourself or change your member access,
+ you'll need to add another admin.
+
+
+
+ {% endif %}
{% include "includes/summary_item.html" with title='Member access and permissions' permissions=True value=portfolio_permission edit_link=edit_url editable=has_edit_members_portfolio_permission %}
{% elif portfolio_invitation %}
{% include "includes/summary_item.html" with title='Member access and permissions' permissions=True value=portfolio_invitation edit_link=edit_url editable=has_edit_members_portfolio_permission %}
diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html
index e5ae5864e..59a8b1fd8 100644
--- a/src/registrar/templates/portfolio_member_permissions.html
+++ b/src/registrar/templates/portfolio_member_permissions.html
@@ -75,11 +75,25 @@
+