mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 01:06:23 +02:00
feat: implement inline editing for comments
This commit is contained in:
parent
13135ec51b
commit
9d11446553
5 changed files with 111 additions and 15 deletions
|
|
@ -86,9 +86,11 @@ export function CommentComposer({
|
|||
onSubmit,
|
||||
onCancel,
|
||||
autoFocus = false,
|
||||
initialValue = "",
|
||||
}: CommentComposerProps) {
|
||||
const [displayContent, setDisplayContent] = useState("");
|
||||
const [displayContent, setDisplayContent] = useState(initialValue);
|
||||
const [insertedMentions, setInsertedMentions] = useState<InsertedMention[]>([]);
|
||||
const [mentionsInitialized, setMentionsInitialized] = useState(false);
|
||||
const [mentionState, setMentionState] = useState<MentionState>({
|
||||
isActive: false,
|
||||
query: "",
|
||||
|
|
@ -200,6 +202,33 @@ export function CommentComposer({
|
|||
setInsertedMentions([]);
|
||||
};
|
||||
|
||||
// Pre-populate insertedMentions from initialValue when members are loaded
|
||||
useEffect(() => {
|
||||
if (mentionsInitialized || !initialValue || members.length === 0) return;
|
||||
|
||||
const mentionPattern = /@([^\s@]+(?:\s+[^\s@]+)*?)(?=\s|$|[.,!?;:]|@)/g;
|
||||
const foundMentions: InsertedMention[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = mentionPattern.exec(initialValue)) !== null) {
|
||||
const displayName = match[1];
|
||||
const member = members.find(
|
||||
(m) => m.displayName === displayName || m.email.split("@")[0] === displayName
|
||||
);
|
||||
if (member) {
|
||||
const exists = foundMentions.some((m) => m.id === member.id);
|
||||
if (!exists) {
|
||||
foundMentions.push({ id: member.id, displayName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundMentions.length > 0) {
|
||||
setInsertedMentions(foundMentions);
|
||||
}
|
||||
setMentionsInitialized(true);
|
||||
}, [initialValue, members, mentionsInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface CommentComposerProps {
|
|||
onSubmit: (content: string) => void;
|
||||
onCancel?: () => void;
|
||||
autoFocus?: boolean;
|
||||
initialValue?: string;
|
||||
}
|
||||
|
||||
export interface MentionState {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
|||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CommentComposer } from "../comment-composer/comment-composer";
|
||||
import { CommentActions } from "./comment-actions";
|
||||
import type { CommentItemProps } from "./types";
|
||||
|
||||
|
|
@ -67,6 +68,11 @@ function formatTimestamp(dateString: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
function convertRenderedToDisplay(contentRendered: string): string {
|
||||
// Convert @{DisplayName} format to @DisplayName for editing
|
||||
return contentRendered.replace(/@\{([^}]+)\}/g, "@$1");
|
||||
}
|
||||
|
||||
function renderMentions(content: string): React.ReactNode {
|
||||
// Match @{DisplayName} format from backend
|
||||
const mentionPattern = /@\{([^}]+)\}/g;
|
||||
|
|
@ -99,9 +105,15 @@ function renderMentions(content: string): React.ReactNode {
|
|||
export function CommentItem({
|
||||
comment,
|
||||
onEdit,
|
||||
onEditSubmit,
|
||||
onEditCancel,
|
||||
onDelete,
|
||||
onReply,
|
||||
isReply = false,
|
||||
isEditing = false,
|
||||
isSubmitting = false,
|
||||
members = [],
|
||||
membersLoading = false,
|
||||
}: CommentItemProps) {
|
||||
const [{ data: currentUser }] = useAtom(currentUserAtom);
|
||||
|
||||
|
|
@ -111,6 +123,10 @@ export function CommentItem({
|
|||
: comment.author?.displayName || comment.author?.email.split("@")[0] || "Unknown";
|
||||
const email = comment.author?.email || "";
|
||||
|
||||
const handleEditSubmit = (content: string) => {
|
||||
onEditSubmit?.(comment.id, content);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("group flex gap-3")}>
|
||||
<Avatar className="size-8 shrink-0">
|
||||
|
|
@ -131,21 +147,39 @@ export function CommentItem({
|
|||
{comment.isEdited && (
|
||||
<span className="shrink-0 text-xs text-muted-foreground">(edited)</span>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
<CommentActions
|
||||
canEdit={comment.canEdit}
|
||||
canDelete={comment.canDelete}
|
||||
onEdit={() => onEdit?.(comment.id)}
|
||||
onDelete={() => onDelete?.(comment.id)}
|
||||
{!isEditing && (
|
||||
<div className="ml-auto">
|
||||
<CommentActions
|
||||
canEdit={comment.canEdit}
|
||||
canDelete={comment.canDelete}
|
||||
onEdit={() => onEdit?.(comment.id)}
|
||||
onDelete={() => onDelete?.(comment.id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="mt-1">
|
||||
<CommentComposer
|
||||
members={members}
|
||||
membersLoading={membersLoading}
|
||||
placeholder="Edit your comment..."
|
||||
submitLabel="Save"
|
||||
isSubmitting={isSubmitting}
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={onEditCancel}
|
||||
initialValue={convertRenderedToDisplay(comment.contentRendered)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1 text-sm text-foreground whitespace-pre-wrap wrap-break-word">
|
||||
{renderMentions(comment.contentRendered)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-1 text-sm text-foreground whitespace-pre-wrap wrap-break-word">
|
||||
{renderMentions(comment.contentRendered)}
|
||||
</div>
|
||||
|
||||
{!isReply && onReply && (
|
||||
{!isReply && onReply && !isEditing && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -20,9 +20,15 @@ export interface CommentData {
|
|||
export interface CommentItemProps {
|
||||
comment: CommentData;
|
||||
onEdit?: (commentId: number) => void;
|
||||
onEditSubmit?: (commentId: number, content: string) => void;
|
||||
onEditCancel?: () => void;
|
||||
onDelete?: (commentId: number) => void;
|
||||
onReply?: (commentId: number) => void;
|
||||
isReply?: boolean;
|
||||
isEditing?: boolean;
|
||||
isSubmitting?: boolean;
|
||||
members?: Array<{ id: string; displayName: string | null; email: string; avatarUrl?: string | null }>;
|
||||
membersLoading?: boolean;
|
||||
}
|
||||
|
||||
export interface CommentActionsProps {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function CommentThread({
|
|||
}: CommentThreadProps) {
|
||||
const [isRepliesExpanded, setIsRepliesExpanded] = useState(true);
|
||||
const [isReplyComposerOpen, setIsReplyComposerOpen] = useState(false);
|
||||
const [editingCommentId, setEditingCommentId] = useState<number | null>(null);
|
||||
|
||||
const parentComment = {
|
||||
id: thread.id,
|
||||
|
|
@ -45,6 +46,19 @@ export function CommentThread({
|
|||
setIsReplyComposerOpen(false);
|
||||
};
|
||||
|
||||
const handleEditStart = (commentId: number) => {
|
||||
setEditingCommentId(commentId);
|
||||
};
|
||||
|
||||
const handleEditSubmit = (commentId: number, content: string) => {
|
||||
onEditComment(commentId, content);
|
||||
setEditingCommentId(null);
|
||||
};
|
||||
|
||||
const handleEditCancel = () => {
|
||||
setEditingCommentId(null);
|
||||
};
|
||||
|
||||
const hasReplies = thread.replies.length > 0;
|
||||
const showReplies = thread.replies.length === 1 || isRepliesExpanded;
|
||||
|
||||
|
|
@ -53,8 +67,14 @@ export function CommentThread({
|
|||
{/* Parent comment */}
|
||||
<CommentItem
|
||||
comment={parentComment}
|
||||
onEdit={(id) => onEditComment(id, "")}
|
||||
onEdit={handleEditStart}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
onEditCancel={handleEditCancel}
|
||||
onDelete={onDeleteComment}
|
||||
isEditing={editingCommentId === parentComment.id}
|
||||
isSubmitting={isSubmitting}
|
||||
members={members}
|
||||
membersLoading={membersLoading}
|
||||
/>
|
||||
|
||||
{/* Replies and actions - using flex layout with connector */}
|
||||
|
|
@ -92,8 +112,14 @@ export function CommentThread({
|
|||
key={reply.id}
|
||||
comment={reply}
|
||||
isReply
|
||||
onEdit={(id) => onEditComment(id, "")}
|
||||
onEdit={handleEditStart}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
onEditCancel={handleEditCancel}
|
||||
onDelete={onDeleteComment}
|
||||
isEditing={editingCommentId === reply.id}
|
||||
isSubmitting={isSubmitting}
|
||||
members={members}
|
||||
membersLoading={membersLoading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue