Skip to content

Commit 9488fc5

Browse files
committed
Search for names in kudos text and link any that are unambiguous.
1 parent 8aa3f26 commit 9488fc5

File tree

3 files changed

+322
-1
lines changed

3 files changed

+322
-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 getRecipientComponent = useCallback(() => {
55129
if (kudos.recipientTeam) {
56130
return (
@@ -98,7 +172,9 @@ const KudosCard = ({ kudos }) => {
98172
subheaderTypographyProps={{variant:"subtitle1"}}
99173
/>
100174
<CardContent>
101-
<Typography variant="body1"><em>{kudos.message}</em></Typography>
175+
<Typography variant="body1">
176+
{linkNames(kudos)}
177+
</Typography>
102178
{kudos.recipientTeam && (
103179
<AvatarGroup max={12}>
104180
{kudos.recipientMembers.map((member) => (
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: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
+3
21+
</div>
22+
<div
23+
aria-label="Clark Kent"
24+
class="MuiAvatar-root MuiAvatar-circular MuiAvatarGroup-avatar css-1wlk0hk-MuiAvatar-root"
25+
data-mui-internal-clone-element="true"
26+
>
27+
<img
28+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
29+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
30+
/>
31+
</div>
32+
<div
33+
aria-label="Jimmy Olsen"
34+
class="MuiAvatar-root MuiAvatar-circular MuiAvatarGroup-avatar css-1wlk0hk-MuiAvatar-root"
35+
data-mui-internal-clone-element="true"
36+
>
37+
<img
38+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
39+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
40+
/>
41+
</div>
42+
<div
43+
aria-label="Jimmy Johnson"
44+
class="MuiAvatar-root MuiAvatar-circular MuiAvatarGroup-avatar css-1wlk0hk-MuiAvatar-root"
45+
data-mui-internal-clone-element="true"
46+
>
47+
<img
48+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
49+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
50+
/>
51+
</div>
52+
</div>
53+
</div>
54+
<div
55+
class="MuiCardHeader-content css-1qbkelo-MuiCardHeader-content"
56+
>
57+
<span
58+
class="MuiTypography-root MuiTypography-h5 MuiCardHeader-title css-1qvr50w-MuiTypography-root"
59+
>
60+
Kudos!
61+
</span>
62+
<span
63+
class="MuiTypography-root MuiTypography-subtitle1 MuiCardHeader-subheader css-1s216c8-MuiTypography-root"
64+
>
65+
from
66+
<div
67+
class="MuiChip-root MuiChip-filled MuiChip-sizeSmall MuiChip-colorDefault MuiChip-filledDefault css-13htn8f-MuiChip-root"
68+
>
69+
<div
70+
class="MuiAvatar-root MuiAvatar-circular MuiChip-avatar MuiChip-avatarSmall MuiChip-avatarColorDefault css-1wlk0hk-MuiAvatar-root"
71+
>
72+
<img
73+
class="MuiAvatar-img css-1pqm26d-MuiAvatar-img"
74+
src="http://localhost:8080/services/member-profiles/member-photos/undefined"
75+
/>
76+
</div>
77+
<span
78+
class="MuiChip-label MuiChip-labelSmall css-wjsjww-MuiChip-label"
79+
/>
80+
</div>
81+
</span>
82+
</div>
83+
</div>
84+
<div
85+
class="MuiCardContent-root css-46bh2p-MuiCardContent-root"
86+
>
87+
<p
88+
class="MuiTypography-root MuiTypography-body1 css-ahj2mt-MuiTypography-root"
89+
>
90+
<a
91+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
92+
href="/profile/6"
93+
>
94+
Brock
95+
</a>
96+
and
97+
<a
98+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
99+
href="/profile/4"
100+
>
101+
Brockman
102+
</a>
103+
did a great job helping
104+
<a
105+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
106+
href="/profile/3"
107+
>
108+
Clark
109+
</a>
110+
, Jimmy Olsen,
111+
<a
112+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
113+
href="/profile/7"
114+
>
115+
Jimmy T. Olsen
116+
</a>
117+
, and
118+
<a
119+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1xylxj1-MuiTypography-root-MuiLink-root"
120+
href="/profile/1"
121+
>
122+
Johnson
123+
</a>
124+
</p>
125+
</div>
126+
</div>
127+
</div>
128+
`;

0 commit comments

Comments
 (0)