Skip to content

Commit 3a9c268

Browse files
committed
feat: Add user mentions to incident comments with Quill editor integration
1 parent 610a7ac commit 3a9c268

File tree

16 files changed

+653
-45
lines changed

16 files changed

+653
-45
lines changed

keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { AlertDto } from "@/entities/alerts/model";
3+
import { AlertDto, CommentMentionDto } from "@/entities/alerts/model";
44
import { IncidentDto } from "@/entities/incidents/model";
55
import { useUsers } from "@/entities/users/model/useUsers";
66
import UserAvatar from "@/components/navbar/UserAvatar";
@@ -20,12 +20,13 @@ import { DynamicImageProviderIcon } from "@/components/ui";
2020

2121
// TODO: REFACTOR THIS TO SUPPORT ANY ACTIVITY TYPE, IT'S A MESS!
2222

23-
interface IncidentActivity {
23+
export interface IncidentActivity {
2424
id: string;
2525
type: "comment" | "alert" | "newcomment" | "statuschange" | "assign";
2626
text?: string;
2727
timestamp: string;
2828
initiator?: string | AlertDto;
29+
mentions?: CommentMentionDto[];
2930
}
3031

3132
const ACTION_TYPES = [
@@ -139,6 +140,7 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
139140
? auditEvent.description
140141
: "",
141142
timestamp: auditEvent.timestamp,
143+
mentions: auditEvent.mentions,
142144
} as IncidentActivity;
143145
}) || []
144146
);
Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { IncidentDto } from "@/entities/incidents/model";
2-
import { TextInput, Button } from "@tremor/react";
3-
import { useState, useCallback, useEffect } from "react";
2+
import { Button } from "@tremor/react";
3+
import { useState, useCallback } from "react";
44
import { toast } from "react-toastify";
55
import { KeyedMutator } from "swr";
66
import { useApi } from "@/shared/lib/hooks/useApi";
77
import { showErrorToast } from "@/shared/ui";
88
import { AuditEvent } from "@/entities/alerts/model";
9+
import { useUsers } from "@/entities/users/model/useUsers";
10+
import { IncidentCommentInput, extractTaggedUsers } from "./IncidentCommentInput";
911

