Skip to content

Commit af4a92d

Browse files
authored
Merge pull request #2886 from objectcomputing/feature-2875/link-recipient-names
Search for names in kudos text and link any that are unambiguous.
2 parents 1e60cbd + 32b36be commit af4a92d

File tree

3 files changed

+328
-1
lines changed

3 files changed

+328
-1
lines changed

web-ui/src/components/kudos/PublicKudosCard.jsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
DialogContentText,
1818
DialogActions,
1919
TextField,
20+
Link,
2021
} from "@mui/material";
2122
import { selectCsrfToken, selectProfile } from "../../context/selectors";
2223
import { AppContext } from "../../context/AppContext";
@@ -51,6 +52,79 @@ const KudosCard = ({ kudos }) => {
5152

5253
const sender = selectProfile(state, kudos.senderId);
5354

55+
const regexIndexOf = (text, regex, start) => {
56+
const indexInSuffix = text.slice(start).search(regex);
57+
return indexInSuffix < 0 ? indexInSuffix : indexInSuffix + start;
58+
};
59+
60+
const linkMember = (member, name, message) => {
61+
const components = [];
62+
let index = 0;
63+
do {
64+
index = regexIndexOf(message,
65+
new RegExp('\\b' + name + '\\b', 'i'), index);
66+
if (index != -1) {
67+
const link = <Link key={`${member.id}-${index}`}
68+
href={`/profile/${member.id}`}>
69+
{name}
70+
</Link>;
71+
if (index > 0) {
72+
components.push(message.slice(0, index));
73+
}
74+
components.push(link);
75+
message = message.slice(index + name.length);
76+
}
77+
} while(index != -1);
78+
components.push(message);
79+
return components;
80+
};
81+
82+
const searchNames = (member, members) => {
83+
const names = [];
84+
if (member.middleName) {
85+
names.push(`${member.firstName} ${member.middleName} ${member.lastName}`);
86+
}
87+
const firstAndLast = `${member.firstName} ${member.lastName}`;
88+
if (!members.some((k) => k.id != member.id &&
89+
firstAndLast != `${k.firstName} ${k.lastName}`)) {
90+
names.push(firstAndLast);
91+
}
92+
if (!members.some((k) => k.id != member.id &&
93+
(member.lastName == k.lastName ||
94+
member.lastName == k.firstName))) {
95+
// If there are no other recipients with a name that contains this
96+
// member's last name, we can replace based on that.
97+
names.push(member.lastName);
98+
}
99+
if (!members.some((k) => k.id != member.id &&
100+
(member.firstName == k.lastName ||
101+
member.firstName == k.firstName))) {
102+
// If there are no other recipients with a name that contains this
103+
// member's first name, we can replace based on that.
104+
names.push(member.firstName);
105+
}
106+
return names;
107+
};
108+
109+
const linkNames = (kudos) => {
110+
const components = [ kudos.message ];
111+
for (let member of kudos.recipientMembers) {
112+
const names = searchNames(member, kudos.recipientMembers);
113+
for (let name of names) {
114+
for (let i = 0; i < components.length; i++) {
115+
const component = components[i];
116+
if (typeof(component) === "string") {
117+
const built = linkMember(member, name, component);
118+
if (built.length > 1) {
119+
components.splice(i, 1, ...built);
120+
}
121+
}
122+
}
123+
}
124+
}
125+
return components;
126+
};
127+
54128
const multiTooltip = (num, list) => {
55129
let tooltip = "";
56130
let prefix = "";
@@ -112,7 +186,9 @@ const KudosCard = ({ kudos }) => {
112186
subheaderTypographyProps={{variant:"subtitle1"}}
113187
/>
114188
<CardContent>
115-
<Typography variant="body1"><em>{kudos.message}</em></Typography>
189+
<Typography variant="body1">
190+
{linkNames(kudos)}
191+
</Typography>
116192
{kudos.recipientTeam && (
117193
<AvatarGroup max={12}
118194
renderSurplus={(extra) => multiTooltip(
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React from 'react';
2+
import PublicKudosCard from './PublicKudosCard';
3+
import { AppContextProvider } from '../../context/AppContext';
4+
5+
const initialState = {
6+
state: {
7+
csrf: 'O_3eLX2-e05qpS_yOeg1ZVAs9nDhspEi',
8+
teams: [],
9+
userProfile: {
10+
id: "1",
11+
firstName: 'Jimmy',
12+
lastName: 'Johnson',
13+
role: ['MEMBER'],
14+
},
15+
memberProfiles: [
16+
{
17+
id: "1",
18+
firstName: 'Jimmy',
19+
lastName: 'Johnson',
20+
role: ['MEMBER'],
21+
},
22+
{
23+
id: "2",
24+
firstName: 'Jimmy',
25+
lastName: 'Olsen',
26+
role: ['MEMBER'],
27+
},
28+
{
29+
id: "3",
30+
firstName: 'Clark',
31+
lastName: 'Kent',
32+
role: ['MEMBER'],
33+
},
34+
{
35+
id: "4",
36+
firstName: 'Kent',
37+
lastName: 'Brockman',
38+
role: ['MEMBER'],
39+
},
40+
{
41+
id: "5",
42+
firstName: 'Jerry',
43+
lastName: 'Garcia',
44+
role: ['MEMBER'],
45+
},
46+
{
47+
id: "6",
48+
firstName: 'Brock',
49+
lastName: 'Smith',
50+
role: ['MEMBER'],
51+
},
52+
{
53+
id: "7",
54+
firstName: 'Jimmy',
55+
middleName: 'T.',
56+
lastName: 'Olsen',
57+
role: ['MEMBER'],
58+
},
59+
],
60+
}
61+
};
62+
63+
const kudos = {
64+
id: 'test-kudos',
65+
message: "Brock and Brockman did a great job helping Clark, Jimmy Olsen, Jimmy T. Olsen, and Johnson",
66+
senderId: "5",
67+
dateCreated: [ 2025, 2, 14 ],
68+
recipientMembers: [
69+
{
70+
id: "1",
71+
firstName: 'Jimmy',
72+
lastName: 'Johnson',
73+
role: ['MEMBER'],
74+
},
75+
{
76+
id: "2",
77+
firstName: 'Jimmy',
78+
lastName: 'Olsen',
79+
role: ['MEMBER'],
80+
},
81+
{
82+
id: "3",
83+
firstName: 'Clark',
84+
lastName: 'Kent',
85+
role: ['MEMBER'],
86+
},
87+
{
88+
id: "6",
89+
firstName: 'Brock',
90+
lastName: 'Smith',
91+
role: ['MEMBER'],
92+
},
93+
{
94+
id: "4",
95+
firstName: 'Kent',
96+
lastName: 'Brockman',
97+
role: ['MEMBER'],
98+
},
99+
{
100+
id: "7",
101+
firstName: 'Jimmy',
102+
middleName: 'T.',
103+
lastName: 'Olsen',
104+
role: ['MEMBER'],
105+
},
106+
],
107+
};
108+
109+
it('renders correctly', () => {
110+
snapshot(
111+
<AppContextProvider value={initialState}>
112+
<PublicKudosCard
113+
kudos={kudos}
114+
/>
115+
</AppContextProvider>
116+
);
117+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`renders correctly 1`] = `
4+
<div>
5+
<div
6+
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation1 MuiCard-root kudos-card css-bhp9pd-MuiPaper-root-MuiCard-root"
7+
>
8+
<div
9+
class="MuiCardHeader-root css-185gdzj-MuiCardHeader-root"
10+
>
11+
<div
12+
class="MuiCardHeader-avatar css-1ssile9-MuiCardHeader-avatar"
13+
>
14+
<div
15+
class="MuiAvatarGroup-root css-1ytufz-MuiAvatarGroup-root"
16+
>
17+
<div
18+
class="MuiAvatar-root MuiAvatar-circular MuiAvatar-colorDefault MuiAvatarGroup-avatar css-17o22dy-MuiAvatar-root"
19+
>
20+
<p
21+
aria-label="Brock Smith, Kent Brockman, Jimmy Olsen"
22+
class="MuiTypography-root MuiTypography-body1 css-ahj2mt-MuiTypography-root"
23+
data-mui-internal-clone-element="true"
24+
>
25+
+3
26+
</p>
27+
</div>
28+
<div
29+
aria-label="Clark Kent"
30+
class="MuiAvatar-root MuiAvatar-circular MuiAvatarGroup-avatar css-1wlk0hk-MuiAvatar-root"
31+
data-mui-internal-clone-element="true"
32+
>
33+
<img
34+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
35+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
36+
/>
37+
</div>
38+
<div
39+
aria-label="Jimmy Olsen"
40+
class="MuiAvatar-root MuiAvatar-circular MuiAvatarGroup-avatar css-1wlk0hk-MuiAvatar-root"
41+
data-mui-internal-clone-element="true"
42+
>
43+
<img
44+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
45+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
46+
/>
47+
</div>
48+
<div
49+
aria-label="Jimmy Johnson"
50+
class="MuiAvatar-root MuiAvatar-circular MuiAvatarGroup-avatar css-1wlk0hk-MuiAvatar-root"
51+
data-mui-internal-clone-element="true"
52+
>
53+
<img
54+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
55+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
56+
/>
57+
</div>
58+
</div>
59+
</div>
60+
<div
61+
class="MuiCardHeader-content css-1qbkelo-MuiCardHeader-content"
62+
>
63+
<span
64+
class="MuiTypography-root MuiTypography-h5 MuiCardHeader-title css-1qvr50w-MuiTypography-root"
65+
>
66+
Kudos!
67+
</span>
68+
<span
69+
class="MuiTypography-root MuiTypography-subtitle1 MuiCardHeader-subheader css-1s216c8-MuiTypography-root"
70+
>
71+
from
72+
<div
73+
class="MuiChip-root MuiChip-filled MuiChip-sizeSmall MuiChip-colorDefault MuiChip-filledDefault css-13htn8f-MuiChip-root"
74+
>
75+
<div
76+
class="MuiAvatar-root MuiAvatar-circular MuiChip-avatar MuiChip-avatarSmall MuiChip-avatarColorDefault css-1wlk0hk-MuiAvatar-root"
77+
>
78+
<img
79+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
80+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
81+
/>
82+
</div>
83+
<span
84+
class="MuiChip-label MuiChip-labelSmall css-wjsjww-MuiChip-label"
85+
/>
86+
</div>
87+
</span>
88+
</div>
89+
</div>
90+
<div
91+
class="MuiCardContent-root css-46bh2p-MuiCardContent-root"
92+
>
93+
<p
94+
class="MuiTypography-root MuiTypography-body1 css-ahj2mt-MuiTypography-root"
95+
>
96+
<a
97+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
98+
href="/profile/6"
99+
>
100+
Brock
101+
</a>
102+
and
103+
<a
104+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
105+
href="/profile/4"
106+
>
107+
Brockman
108+
</a>
109+
did a great job helping
110+
<a
111+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
112+
href="/profile/3"
113+
>
114+
Clark
115+
</a>
116+
, Jimmy Olsen,
117+
<a
118+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
119+
href="/profile/7"
120+
>
121+
Jimmy T. Olsen
122+
</a>
123+
, and
124+
<a
125+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
126+
href="/profile/1"
127+
>
128+
Johnson
129+
</a>
130+
</p>
131+
</div>
132+
</div>
133+
</div>
134+
`;

0 commit comments

Comments
 (0)