import { Stack, Text, Group, Button, Textarea, Badge, ActionIcon, Loader, Paper, Divider, Tooltip, } from "@mantine/core"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useRowComments, useCreateRowComment, useResolveRowComment, useDeleteRowComment, } from "../hooks/use-row-comments"; import type { RowComment } from "../services/row-comments-client"; interface RowCommentsPanelProps { tableId: string; rowId: string; /** Current user id — used to show own-comment controls */ currentUserId: string; } /** * RowCommentsPanel — threaded comment panel for a Baserow row (R3.8). * * Shows unresolved threads by default; a "Resolved" tab toggles to archived. * Compose area creates root comments; "Reply" button creates a child. * * Architecture note: content is stored as Tiptap JSON on the server but * displayed as plain text here. A future enhancement could plug in a * read-only Tiptap renderer — out of scope for R3.8 (Notion-like phase 1). */ export function RowCommentsPanel({ tableId, rowId, currentUserId, }: RowCommentsPanelProps) { const { t } = useTranslation(); const [showResolved, setShowResolved] = useState(false); const [draft, setDraft] = useState(""); const [replyTo, setReplyTo] = useState(null); const { data: comments = [], isLoading } = useRowComments( tableId, rowId, showResolved ? true : false, ); const createMutation = useCreateRowComment(); const resolveMutation = useResolveRowComment(); const deleteMutation = useDeleteRowComment(); function tiptapDocFromText(text: string) { return JSON.stringify({ type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text }] }], }); } function extractText(content: Record | string): string { if (!content) return ""; const doc = typeof content === "string" ? JSON.parse(content) : content; if (!doc?.content) return ""; const texts: string[] = []; function walk(node: Record) { if (node.type === "text" && typeof node.text === "string") { texts.push(node.text); } if (Array.isArray(node.content)) { (node.content as Record[]).forEach(walk); } } (doc.content as Record[]).forEach(walk); return texts.join(" "); } async function handleSubmit() { const text = draft.trim(); if (!text) return; await createMutation.mutateAsync({ tableId, rowId, content: tiptapDocFromText(text), parentCommentId: replyTo ?? undefined, }); setDraft(""); setReplyTo(null); } const rootComments = comments.filter((c) => c.parentCommentId === null); return ( {/* Tab toggle */} {isLoading && } {!isLoading && rootComments.length === 0 && ( {t("acadenice.comments.empty")} )} {/* Thread list */} {rootComments.map((root) => { const replies = comments.filter((c) => c.parentCommentId === root.id); return ( setReplyTo(root.id)} onResolve={(resolved) => resolveMutation.mutate({ commentId: root.id, resolved, tableId, rowId, }) } onDelete={() => deleteMutation.mutate({ commentId: root.id, tableId, rowId }) } /> {replies.map((reply) => ( deleteMutation.mutate({ commentId: reply.id, tableId, rowId, }) } /> ))} {replyTo === root.id && (