From 928891173f8f0f01ac479e3c8edead3decb3b2e6 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Sun, 12 Apr 2026 23:02:19 -0400 Subject: [PATCH 1/2] Markdown comment box with @mentions, review fixes, and OAuth error handling - Add reusable MarkdownEditor with Write/Preview tabs, toolbar, and @mention support - Wire up Send button on PR/issue comment boxes with proper cache invalidation - Fix submit review flow: async handling, error feedback, and OAuth org restriction warnings - Add edit mode for pending review comments and redesign comment bubble styling - Prioritize involved users (author, reviewers, commenters) in mention suggestions --- .../components/details/detail-activity.tsx | 122 ++++++- .../issues/detail/issue-detail-activity.tsx | 40 ++- .../issues/detail/issue-detail-page.tsx | 2 + .../pulls/detail/pull-body-section.tsx | 322 +----------------- .../pulls/detail/pull-detail-activity.tsx | 40 ++- .../pulls/review/review-diff-pane.tsx | 7 + .../pulls/review/review-file-diff-block.tsx | 142 +++++--- .../components/pulls/review/review-page.tsx | 112 +++++- .../pulls/review/review-submit-popover.tsx | 12 +- apps/dashboard/src/lib/github.functions.ts | 65 +++- apps/dashboard/src/lib/warning-store.ts | 43 ++- 11 files changed, 517 insertions(+), 390 deletions(-) diff --git a/apps/dashboard/src/components/details/detail-activity.tsx b/apps/dashboard/src/components/details/detail-activity.tsx index 7980900..b9d79b0 100644 --- a/apps/dashboard/src/components/details/detail-activity.tsx +++ b/apps/dashboard/src/components/details/detail-activity.tsx @@ -1,4 +1,20 @@ -import { useState } from "react"; +import { + MarkdownEditor, + type MentionCandidate, +} from "@diffkit/ui/components/markdown-editor"; +import { toast } from "@diffkit/ui/components/sonner"; +import { Spinner } from "@diffkit/ui/components/spinner"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMemo, useState } from "react"; +import { createComment } from "#/lib/github.functions"; +import { + type GitHubQueryScope, + githubQueryKeys, + githubRepoCollaboratorsQueryOptions, + githubViewerQueryOptions, +} from "#/lib/github.query"; +import type { GitHubActor } from "#/lib/github.types"; +import { checkPermissionWarning } from "#/lib/warning-store"; export function DetailActivityHeader({ title, @@ -19,24 +35,112 @@ export function DetailActivityHeader({ ); } -export function DetailCommentBox() { +export function DetailCommentBox({ + owner, + repo, + issueNumber, + scope, + involvedUsers, +}: { + owner: string; + repo: string; + issueNumber: number; + scope: GitHubQueryScope; + involvedUsers?: GitHubActor[]; +}) { const [value, setValue] = useState(""); + const [isSending, setIsSending] = useState(false); + const [mentionActivated, setMentionActivated] = useState(false); + const queryClient = useQueryClient(); + + const viewerQuery = useQuery(githubViewerQueryOptions(scope)); + const viewerLogin = viewerQuery.data?.login; + + const collaboratorsQuery = useQuery({ + ...githubRepoCollaboratorsQueryOptions(scope, { owner, repo }), + enabled: mentionActivated, + }); + + const mentionCandidates: MentionCandidate[] = useMemo(() => { + const seen = new Set(); + const candidates: MentionCandidate[] = []; + + // Exclude the current user + if (viewerLogin) seen.add(viewerLogin); + + // Involved users first (commenters, reviewers, author) + if (involvedUsers) { + for (const user of involvedUsers) { + if (seen.has(user.login)) continue; + seen.add(user.login); + candidates.push({ + id: user.login, + label: user.login, + avatarUrl: user.avatarUrl, + secondary: user.type === "Bot" ? "Bot" : undefined, + }); + } + } + + // Remaining collaborators + for (const c of collaboratorsQuery.data ?? []) { + if (seen.has(c.login)) continue; + seen.add(c.login); + candidates.push({ + id: c.login, + label: c.login, + avatarUrl: c.avatarUrl, + secondary: c.type === "Bot" ? "Bot" : undefined, + }); + } + + return candidates; + }, [viewerLogin, involvedUsers, collaboratorsQuery.data]); + + const handleSend = async () => { + if (!value.trim()) return; + setIsSending(true); + try { + const result = await createComment({ + data: { owner, repo, issueNumber, body: value.trim() }, + }); + if (result.ok) { + setValue(""); + void queryClient.invalidateQueries({ + queryKey: githubQueryKeys.all, + }); + } else { + toast.error(result.error); + checkPermissionWarning(result, `${owner}/${repo}`); + } + } catch { + toast.error("Failed to send comment"); + } finally { + setIsSending(false); + } + }; return ( -
-