Skip to content

Commit 9d11446

Browse files
committed
feat: implement inline editing for comments
1 parent 13135ec commit 9d11446

File tree

5 files changed

+111
-15
lines changed

5 files changed

+111
-15
lines changed

surfsense_web/components/chat-comments/comment-composer/comment-composer.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ export function CommentComposer({
8686
onSubmit,
8787
onCancel,
8888
autoFocus = false,
89+
initialValue = "",
8990
}: CommentComposerProps) {
90-
const [displayContent, setDisplayContent] = useState("");
91+
const [displayContent, setDisplayContent] = useState(initialValue);
9192
const [insertedMentions, setInsertedMentions] = useState<InsertedMention[]>([]);
93+
const [mentionsInitialized, setMentionsInitialized] = useState(false);
9294
const [mentionState, setMentionState] = useState<MentionState>({
9395
isActive: false,
9496
query: "",
@@ -200,6 +202,33 @@ export function CommentComposer({
200202
setInsertedMentions([]);
201203
};
202204

205+
// Pre-populate insertedMentions from initialValue when members are loaded
206+
useEffect(() => {
207+
if (mentionsInitialized || !initialValue || members.length === 0) return;
208+
209+
const mentionPattern = /@([^\s@]+(?:\s+[^\s@]+)*?)(?=\s|$|[.,!?;:]|@)/g;
210+
const foundMentions: InsertedMention[] = [];
211+
let match: RegExpExecArray | null;
212+
213+
while ((match = mentionPattern.exec(initialValue)) !== null) {
214+
const displayName = match[1];
215+
const member = members.find(
216+
(m) => m.displayName === displayName || m.email.split("@")[0] === displayName
217+
);
218+
if (member) {
219+
const exists = foundMentions.some((m) => m.id === member.id);
220+
if (!exists) {
221+
foundMentions.push({ id: member.id, displayName });
222+
}
223+
}
224+
}
225+
226+
if (foundMentions.length > 0) {
227+
setInsertedMentions(foundMentions);
228+
}
229+
setMentionsInitialized(true);
230+
}, [initialValue, members, mentionsInitialized]);
231+
203232
useEffect(() => {
204233
if (autoFocus && textareaRef.current) {
205234
textareaRef.current.focus();

surfsense_web/components/chat-comments/comment-composer/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface CommentComposerProps {
99
onSubmit: (content: string) => void;
1010
onCancel?: () => void;
1111
autoFocus?: boolean;
12+
initialValue?: string;
1213
}
1314

1415
export interface MentionState {

surfsense_web/components/chat-comments/comment-item/comment-item.tsx

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms";
66
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
77
import { Button } from "@/components/ui/button";
88
import { cn } from "@/lib/utils";
9+
import { CommentComposer } from "../comment-composer/comment-composer";
910
import { CommentActions } from "./comment-actions";
1011
import type { CommentItemProps } from "./types";
1112

@@ -67,6 +68,11 @@ function formatTimestamp(dateString: string): string {
6768
);
6869
}
6970

71+
function convertRenderedToDisplay(contentRendered: string): string {
72+
// Convert @{DisplayName} format to @DisplayName for editing
73+
return contentRendered.replace(/@\{([^}]+)\}/g, "@$1");
74+
}
75+
7076
function renderMentions(content: string): React.ReactNode {
7177
// Match @{DisplayName} format from backend
7278
const mentionPattern = /@\{([^}]+)\}/g;
@@ -99,9 +105,15 @@ function renderMentions(content: string): React.ReactNode {
99105
export function CommentItem({
100106
comment,
101107
onEdit,
108+
onEditSubmit,
109+
onEditCancel,
102110
onDelete,
103111
onReply,
104112
isReply = false,
113+
isEditing = false,
114+
isSubmitting = false,
115+
members = [],
116+
membersLoading = false,
105117
}: CommentItemProps) {
106118
const [{ data: currentUser }] = useAtom(currentUserAtom);
107119

@@ -111,6 +123,10 @@ export function CommentItem({
111123
: comment.author?.displayName || comment.author?.email.split("@")[0] || "Unknown";
112124
const email = comment.author?.email || "";
113125

126+
const handleEditSubmit = (content: string) => {
127+
onEditSubmit?.(comment.id, content);
128+
};
129+
114130
return (
115131
<div className={cn("group flex gap-3")}>
116132
<Avatar className="size-8 shrink-0">
@@ -131,21 +147,39 @@ export function CommentItem({
131147
{comment.isEdited && (
132148
<span className="shrink-0 text-xs text-muted-foreground">(edited)</span>
133149
)}
134-
<div className="ml-auto">
135-
<CommentActions
136-
canEdit={comment.canEdit}
137-
canDelete={comment.canDelete}
138-
onEdit={() => onEdit?.(comment.id)}
139-
onDelete={() => onDelete?.(comment.id)}
140-
/>
141-
</div>
150+
{!isEditing && (
151+
<div className="ml-auto">
152+
<CommentActions
153+
canEdit={comment.canEdit}
154+
canDelete={comment.canDelete}
155+
onEdit={() => onEdit?.(comment.id)}
156+
onDelete={() => onDelete?.(comment.id)}
157+
/>
158+
</div>
159+
)}
142160
</div>
143161

144-
<div className="mt-1 text-sm text-foreground whitespace-pre-wrap wrap-break-word">
145-
{renderMentions(comment.contentRendered)}
146-
</div>
162+
{isEditing ? (
163+
<div className="mt-1">
164+
<CommentComposer
165+
members={members}
166+
membersLoading={membersLoading}
167+
placeholder="Edit your comment..."
168+
submitLabel="Save"
169+
isSubmitting={isSubmitting}
170+
onSubmit={handleEditSubmit}
171+
onCancel={onEditCancel}
172+
initialValue={convertRenderedToDisplay(comment.contentRendered)}
173+
autoFocus
174+
/>
175+
</div>
176+
) : (
177+
<div className="mt-1 text-sm text-foreground whitespace-pre-wrap wrap-break-word">
178+
{renderMentions(comment.contentRendered)}
179+
</div>
180+
)}
147181

148-
{!isReply && onReply && (
182+
{!isReply && onReply && !isEditing && (
149183
<Button
150184
variant="ghost"
151185
size="sm"

surfsense_web/components/chat-comments/comment-item/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ export interface CommentData {
2020
export interface CommentItemProps {
2121
comment: CommentData;
2222
onEdit?: (commentId: number) => void;
23+
onEditSubmit?: (commentId: number, content: string) => void;
24+
onEditCancel?: () => void;
2325
onDelete?: (commentId: number) => void;
2426
onReply?: (commentId: number) => void;
2527
isReply?: boolean;
28+
isEditing?: boolean;
29+
isSubmitting?: boolean;
30+
members?: Array<{ id: string; displayName: string | null; email: string; avatarUrl?: string | null }>;
31+
membersLoading?: boolean;
2632
}
2733

2834
export interface CommentActionsProps {

surfsense_web/components/chat-comments/comment-thread/comment-thread.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function CommentThread({
1818
}: CommentThreadProps) {
1919
const [isRepliesExpanded, setIsRepliesExpanded] = useState(true);
2020
const [isReplyComposerOpen, setIsReplyComposerOpen] = useState(false);
21+
const [editingCommentId, setEditingCommentId] = useState<number | null>(null);
2122

2223
const parentComment = {
2324
id: thread.id,
@@ -45,6 +46,19 @@ export function CommentThread({
4546
setIsReplyComposerOpen(false);
4647
};
4748

49+
const handleEditStart = (commentId: number) => {
50+
setEditingCommentId(commentId);
51+
};
52+
53+
const handleEditSubmit = (commentId: number, content: string) => {
54+
onEditComment(commentId, content);
55+
setEditingCommentId(null);
56+
};
57+
58+
const handleEditCancel = () => {
59+
setEditingCommentId(null);
60+
};
61+
4862
const hasReplies = thread.replies.length > 0;
4963
const showReplies = thread.replies.length === 1 || isRepliesExpanded;
5064

@@ -53,8 +67,14 @@ export function CommentThread({
5367
{/* Parent comment */}
5468
<CommentItem
5569
comment={parentComment}
56-
onEdit={(id) => onEditComment(id, "")}
70+
onEdit={handleEditStart}
71+
onEditSubmit={handleEditSubmit}
72+
onEditCancel={handleEditCancel}
5773
onDelete={onDeleteComment}
74+
isEditing={editingCommentId === parentComment.id}
75+
isSubmitting={isSubmitting}
76+
members={members}
77+
membersLoading={membersLoading}
5878
/>
5979

6080
{/* Replies and actions - using flex layout with connector */}
@@ -92,8 +112,14 @@ export function CommentThread({
92112
key={reply.id}
93113
comment={reply}
94114
isReply
95-
onEdit={(id) => onEditComment(id, "")}
115+
onEdit={handleEditStart}
116+
onEditSubmit={handleEditSubmit}
117+
onEditCancel={handleEditCancel}
96118
onDelete={onDeleteComment}
119+
isEditing={editingCommentId === reply.id}
120+
isSubmitting={isSubmitting}
121+
members={members}
122+
membersLoading={membersLoading}
97123
/>
98124
))}
99125
</div>

0 commit comments

Comments
 (0)