diff --git a/src/components/EmailManagement/template-management/templates/EmailTemplateEditor.module.css b/src/components/EmailManagement/template-management/templates/EmailTemplateEditor.module.css index 46a3c199c6..f947b930e0 100644 --- a/src/components/EmailManagement/template-management/templates/EmailTemplateEditor.module.css +++ b/src/components/EmailManagement/template-management/templates/EmailTemplateEditor.module.css @@ -797,53 +797,3 @@ .email-template-editor .d-flex { align-items: center; } - -/* Dark Mode - Add/Edit Variable Modal */ -:global(body.bm-dashboard-dark) :global(.modal-content) { - background-color: #1a2a3a; - color: #e0e0e0; - border-color: #2e5061; -} - -:global(body.bm-dashboard-dark) :global(.modal-header) { - background-color: #1a2a3a; - border-bottom-color: #2e5061; - color: #e0e0e0; -} - -:global(body.bm-dashboard-dark) :global(.modal-body) { - background-color: #1a2a3a; - color: #e0e0e0; -} - -:global(body.bm-dashboard-dark) :global(.modal-footer) { - background-color: #1a2a3a; - border-top-color: #2e5061; -} - -:global(body.bm-dashboard-dark) :global(.modal-body) :global(.form-control), -:global(body.bm-dashboard-dark) :global(.modal-body) :global(.form-select) { - background-color: #0d1f2d; - color: #e0e0e0; - border-color: #2e5061; -} - -:global(body.bm-dashboard-dark) :global(.modal-body) :global(label), -:global(body.bm-dashboard-dark) :global(.modal-body) :global(.col-form-label) { - color: #e0e0e0; -} - -:global(body.bm-dashboard-dark) :global(.modal-title), -:global(body.bm-dashboard-dark) :global(.modal-header) :global(.btn-close) { - color: #e0e0e0; - filter: invert(1); -} - -/* Dark Mode - Type Selection Modal ListGroup */ -:global(body.bm-dashboard-dark) :global(.list-group-item) { - background-color: #0d1f2d; - color: #e0e0e0; - border-color: #2e5061; -} - - diff --git a/src/components/PermissionsManagement/PermissionChangeLogTable.jsx b/src/components/PermissionsManagement/PermissionChangeLogTable.jsx index 5d3d64a83b..3af59d8c4b 100644 --- a/src/components/PermissionsManagement/PermissionChangeLogTable.jsx +++ b/src/components/PermissionsManagement/PermissionChangeLogTable.jsx @@ -105,6 +105,14 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight = ); }; + + const darkHighLight = shouldHighlight => { + const darkClass = shouldHighlight ? styles['dark-highlight-row'] : styles.darkModeRow; + const lightClass = shouldHighlight ? styles['highlight-row'] : ''; + + return darkMode ? darkClass : lightClass; + }; + return ( <>
@@ -128,7 +136,7 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight = const nameValue = log?.individualName ? formatName(log.individualName) : log.roleName; const shouldHighlight = roleSet.has(normalize(nameValue)); return ( - + {`${formatDate(log.logDateTime)} ${formattedAmPmTime(log.logDateTime)}`} diff --git a/src/components/PermissionsManagement/PermissionChangeLogTable.module.css b/src/components/PermissionsManagement/PermissionChangeLogTable.module.css index a12018d200..f96972cdf2 100644 --- a/src/components/PermissionsManagement/PermissionChangeLogTable.module.css +++ b/src/components/PermissionsManagement/PermissionChangeLogTable.module.css @@ -13,6 +13,20 @@ background-color: #b2ebf2; } +.darkModeRow:hover { + background-color: #1c2541 !important; +} + +.dark-highlight-row { + background-color: #012758; + border-left: 4px solid #007281; + transition: background 0.2s ease; +} + +.dark-highlight-row:hover { + background-color: #12328a; +} + .permission-change-log-table { border-collapse: 'collapse'; width: 100%; diff --git a/src/components/UserProfile/BasicInformationTab/BasicInformationTab.jsx b/src/components/UserProfile/BasicInformationTab/BasicInformationTab.jsx index 898af49697..3d85b630be 100644 --- a/src/components/UserProfile/BasicInformationTab/BasicInformationTab.jsx +++ b/src/components/UserProfile/BasicInformationTab/BasicInformationTab.jsx @@ -20,6 +20,7 @@ import hasPermission from '~/utils/permissions'; import { ENDPOINTS } from '~/utils/URL'; import TimeZoneDropDown from '../TimeZoneDropDown'; import styles from './BasicInformationTab.module.css'; +import RoleChangePermissionsModal from '~/components/UserProfile/RoleChangePermissionsModal'; export const Name = props => { @@ -494,6 +495,7 @@ const BasicInformationTab = props => { const [timeZoneFilter, setTimeZoneFilter] = useState(''); const [desktopDisplay, setDesktopDisplay] = useState(window.innerWidth > 1024); const [errorOccurred, setErrorOccurred] = useState(false); + const [showRolePermsModal, setShowRolePermsModal] = useState(false); const dispatch = useDispatch(); const rolesAllowedToEditStatusFinalDay = ['Administrator', 'Owner']; const canEditStatus = dispatch(hasPermission('interactWithPauseUserButton')); @@ -723,43 +725,16 @@ const BasicInformationTab = props => { {canEditRole ? ( - - - + Manage Role & Permissions + +
+ ) : (

{userProfile.role}

)} @@ -1047,6 +1022,16 @@ const BasicInformationTab = props => { )} + setShowRolePermsModal(false)} + roles={roles} + userProfile={userProfile} + setUserProfile={setUserProfile} + loadUserProfile={loadUserProfile} + desktopDisplay={desktopDisplay} + canAddDeleteEditOwners={canAddDeleteEditOwners} + /> ); }; diff --git a/src/components/UserProfile/BasicInformationTab/BasicInformationTab.module.css b/src/components/UserProfile/BasicInformationTab/BasicInformationTab.module.css index dd63f767e5..b254e2c92a 100644 --- a/src/components/UserProfile/BasicInformationTab/BasicInformationTab.module.css +++ b/src/components/UserProfile/BasicInformationTab/BasicInformationTab.module.css @@ -78,4 +78,8 @@ :global(.bg-yinmn-blue) .dark-label-col { background-color: #1c2541 !important; -} \ No newline at end of file +} + +.darkModeText { + color: white; +} diff --git a/src/components/UserProfile/BasicInformationTab/__tests__/BasicInformationTab.test.jsx b/src/components/UserProfile/BasicInformationTab/__tests__/BasicInformationTab.test.jsx index 6f2fe68e1a..93bd2c51b0 100644 --- a/src/components/UserProfile/BasicInformationTab/__tests__/BasicInformationTab.test.jsx +++ b/src/components/UserProfile/BasicInformationTab/__tests__/BasicInformationTab.test.jsx @@ -368,7 +368,6 @@ let testProps= { handleUserProfile:vi.fn(), roles:['Admin','Owner','Volunteer','Manager'], canEditRole:true, - }; @@ -505,7 +504,7 @@ it('Test case 9: Renders the role component with a combo box when canEditRole , ); expect(screen.getByText("Role")).toBeInTheDocument(); // Label - expect(screen.getByRole('combobox')).toBeInTheDocument(); // combo box + expect(screen.getByRole('button', { name: 'Manage Role & Permissions' })).toBeInTheDocument(); // Role Modal button }); it('Test case 10 : Does not render a combo box for role component when canEditRole is false', () => { testProps.canEditRole=false; diff --git a/src/components/UserProfile/RoleChangePermissionsModal.jsx b/src/components/UserProfile/RoleChangePermissionsModal.jsx new file mode 100644 index 0000000000..4ebf00ac45 --- /dev/null +++ b/src/components/UserProfile/RoleChangePermissionsModal.jsx @@ -0,0 +1,287 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Input } from 'reactstrap'; +import { connect, useSelector } from 'react-redux'; +import axios from 'axios'; +import { toast } from 'react-toastify'; +import { ENDPOINTS } from '~/utils/URL'; +import { boxStyle, boxStyleDark } from '~/styles'; +import { permissionLabelKeyMappingObj } from '../PermissionsManagement/PermissionsConst'; +import EditableInfoModal from '../UserProfile/EditableModal/EditableInfoModal'; +import PropTypes from 'prop-types'; +import styles from './BasicInformationTab/BasicInformationTab.module.css'; + +/** + * RoleChangePermissionsModal + * - Allows admins to switch a user's role and adjust custom permission overrides in one flow + */ +function RoleChangePermissionsModal(props) { + const { + isOpen, + onClose, + roles = [], + userProfile, + loadUserProfile, + authRole, + desktopDisplay, + canAddDeleteEditOwners, + } = props; + const darkMode = useSelector(state => state.theme.darkMode); + + const roleNameToDefaults = useMemo(() => { + const map = {}; + roles.forEach(r => { + map[r.roleName] = r.permissions || []; + }); + return map; + }, [roles]); + + const initialSelectedRole = userProfile?.role || ''; + const initialCustomPerms = userProfile?.permissions?.frontPermissions || []; + const initialRemovedDefaults = userProfile?.permissions?.removedDefaultPermissions || []; + + const [selectedRole, setSelectedRole] = useState(initialSelectedRole); + const [userCustomPermissions, setUserCustomPermissions] = useState(initialCustomPerms); + const [removedDefaultsByRole, setRemovedDefaultsByRole] = useState({}); + const [saving, setSaving] = useState(false); + + useEffect(() => { + setSelectedRole(initialSelectedRole); + setUserCustomPermissions(initialCustomPerms); + setRemovedDefaultsByRole(prev => ({ + ...prev, + [initialSelectedRole]: initialRemovedDefaults, + })); + }, [isOpen]); + + const getRemovedDefaults = roleName => removedDefaultsByRole[roleName] || initialRemovedDefaults; + const [keptFrontPermissions, setKeptFrontPermissions] = useState([]); + const [keptRemovedPermissions, setKeptRemovedPermissions] = useState([]); + + const checked = (permissions, permission) => { + permissions(permissionArray => + permissionArray.includes(permission) + ? permissionArray.filter(perm => perm !== permission) + : [...permissionArray, permission] + ); + } + + const handleConfirm = async () => { + try { + setSaving(true); + const userId = userProfile?._id; + if (!userId) { + toast.error('Missing user id'); + setSaving(false); + return; + } + + // Build updated profile payload + const updated = { + ...userProfile, + role: selectedRole, + permissions: { + frontPermissions: keptFrontPermissions, + removedDefaultPermissions: keptRemovedPermissions, + defaultPermissions: roleNameToDefaults[selectedRole], + }, + }; + + const url = ENDPOINTS.USER_PROFILE(userId); + await axios.put(url, updated); + + const permissionURL = `${ENDPOINTS.PERMISSION_MANAGEMENT_UPDATE()}/user/${userId}`; + const requestor = authRole; + + // Ensures a change log with reason and user's modified permissions when their role is changed + const permissionData = { + reason: `Role Changed to **${selectedRole}**.`, + permissions: updated.permissions, + requestor: requestor, + }; + + await axios.patch(permissionURL, permissionData) + + loadUserProfile(); + toast.success('Your changes have been saved, you can verify it in Permissions Management'); + onClose(); + } catch (err) { + const errorData = `: ${err.response.data}` + toast.error(`Failed to update role/permissions${err?.response?.data ? errorData : ''}`); + } finally { + setSaving(false); + } + }; + + const boxStyling = darkMode ? boxStyleDark : boxStyle; + + const updateSelectedRole = (newRole) => { + const selectedRole2 = roles.some(r => r.roleName === newRole) + if(!selectedRole2) return + const roleDefaults = roleNameToDefaults[newRole] || []; + + setSelectedRole(newRole); + setKeptFrontPermissions(keptFrontPermissions.filter(perm => roleDefaults.includes(perm))); + setKeptRemovedPermissions(keptRemovedPermissions.filter(perm => !roleDefaults.includes(perm))); + } + + const selectedRoleToDisplay = () => { + const selectedRole2 = roles.find(r => r.roleName === selectedRole) + if(!selectedRole2) return + const roleDefaults = roleNameToDefaults[selectedRole2.roleName] || []; + const removedDefaults = getRemovedDefaults(selectedRole2.roleName) + return ( +
+
+ {userProfile?.role === selectedRole &&

User's Current Role

} + {userProfile?.role !== selectedRole &&
Changing User's Role from {userProfile?.role} to {selectedRole}
} +
+
+ These permissions were modified and do not match the new role's. + Which of these permissions do you wish to keep? +
+
+ {userCustomPermissions + .some(perms => { + return !roleDefaults.includes(perms) + }) + && +
+

+

Added Permissions:

+ These pemissions are NOT normally part of this new role, and will remain added if you check the boxes and click confirm +
+ } + {userCustomPermissions + .filter(perms => { + return !roleDefaults.includes(perms) + }) + .map(perm => { + return( +

+ checked(setKeptFrontPermissions, perm)} + > {permissionLabelKeyMappingObj?.[perm]} +

+ ); + })} + {removedDefaults + .some(perms => { + return roleDefaults.includes(perms) + }) + && +
+

+

Removed Permissions:

+ These pemissions ARE normally part of the new role, and will remain removed if you check the boxes and click confirm +
+ } + {removedDefaults + .filter(perms => { + return roleDefaults.includes(perms) + }) + .map(perm => { + return( +

+ checked(setKeptRemovedPermissions, perm)} + > {permissionLabelKeyMappingObj?.[perm]} +

+ ); + })} +
+
+ ); + } + + return ( + + +
+ Manage Role & Permissions + +
+
+ +
+ + updateSelectedRole(e.target.value)} + className={darkMode ? 'bg-darkmode-liblack border-0 text-light' : ''} + > + {canAddDeleteEditOwners && ( + + )} + {(roles || []) + .map(r => (typeof r === 'string' ? r : r.roleName)) // normalize + .filter(Boolean) + .map(roleName => { + if (roleName === 'Owner') return null; // skip Owner in this list + return ( + + ); + })} + +
+ +
+ {isOpen && selectedRoleToDisplay()} +
+
+ + + + +
+ ); +} + +RoleChangePermissionsModal.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func.isRequired, + roles: PropTypes.arrayOf( + PropTypes.shape({ + roleName: PropTypes.string, + }) + ), + userProfile: PropTypes.shape({ + role: PropTypes.string, + permissions: PropTypes.shape({ + frontPermissions: PropTypes.array, + removedDefaultPermissions: PropTypes.array, + }), + _id: PropTypes.string, + }).isRequired, + loadUserProfile: PropTypes.func.isRequired, + authRole: PropTypes.string, + desktopDisplay: PropTypes.bool, + canAddDeleteEditOwners: PropTypes.bool, +} + +const mapStateToProps = state => ({ + authRole: state.auth.user.role || {}, +}) + +export default connect(mapStateToProps, null)(RoleChangePermissionsModal); \ No newline at end of file