12+
/**
13+
* Component for adding comments to an incident with user mention capability
14+
*/
1015
export function IncidentActivityComment({
1116
incident,
1217
mutator,
@@ -15,58 +20,54 @@ export function IncidentActivityComment({
1520
mutator: KeyedMutator<AuditEvent[]>;
1621
}) {
1722
const [comment, setComment] = useState("");
23+
const [taggedUsers, setTaggedUsers] = useState<string[]>([]);
24+
1825
const api = useApi();
19-
26+
27+
const { data: users = [] } = useUsers();
2028
const onSubmit = useCallback(async () => {
2129
try {
30+
const extractedTaggedUsers = extractTaggedUsers(comment);
31+
console.log('Extracted tagged users:', extractedTaggedUsers);
32+
2233
await api.post(`/incidents/${incident.id}/comment`, {
2334
status: incident.status,
2435
comment,
36+
tagged_users: extractedTaggedUsers,
2537
});
2638
toast.success("Comment added!", { position: "top-right" });
2739
setComment("");
40+
setTaggedUsers([]);
2841
mutator();
2942
} catch (error) {
3043
showErrorToast(error, "Failed to add comment");
3144
}
3245
}, [api, incident.id, incident.status, comment, mutator]);
3346

34-
const handleKeyDown = useCallback(
35-
(event: KeyboardEvent) => {
36-
if (
37-
event.key === "Enter" &&
38-
(event.metaKey || event.ctrlKey) &&
39-
comment
40-
) {
41-
onSubmit();
42-
}
43-
},
44-
[onSubmit, comment]
45-
);
46-
47-
useEffect(() => {
48-
window.addEventListener("keydown", handleKeyDown);
49-
return () => {
50-
window.removeEventListener("keydown", handleKeyDown);
51-
};
52-
}, [comment, handleKeyDown]);
53-
5447
return (
55-
<div className="flex h-full w-full relative items-center">
56-
<TextInput
57-
value={comment}
58-
onValueChange={setComment}
59-
placeholder="Add a new comment..."
60-
/>
61-
<Button
62-
color="orange"
63-
variant="secondary"
64-
className="ml-2.5"
65-
disabled={!comment}
66-
onClick={onSubmit}
67-
>
68-
Comment
69-
</Button>
48+
<div className="relative border border-gray-300 rounded-md mb-4">
49+
<div className="flex flex-col p-2.5 gap-2.5">
50+
<div className="w-full">
51+
<IncidentCommentInput
52+
value={comment}
53+
onValueChange={setComment}
54+
users={users}
55+
placeholder="Add a comment..."
56+
className="comment-editor"
57+
/>
58+
</div>
59+
60+
<div className="flex justify-end mt-2">
61+
<Button
62+
color="orange"
63+
variant="secondary"
64+
disabled={!comment}
65+
onClick={onSubmit}
66+
>
67+
Comment
68+
</Button>
69+
</div>
70+
</div>
7071
</div>
7172
);
7273
}

keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityItem.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { AlertSeverity } from "@/entities/alerts/ui";
2-
import { AlertDto } from "@/entities/alerts/model";
2+
import { AlertDto, CommentMentionDto } from "@/entities/alerts/model";
33
import TimeAgo from "react-timeago";
4+
import { useUsers } from "@/entities/users/model/useUsers";
5+
import { User } from "@/app/(keep)/settings/models";
46

57
// TODO: REFACTOR THIS TO SUPPORT ANY ACTIVITY TYPE, IT'S A MESS!
68

79
export function IncidentActivityItem({ activity }: { activity: any }) {
10+
const { data: users = [] } = useUsers();
11+
812
const title =
913
typeof activity.initiator === "string"
1014
? activity.initiator
@@ -17,6 +21,33 @@ export function IncidentActivityItem({ activity }: { activity: any }) {
1721
: activity.initiator?.status === "firing"
1822
? " triggered"
1923
: " resolved" + ". ";
24+
25+
// Process comment text to style mentions if it's a comment with mentions
26+
const processCommentText = (text: string) => {
27+
console.log(activity);
28+
if (!text || activity.type !== 'comment') return text;
29+
30+
// Create a map of email to name for user lookup
31+
const emailToName = new Map();
32+
users.forEach((user: User) => {
33+
if (user.email) {
34+
emailToName.set(user.email, user.name || user.email);
35+
}
36+
});
37+
38+
// If the text contains HTML (from ReactQuill), it's already formatted
39+
if (text.includes('<span class="mention">') || text.includes('<p>')) {
40+
// Sanitize HTML to prevent XSS attacks if needed
41+
// For a production app, consider using a library like DOMPurify
42+
43+
return (
44+
<div className="quill-content" dangerouslySetInnerHTML={{ __html: text }} />
45+
);
46+
}
47+
48+
return text;
49+
};
50+
2051
return (
2152
<div className="relative h-full w-full flex flex-col">
2253
<div className="flex items-center gap-2">
@@ -32,7 +63,9 @@ export function IncidentActivityItem({ activity }: { activity: any }) {
3263
</span>
3364
</div>
3465
{activity.text && (
35-
<div className="font-light text-gray-800">{activity.text}</div>
66+
<div className="font-light text-gray-800">
67+
{processCommentText(activity.text)}
68+
</div>
3669
)}
3770
</div>
3871
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
.quill-editor-container .quill {
2+
display: block;
3+
height: 100%;
4+
}
5+
6+
.quill-editor-container .ql-container {
7+
height: 100%;
8+
font-size: 16px;
9+
border: none;
10+
border-top: 1px solid #ccc;
11+
}
12+
13+
.quill-editor-container .ql-toolbar {
14+
border: none;
15+
border-bottom: 1px solid #eee;
16+
}
17+
18+
.quill-editor-container .ql-editor {
19+
min-height: 100%;
20+
max-height: 100%;
21+
overflow-y: auto;
22+
}
23+
24+
.mention {
25+
background-color: #E8F4FE;
26+
border-radius: 4px;
27+
padding: 0 2px;
28+
color: #0366d6;
29+
}
30+
31+
.mention-container {
32+
display: block !important;
33+
position: absolute !important;
34+
background-color: white;
35+
border: 1px solid #ddd;
36+
border-radius: 4px;
37+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
38+
z-index: 9999 !important;
39+
max-height: 100%;
40+
overflow-y: auto;
41+
padding: 5px 0;
42+
min-width: 180px;
43+
}
44+
45+
.mention-list {
46+
list-style: none;
47+
margin: 0;
48+
padding: 0;
49+
}
50+
51+
.mention-item {
52+
display: block;
53+
padding: 8px 12px;
54+
cursor: pointer;
55+
color: #333;
56+
}
57+
58+
.mention-item:hover {
59+
background-color: #f0f0f0;
60+
}
61+
62+
.mention-item.selected {
63+
background-color: #e8f4fe;
64+
}
65+
66+
/* Prevent hidden overflow that could hide the dropdown */
67+
.ql-editor p {
68+
overflow: visible;
69+
}

0 commit comments

Comments
 (0)