diff --git a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java index ba203ca9a..00ddde2fb 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java @@ -17,6 +17,7 @@ public enum Permission { CAN_ADMINISTER_FEEDBACK_ANSWER("Administer feedback answers", "Feedback"), CAN_ADMINISTER_FEEDBACK_TEMPLATES("Administer feedback templates", "Feedback"), CAN_SEND_EMAIL("Send email", "Feedback"), + CAN_VIEW_TERMINATED_MEMBERS("Can view the profiles of terminated members", "User Management"), CAN_EDIT_ALL_ORGANIZATION_MEMBERS("Edit all member profiles", "User Management"), CAN_DELETE_ORGANIZATION_MEMBERS("Delete organization members", "User Management"), CAN_CREATE_ORGANIZATION_MEMBERS("Create organization members", "User Management"), diff --git a/web-ui/src/components/kudos/PublicKudosCard.jsx b/web-ui/src/components/kudos/PublicKudosCard.jsx index 465c10108..703a76f70 100644 --- a/web-ui/src/components/kudos/PublicKudosCard.jsx +++ b/web-ui/src/components/kudos/PublicKudosCard.jsx @@ -19,7 +19,10 @@ import { TextField, Link, } from "@mui/material"; -import { selectCsrfToken, selectProfile } from "../../context/selectors"; +import { + selectCsrfToken, + selectActiveOrInactiveProfile, +} from "../../context/selectors"; import { AppContext } from "../../context/AppContext"; import { getAvatarURL } from "../../api/api"; import DateFnsUtils from "@date-io/date-fns"; @@ -50,7 +53,7 @@ const KudosCard = ({ kudos }) => { const { state, dispatch } = useContext(AppContext); const csrf = selectCsrfToken(state); - const sender = selectProfile(state, kudos.senderId); + const sender = selectActiveOrInactiveProfile(state, kudos.senderId); const regexIndexOf = (text, regex, start) => { const indexInSuffix = text.slice(start).search(regex); diff --git a/web-ui/src/components/kudos_card/KudosCard.jsx b/web-ui/src/components/kudos_card/KudosCard.jsx index 798f0d946..b856dc976 100644 --- a/web-ui/src/components/kudos_card/KudosCard.jsx +++ b/web-ui/src/components/kudos_card/KudosCard.jsx @@ -20,7 +20,10 @@ import { FormControlLabel, Checkbox, } from "@mui/material"; -import { selectCsrfToken, selectProfile } from "../../context/selectors"; +import { + selectCsrfToken, + selectActiveOrInactiveProfile, +} from "../../context/selectors"; import MemberSelector from '../member_selector/MemberSelector'; import { AppContext } from "../../context/AppContext"; import { getAvatarURL } from "../../api/api"; @@ -63,7 +66,7 @@ const KudosCard = ({ kudos, includeActions, includeEdit, onKudosAction }) => { const [memberSelectorOpen, setMemberSelectorOpen] = useState(false); const [kudosRecipientMembers, setKudosRecipientMembers] = useState(kudos.recipientMembers); - const sender = selectProfile(state, kudos.senderId); + const sender = selectActiveOrInactiveProfile(state, kudos.senderId); const getRecipientComponent = useCallback(() => { if (kudos.recipientTeam) { diff --git a/web-ui/src/components/kudos_card/KudosCard.test.jsx b/web-ui/src/components/kudos_card/KudosCard.test.jsx new file mode 100644 index 000000000..f411e0441 --- /dev/null +++ b/web-ui/src/components/kudos_card/KudosCard.test.jsx @@ -0,0 +1,157 @@ +import React from 'react'; +import KudosCard from './KudosCard'; +import { AppContextProvider } from '../../context/AppContext'; + +const initialState = { + state: { + csrf: 'O_3eLX2-e05qpS_yOeg1ZVAs9nDhspEi', + teams: [], + userProfile: { + id: "1", + firstName: 'Jimmy', + lastName: 'Johnson', + role: ['MEMBER'], + }, + terminatedMembers: [ + { + id: "5", + firstName: 'Jerry', + lastName: 'Garcia', + name: 'Jerry Garcia', + role: ['MEMBER'], + }, + ], + memberProfiles: [ + { + id: "1", + firstName: 'Jimmy', + lastName: 'Johnson', + name: 'Jimmy Johnson', + role: ['MEMBER'], + }, + { + id: "2", + firstName: 'Jimmy', + lastName: 'Olsen', + name: 'Jimmy Olsen', + role: ['MEMBER'], + }, + { + id: "3", + firstName: 'Clark', + lastName: 'Kent', + name: 'Clark Kent', + role: ['MEMBER'], + }, + { + id: "4", + firstName: 'Kent', + lastName: 'Brockman', + name: 'Kent Brockman', + role: ['MEMBER'], + }, + { + id: "6", + firstName: 'Brock', + lastName: 'Smith', + name: 'Brock Smith', + role: ['MEMBER'], + }, + { + id: "7", + firstName: 'Jimmy', + middleName: 'T.', + lastName: 'Olsen', + name: 'Jimmy T. Olsen', + role: ['MEMBER'], + }, + ], + } +}; + +const terminated = { + id: 'test-terminated-kudos', + message: "Brock and Brockman did a great job helping Clark, Jimmy Olsen, Jimmy T. Olsen, and Johnson", + senderId: "5", + dateCreated: [ 2025, 2, 14 ], + recipientMembers: [ + { + id: "1", + firstName: 'Jimmy', + lastName: 'Johnson', + role: ['MEMBER'], + }, + { + id: "2", + firstName: 'Jimmy', + lastName: 'Olsen', + role: ['MEMBER'], + }, + { + id: "3", + firstName: 'Clark', + lastName: 'Kent', + role: ['MEMBER'], + }, + { + id: "6", + firstName: 'Brock', + lastName: 'Smith', + role: ['MEMBER'], + }, + { + id: "4", + firstName: 'Kent', + lastName: 'Brockman', + role: ['MEMBER'], + }, + { + id: "7", + firstName: 'Jimmy', + middleName: 'T.', + lastName: 'Olsen', + role: ['MEMBER'], + }, + ], +}; + +const kudos = { + id: 'test-kudos', + message: "Jimmy is awesome!", + senderId: "1", + dateCreated: [ 2025, 2, 17 ], + recipientMembers: [ + { + id: "2", + firstName: 'Jimmy', + lastName: 'Olsen', + role: ['MEMBER'], + }, + ], +}; + +it('inactive renders correctly', () => { + snapshot( + + {}} + /> + + ); +}); + +it('active renders correctly', () => { + snapshot( + + {}} + /> + + ); +}); diff --git a/web-ui/src/components/kudos_card/__snapshots__/KudosCard.test.jsx.snap b/web-ui/src/components/kudos_card/__snapshots__/KudosCard.test.jsx.snap new file mode 100644 index 000000000..12d66db3f --- /dev/null +++ b/web-ui/src/components/kudos_card/__snapshots__/KudosCard.test.jsx.snap @@ -0,0 +1,469 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`active renders correctly 1`] = ` +
+
+
+
+
+
+ +
+ + Jimmy Olsen + +
+

+ received kudos from +

+
+
+ +
+ + Jimmy Johnson + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+

+ Jimmy is awesome! +

+
+
+
+
+
+
+`; + +exports[`inactive renders correctly 1`] = ` +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+

+ received kudos from +

+
+
+ +
+ + Jerry Garcia + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+

+ Brock and Brockman did a great job helping Clark, Jimmy Olsen, Jimmy T. Olsen, and Johnson +

+
+

+ Members: +

+
+
+ +
+ + Jimmy Johnson + +
+
+
+ +
+ + Jimmy Olsen + +
+
+
+ +
+ + Clark Kent + +
+
+
+ +
+ + Brock Smith + +
+
+
+ +
+ + Kent Brockman + +
+
+
+ +
+ + Jimmy Olsen + +
+
+
+
+
+
+
+
+`; diff --git a/web-ui/src/context/AppContext.jsx b/web-ui/src/context/AppContext.jsx index 5dd175260..78481c04e 100644 --- a/web-ui/src/context/AppContext.jsx +++ b/web-ui/src/context/AppContext.jsx @@ -23,7 +23,7 @@ import { } from '../api/member'; import { selectCanViewCheckinsPermission, - selectCanEditAllOrganizationMembers, + selectCanViewTerminatedMembers, } from './selectors'; import { getAllRoles, getAllUserRoles } from '../api/roles'; import { getMemberSkills } from '../api/memberskill'; @@ -191,7 +191,7 @@ const AppContextProvider = props => { if (csrf && userProfile && !memberProfiles) { dispatch({ type: UPDATE_PEOPLE_LOADING, payload: true }); getMemberProfiles(); - if (selectCanEditAllOrganizationMembers(state)) { + if (selectCanViewTerminatedMembers(state)) { getTerminatedMembers(); } } diff --git a/web-ui/src/context/selectors.js b/web-ui/src/context/selectors.js index 984f7d43a..27d53c1c1 100644 --- a/web-ui/src/context/selectors.js +++ b/web-ui/src/context/selectors.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; export const selectMemberProfiles = state => state.memberProfiles || []; -export const selectTerminatedMembers = state => state.terminatedMembers; +export const selectTerminatedMembers = state => state.terminatedMembers || []; export const selectMemberSkills = state => state.memberSkills || []; export const selectSkills = state => state.skills || []; export const selectTeamMembers = state => state.teamMembers; @@ -242,6 +242,14 @@ export const selectCanEditAllOrganizationMembers = hasPermission( 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS', ); +export const selectCanViewTerminatedMembers = createSelector( + selectCanEditAllOrganizationMembers, + hasPermission( + 'CAN_VIEW_TERMINATED_MEMBERS' + ), + (canEdit, canView) => canEdit || canView +); + export const selectIsPDL = createSelector( selectUserProfile, userProfile => @@ -327,6 +335,13 @@ export const selectProfile = createSelector( (profileMap, profileId) => profileMap[profileId] ); +export const selectActiveOrInactiveProfile = createSelector( + selectProfileMap, + selectProfileMapForTerminatedMembers, + (state, profileId) => profileId, + (profileMap, termedProfileMap, profileId) => profileMap[profileId] || termedProfileMap[profileId] +); + export const selectSkill = createSelector( selectSkills, (state, skillId) => skillId, diff --git a/web-ui/src/context/selectors.test.js b/web-ui/src/context/selectors.test.js index 0ba8e98cd..8f6007702 100644 --- a/web-ui/src/context/selectors.test.js +++ b/web-ui/src/context/selectors.test.js @@ -20,7 +20,10 @@ import { selectSupervisorHierarchyIds, selectSubordinates, selectIsSubordinateOfCurrentUser, - selectHasReportPermission + selectHasReportPermission, + selectActiveOrInactiveProfile, + selectCanEditAllOrganizationMembers, + selectCanViewTerminatedMembers, } from './selectors'; describe('Selectors', () => { @@ -1525,4 +1528,155 @@ describe('Selectors', () => { expect(selectHasReportPermission(testState)).toBe(false); }); + + it("selectCanEditAllOrganizationMembers should return false when user does not have 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' permission", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanEditAllOrganizationMembers(testState)).toBe(false); + }); + + it("selectCanEditAllOrganizationMembers should return true when user has 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' permission", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanEditAllOrganizationMembers(testState)).toBe(true); + }); + + it("selectCanViewTerminatedMembers should return false when user does not have 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' or 'CAN_VIEW_TERMINATED_MEMBERS' permission", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanViewTerminatedMembers(testState)).toBe(false); + }); + + it("selectCanViewTerminatedMembers should return true when user has 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' or 'CAN_VIEW_TERMINATED_MEMBERS' permissions", () => { + const testState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_EDIT_ALL_ORGANIZATION_MEMBERS' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + const otherTestState = { + userProfile: { + firstName: 'Huey', + lastName: 'Emmerich', + role: 'MEMBER', + permissions: [ + { permission: 'CAN_VIEW_FEEDBACK_REQUEST' }, + { permission: 'CAN_VIEW_TERMINATED_MEMBERS' }, + { permission: 'CAN_VIEW_FEEDBACK_ANSWER' }, + ] + } + }; + + expect(selectCanViewTerminatedMembers(testState)).toBe(true); + expect(selectCanViewTerminatedMembers(otherTestState)).toBe(true); + }); + + it('selectActiveOrInactiveProfile should a profile if active or inactive', () => { + const activeTestMember = { + id: 1, + bioText: 'foo', + employeeId: 11, + name: 'A Person', + firstName: 'A', + lastName: 'PersonA', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + }; + const inactiveTestMember = { + id: 2, + bioText: 'foo', + employeeId: 12, + name: 'B Person', + firstName: 'B', + lastName: 'PersonB', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + terminationDate: [2013, 9, 29], + }; + /** @type MemberProfile[] */ + const testActiveMemberProfiles = [ + activeTestMember, + { + id: 3, + bioText: 'foo', + employeeId: 13, + name: 'C Person', + firstName: 'C', + lastName: 'PersonC', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + } + ]; + /** @type MemberProfile[] */ + const testInactiveMemberProfiles = [ + inactiveTestMember, + { + id: 4, + bioText: 'foo', + employeeId: 13, + name: 'D Person', + firstName: 'D', + lastName: 'PersonD', + location: 'St Louis', + title: 'engineer', + workEmail: 'employee@sample.com', + pdlId: 9, + startDate: [2012, 9, 29], + terminationDate: [2013, 9, 29], + } + ]; + const testState = { + memberProfiles: testActiveMemberProfiles, + terminatedMembers: testInactiveMemberProfiles, + }; + + expect(selectActiveOrInactiveProfile(testState, activeTestMember.id)).toEqual(activeTestMember); + expect(selectActiveOrInactiveProfile(testState, inactiveTestMember.id)).toEqual(inactiveTestMember); + }); });