updated comments
Signed-off-by: Max <ai-agent@topdoglabs.com>
This commit is contained in:
parent
92adbef66e
commit
8aaca14e3a
@ -215,7 +215,7 @@ cmd_task_comment() {
|
||||
--arg id "$comment_id" \
|
||||
--arg text "$text" \
|
||||
--arg createdAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
'{id: $id, text: $text, createdAt: $createdAt, author: "assistant"}')
|
||||
'{id: $id, text: $text, createdAt: $createdAt, commentAuthorId: "assistant", replies: []}')
|
||||
|
||||
local updated_task
|
||||
updated_task=$(echo "$task" | jq --argjson comment "$new_comment" '.comments += [$comment]')
|
||||
|
||||
@ -231,7 +231,7 @@ function mapTaskRow(row: Record<string, unknown>, usersById: Map<string, UserPro
|
||||
throw new HttpError(500, "Invalid tasks.tags value in database", { taskId: row.id, value: row.tags });
|
||||
}
|
||||
|
||||
if (includeFullData && row.comments !== undefined && !Array.isArray(row.comments)) {
|
||||
if (row.comments !== undefined && !Array.isArray(row.comments)) {
|
||||
throw new HttpError(500, "Invalid tasks.comments value in database", { taskId: row.id, value: row.comments });
|
||||
}
|
||||
|
||||
@ -261,7 +261,7 @@ function mapTaskRow(row: Record<string, unknown>, usersById: Map<string, UserPro
|
||||
assigneeEmail: assigneeUser?.email,
|
||||
assigneeAvatarUrl: assigneeUser?.avatarUrl,
|
||||
dueDate: toNonEmptyString(row.due_date),
|
||||
comments: includeFullData ? (row.comments as unknown[] | undefined) ?? [] : [],
|
||||
comments: (row.comments as unknown[] | undefined) ?? [],
|
||||
tags: (row.tags as unknown[]).filter((tag): tag is string => typeof tag === "string"),
|
||||
attachments: includeFullData ? (row.attachments as unknown[] | undefined) ?? [] : [],
|
||||
};
|
||||
|
||||
@ -35,7 +35,7 @@ import {
|
||||
markdownPreviewObjectUrl,
|
||||
textPreviewObjectUrl,
|
||||
} from "@/lib/attachments"
|
||||
import { useTaskStore, Task, TaskType, TaskStatus, Priority, TaskAttachment, type CommentAuthor } from "@/stores/useTaskStore"
|
||||
import { useTaskStore, Task, TaskType, TaskStatus, Priority, TaskAttachment } from "@/stores/useTaskStore"
|
||||
import { BacklogSkeleton, SearchSkeleton } from "@/components/LoadingSkeletons"
|
||||
import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download, Search, Archive } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
@ -422,27 +422,8 @@ export default function Home() {
|
||||
})
|
||||
}
|
||||
|
||||
const getCommentAuthor = (value: unknown): CommentAuthor => {
|
||||
if (value === "assistant") {
|
||||
return { id: "assistant", name: "Assistant", type: "assistant" }
|
||||
}
|
||||
if (value === "user") {
|
||||
return { id: "legacy-user", name: "User", type: "human" }
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return { id: "legacy-user", name: "User", type: "human" }
|
||||
}
|
||||
|
||||
const candidate = value as Partial<CommentAuthor>
|
||||
const type = candidate.type === "assistant" || candidate.id === "assistant" ? "assistant" : "human"
|
||||
return {
|
||||
id: typeof candidate.id === "string" && candidate.id ? candidate.id : type === "assistant" ? "assistant" : "legacy-user",
|
||||
name: typeof candidate.name === "string" && candidate.name ? candidate.name : type === "assistant" ? "Assistant" : "User",
|
||||
email: typeof candidate.email === "string" && candidate.email ? candidate.email : undefined,
|
||||
avatarUrl: typeof candidate.avatarUrl === "string" && candidate.avatarUrl ? candidate.avatarUrl : undefined,
|
||||
type,
|
||||
}
|
||||
}
|
||||
const getCommentAuthorId = (value: unknown): string | null =>
|
||||
typeof value === "string" && value.trim().length > 0 ? value.trim() : null
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"
|
||||
@ -872,7 +853,12 @@ export default function Home() {
|
||||
|
||||
const handleAddComment = () => {
|
||||
if (newComment.trim() && selectedTaskId) {
|
||||
addComment(selectedTaskId, newComment.trim())
|
||||
const commentAuthorId = getCommentAuthorId(currentUser.id)
|
||||
if (!commentAuthorId) {
|
||||
toast.error("You must be signed in to add a comment.")
|
||||
return
|
||||
}
|
||||
addComment(selectedTaskId, newComment.trim(), commentAuthorId)
|
||||
setNewComment("")
|
||||
}
|
||||
}
|
||||
@ -1697,10 +1683,15 @@ export default function Home() {
|
||||
<p className="text-slate-500 text-sm">No comments yet. Add the first one.</p>
|
||||
) : (
|
||||
editedTask.comments.map((comment) => {
|
||||
const author = getCommentAuthor(comment.author)
|
||||
const isAssistant = author.type === "assistant"
|
||||
const displayName = author.id === currentUser.id ? "You" : author.name
|
||||
const resolvedAuthorAvatar = author.avatarUrl || resolveAssignee(author.id)?.avatarUrl
|
||||
const authorId = comment.commentAuthorId
|
||||
const isAssistant = authorId === "assistant"
|
||||
const resolvedAuthor = authorId === currentUser.id ? currentUser : resolveAssignee(authorId)
|
||||
const displayName = isAssistant
|
||||
? "Assistant"
|
||||
: authorId === currentUser.id
|
||||
? "You"
|
||||
: resolvedAuthor?.name || "Unknown user"
|
||||
const resolvedAuthorAvatar = resolvedAuthor?.avatarUrl
|
||||
return (
|
||||
<div
|
||||
key={comment.id}
|
||||
@ -1713,7 +1704,7 @@ export default function Home() {
|
||||
AI
|
||||
</div>
|
||||
) : (
|
||||
<AvatarCircle name={displayName} avatarUrl={resolvedAuthorAvatar} seed={author.id} sizeClass="h-8 w-8" />
|
||||
<AvatarCircle name={displayName} avatarUrl={resolvedAuthorAvatar} seed={authorId} sizeClass="h-8 w-8" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
|
||||
@ -21,7 +21,6 @@ import { parseSprintStart } from "@/lib/utils"
|
||||
import {
|
||||
useTaskStore,
|
||||
type Comment as TaskComment,
|
||||
type CommentAuthor,
|
||||
type Priority,
|
||||
type Task,
|
||||
type TaskAttachment,
|
||||
@ -89,13 +88,15 @@ const getComments = (value: unknown): TaskComment[] => {
|
||||
for (const entry of value) {
|
||||
if (!entry || typeof entry !== "object") continue
|
||||
const comment = entry as Partial<TaskComment>
|
||||
if (typeof comment.id !== "string" || typeof comment.text !== "string") continue
|
||||
if (typeof comment.id !== "string" || typeof comment.text !== "string" || typeof comment.createdAt !== "string") continue
|
||||
const commentAuthorId = getCommentAuthorId(comment.commentAuthorId)
|
||||
if (!commentAuthorId) continue
|
||||
|
||||
normalized.push({
|
||||
id: comment.id,
|
||||
text: comment.text,
|
||||
createdAt: typeof comment.createdAt === "string" ? comment.createdAt : new Date().toISOString(),
|
||||
author: getCommentAuthor(comment.author),
|
||||
createdAt: comment.createdAt,
|
||||
commentAuthorId,
|
||||
replies: getComments(comment.replies),
|
||||
})
|
||||
}
|
||||
@ -103,41 +104,17 @@ const getComments = (value: unknown): TaskComment[] => {
|
||||
return normalized
|
||||
}
|
||||
|
||||
const getCommentAuthor = (value: unknown): CommentAuthor => {
|
||||
if (value === "assistant") {
|
||||
return { id: "assistant", name: "Assistant", type: "assistant" }
|
||||
}
|
||||
if (value === "user") {
|
||||
return { id: "legacy-user", name: "User", type: "human" }
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return { id: "legacy-user", name: "User", type: "human" }
|
||||
}
|
||||
const getCommentAuthorId = (value: unknown): string | null =>
|
||||
typeof value === "string" && value.trim().length > 0 ? value.trim() : null
|
||||
|
||||
const candidate = value as Partial<CommentAuthor>
|
||||
const type = candidate.type === "assistant" || candidate.id === "assistant" ? "assistant" : "human"
|
||||
return {
|
||||
id: typeof candidate.id === "string" && candidate.id ? candidate.id : type === "assistant" ? "assistant" : "legacy-user",
|
||||
name: typeof candidate.name === "string" && candidate.name ? candidate.name : type === "assistant" ? "Assistant" : "User",
|
||||
email: typeof candidate.email === "string" && candidate.email ? candidate.email : undefined,
|
||||
avatarUrl: typeof candidate.avatarUrl === "string" && candidate.avatarUrl ? candidate.avatarUrl : undefined,
|
||||
type,
|
||||
}
|
||||
}
|
||||
const getCurrentUserCommentAuthorId = (profile: UserProfile): string | null =>
|
||||
getCommentAuthorId(profile.id)
|
||||
|
||||
const profileToAuthor = (profile: UserProfile): CommentAuthor => ({
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
type: "human",
|
||||
})
|
||||
|
||||
const buildComment = (text: string, author: CommentAuthor): TaskComment => ({
|
||||
const buildComment = (text: string, commentAuthorId: string): TaskComment => ({
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
text,
|
||||
createdAt: new Date().toISOString(),
|
||||
author,
|
||||
commentAuthorId,
|
||||
replies: [],
|
||||
})
|
||||
|
||||
@ -400,10 +377,14 @@ export default function TaskDetailPage() {
|
||||
const handleAddComment = () => {
|
||||
if (!editedTask || !newComment.trim()) return
|
||||
|
||||
const actor = profileToAuthor(currentUser)
|
||||
const commentAuthorId = getCurrentUserCommentAuthorId(currentUser)
|
||||
if (!commentAuthorId) {
|
||||
toast.error("You must be signed in to add a comment.")
|
||||
return
|
||||
}
|
||||
setEditedTask({
|
||||
...editedTask,
|
||||
comments: [...getComments(editedTask.comments), buildComment(newComment.trim(), actor)],
|
||||
comments: [...getComments(editedTask.comments), buildComment(newComment.trim(), commentAuthorId)],
|
||||
})
|
||||
setNewComment("")
|
||||
}
|
||||
@ -413,10 +394,14 @@ export default function TaskDetailPage() {
|
||||
const text = replyDrafts[parentId]?.trim()
|
||||
if (!text) return
|
||||
|
||||
const actor = profileToAuthor(currentUser)
|
||||
const commentAuthorId = getCurrentUserCommentAuthorId(currentUser)
|
||||
if (!commentAuthorId) {
|
||||
toast.error("You must be signed in to reply.")
|
||||
return
|
||||
}
|
||||
setEditedTask({
|
||||
...editedTask,
|
||||
comments: addReplyToThread(getComments(editedTask.comments), parentId, buildComment(text, actor)),
|
||||
comments: addReplyToThread(getComments(editedTask.comments), parentId, buildComment(text, commentAuthorId)),
|
||||
})
|
||||
|
||||
setReplyDrafts((prev) => ({ ...prev, [parentId]: "" }))
|
||||
@ -586,10 +571,11 @@ export default function TaskDetailPage() {
|
||||
const replies = getComments(comment.replies)
|
||||
const isReplying = !!openReplyEditors[comment.id]
|
||||
const replyDraft = replyDrafts[comment.id] || ""
|
||||
const author = getCommentAuthor(comment.author)
|
||||
const isAssistant = author.type === "assistant"
|
||||
const displayName = isAssistant ? "Assistant" : author.id === currentUser.id ? "You" : author.name
|
||||
const resolvedAuthorAvatar = author.avatarUrl || resolveAssignee(author.id)?.avatarUrl
|
||||
const authorId = comment.commentAuthorId
|
||||
const isAssistant = authorId === "assistant"
|
||||
const resolvedAuthor = authorId === currentUser.id ? currentUser : resolveAssignee(authorId)
|
||||
const displayName = isAssistant ? "Assistant" : authorId === currentUser.id ? "You" : resolvedAuthor?.name || "Unknown user"
|
||||
const resolvedAuthorAvatar = resolvedAuthor?.avatarUrl
|
||||
|
||||
return (
|
||||
<div key={comment.id} className="space-y-2" style={{ marginLeft: depth * 20 }}>
|
||||
@ -601,7 +587,7 @@ export default function TaskDetailPage() {
|
||||
AI
|
||||
</span>
|
||||
) : (
|
||||
<AvatarCircle name={displayName} avatarUrl={resolvedAuthorAvatar} seed={author.id} />
|
||||
<AvatarCircle name={displayName} avatarUrl={resolvedAuthorAvatar} seed={authorId} />
|
||||
)}
|
||||
<span className="text-sm text-slate-300 font-medium">{displayName}</span>
|
||||
<span className="text-xs text-slate-500">{new Date(comment.createdAt).toLocaleString()}</span>
|
||||
|
||||
@ -22,18 +22,10 @@ export interface Comment {
|
||||
id: string
|
||||
text: string
|
||||
createdAt: string
|
||||
author: CommentAuthor | 'user' | 'assistant'
|
||||
commentAuthorId: string
|
||||
replies?: Comment[]
|
||||
}
|
||||
|
||||
export interface CommentAuthor {
|
||||
id: string
|
||||
name: string
|
||||
email?: string
|
||||
avatarUrl?: string
|
||||
type: 'human' | 'assistant'
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string
|
||||
name: string
|
||||
@ -121,7 +113,7 @@ interface TaskStore {
|
||||
getTasksBySprint: (sprintId: string) => Task[]
|
||||
|
||||
// Comment actions
|
||||
addComment: (taskId: string, text: string, author?: CommentAuthor | 'user' | 'assistant') => void
|
||||
addComment: (taskId: string, text: string, commentAuthorId: string) => void
|
||||
deleteComment: (taskId: string, commentId: string) => void
|
||||
|
||||
// Filters
|
||||
@ -145,57 +137,14 @@ const normalizeUserProfile = (value: unknown, fallback: UserProfile = defaultCur
|
||||
return { id, name, email, avatarUrl }
|
||||
}
|
||||
|
||||
const profileToCommentAuthor = (profile: UserProfile): CommentAuthor => ({
|
||||
const profileToCommentAuthor = (profile: UserProfile) => ({
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
type: 'human',
|
||||
})
|
||||
|
||||
const assistantAuthor: CommentAuthor = {
|
||||
id: 'assistant',
|
||||
name: 'Assistant',
|
||||
type: 'assistant',
|
||||
}
|
||||
|
||||
const normalizeCommentAuthor = (value: unknown): CommentAuthor => {
|
||||
if (value === 'assistant') return assistantAuthor
|
||||
if (value === 'user') {
|
||||
return {
|
||||
id: 'legacy-user',
|
||||
name: 'User',
|
||||
type: 'human',
|
||||
}
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object') {
|
||||
return {
|
||||
id: 'legacy-user',
|
||||
name: 'User',
|
||||
type: 'human',
|
||||
}
|
||||
}
|
||||
|
||||
const candidate = value as Partial<CommentAuthor>
|
||||
const type: CommentAuthor['type'] =
|
||||
candidate.type === 'assistant' || candidate.id === 'assistant' ? 'assistant' : 'human'
|
||||
|
||||
const id = typeof candidate.id === 'string' && candidate.id.trim().length > 0
|
||||
? candidate.id
|
||||
: type === 'assistant'
|
||||
? 'assistant'
|
||||
: 'legacy-user'
|
||||
const name = typeof candidate.name === 'string' && candidate.name.trim().length > 0
|
||||
? candidate.name.trim()
|
||||
: type === 'assistant'
|
||||
? 'Assistant'
|
||||
: 'User'
|
||||
const email = typeof candidate.email === 'string' && candidate.email.trim().length > 0 ? candidate.email.trim() : undefined
|
||||
const avatarUrl = typeof candidate.avatarUrl === 'string' && candidate.avatarUrl.trim().length > 0 ? candidate.avatarUrl : undefined
|
||||
|
||||
return { id, name, email, avatarUrl, type }
|
||||
}
|
||||
const normalizeCommentAuthorId = (value: unknown): string | null =>
|
||||
typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
|
||||
|
||||
const normalizeComments = (value: unknown): Comment[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
@ -205,13 +154,15 @@ const normalizeComments = (value: unknown): Comment[] => {
|
||||
for (const entry of value) {
|
||||
if (!entry || typeof entry !== 'object') continue
|
||||
const candidate = entry as Partial<Comment>
|
||||
if (typeof candidate.id !== 'string' || typeof candidate.text !== 'string') continue
|
||||
if (typeof candidate.id !== 'string' || typeof candidate.text !== 'string' || typeof candidate.createdAt !== 'string') continue
|
||||
const commentAuthorId = normalizeCommentAuthorId(candidate.commentAuthorId)
|
||||
if (!commentAuthorId) continue
|
||||
|
||||
comments.push({
|
||||
id: candidate.id,
|
||||
text: candidate.text,
|
||||
createdAt: typeof candidate.createdAt === 'string' ? candidate.createdAt : new Date().toISOString(),
|
||||
author: normalizeCommentAuthor(candidate.author),
|
||||
createdAt: candidate.createdAt,
|
||||
commentAuthorId,
|
||||
replies: normalizeComments(candidate.replies),
|
||||
})
|
||||
}
|
||||
@ -646,13 +597,19 @@ export const useTaskStore = create<TaskStore>()(
|
||||
return get().tasks.filter((t) => t.sprintId === sprintId)
|
||||
},
|
||||
|
||||
addComment: (taskId, text, author) => {
|
||||
const actor = normalizeCommentAuthor(author ?? profileToCommentAuthor(get().currentUser))
|
||||
addComment: (taskId, text, commentAuthorId) => {
|
||||
const normalizedCommentAuthorId = normalizeCommentAuthorId(commentAuthorId)
|
||||
if (!normalizedCommentAuthorId) {
|
||||
const message = 'Failed to add comment: invalid comment author id'
|
||||
console.error(message, { taskId, commentAuthorId })
|
||||
set({ syncError: message })
|
||||
return
|
||||
}
|
||||
const newComment: Comment = {
|
||||
id: Date.now().toString(),
|
||||
text,
|
||||
createdAt: new Date().toISOString(),
|
||||
author: actor,
|
||||
commentAuthorId: normalizedCommentAuthorId,
|
||||
replies: [],
|
||||
}
|
||||
set((state) => {
|
||||
|
||||
1177
supabase/tasks.json
Normal file
1177
supabase/tasks.json
Normal file
File diff suppressed because one or more lines are too long
22
supabase/update_task_comments_only.sql
Normal file
22
supabase/update_task_comments_only.sql
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user