From c19a0f36a2a4332c3394567d13de33e04eebcae5 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Tue, 14 Apr 2026 17:31:01 -0400 Subject: [PATCH 1/3] fix: hide merge section for users without merge permissions --- .../src/components/pulls/detail/pull-detail-activity.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx index a61f7b7..cac240d 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx @@ -1055,6 +1055,8 @@ function MergeFooter({ const isDisabled = !canMerge || (isMergeBlocked && !bypass.shouldBypass) || isMerging; + if (!canMerge) return null; + return (
@@ -1109,11 +1111,7 @@ function MergeFooter({
- {!canMerge ? ( -

- You don't have permission to merge this pull request. -

- ) : isMergeBlocked && !bypass.shouldBypass ? ( + {isMergeBlocked && !bypass.shouldBypass ? (

Merging is blocked — all required conditions have not been met.

From 2884d4f30a61f34fbb6153dce1086b7554254cc0 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Tue, 14 Apr 2026 17:50:07 -0400 Subject: [PATCH 2/3] feat: merge commit form, close/reopen PR, and reviewers empty state - Add inline commit message form when merging (title + extended description, pre-filled from first commit) - Add close/reopen PR button next to send in comment box - Add updatePullState server function - Pass commitTitle and commitMessage through to GitHub merge API - Add empty state in reviews section when no reviewers added - Style commit form with surface background to match other sections --- .../components/details/detail-activity.tsx | 66 ++++++++++- .../pulls/detail/pull-detail-activity.tsx | 109 +++++++++++++++++- apps/dashboard/src/lib/github.functions.ts | 33 ++++++ 3 files changed, 202 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/src/components/details/detail-activity.tsx b/apps/dashboard/src/components/details/detail-activity.tsx index 8b1547c..3162a08 100644 --- a/apps/dashboard/src/components/details/detail-activity.tsx +++ b/apps/dashboard/src/components/details/detail-activity.tsx @@ -1,4 +1,8 @@ -import { CommentIcon } from "@diffkit/icons"; +import { + CommentIcon, + GitPullRequestClosedIcon, + GitPullRequestIcon, +} from "@diffkit/icons"; import { MarkdownEditor, type MentionCandidate, @@ -7,7 +11,7 @@ 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 { createComment, updatePullState } from "#/lib/github.functions"; import { type GitHubQueryScope, githubQueryKeys, @@ -42,15 +46,18 @@ export function DetailCommentBox({ issueNumber, scope, involvedUsers, + pullState, }: { owner: string; repo: string; issueNumber: number; scope: GitHubQueryScope; involvedUsers?: GitHubActor[]; + pullState?: "open" | "closed"; }) { const [value, setValue] = useState(""); const [isSending, setIsSending] = useState(false); + const [isTogglingState, setIsTogglingState] = useState(false); const [mentionActivated, setMentionActivated] = useState(false); const queryClient = useQueryClient(); @@ -121,6 +128,42 @@ export function DetailCommentBox({ } }; + const handleTogglePullState = async () => { + if (!pullState) return; + const newState = pullState === "open" ? "closed" : "open"; + setIsTogglingState(true); + try { + if (value.trim()) { + await createComment({ + data: { owner, repo, issueNumber, body: value.trim() }, + }); + setValue(""); + } + const result = await updatePullState({ + data: { + owner, + repo, + pullNumber: issueNumber, + state: newState, + }, + }); + if (result.ok) { + void queryClient.invalidateQueries({ + queryKey: githubQueryKeys.all, + }); + } else { + toast.error(result.error); + checkPermissionWarning(result, `${owner}/${repo}`); + } + } catch { + toast.error( + `Failed to ${pullState === "open" ? "close" : "reopen"} pull request`, + ); + } finally { + setIsTogglingState(false); + } + }; + return (
-
+
+ {pullState && ( + + )}
)} @@ -250,6 +252,11 @@ export function PullDetailActivitySection({ issueNumber={pullNumber} scope={scope} involvedUsers={getInvolvedUsers(pr, comments)} + pullState={ + !pr.isMerged && (pr.state === "open" || pr.state === "closed") + ? pr.state + : undefined + } />
@@ -294,11 +301,15 @@ function MergeStatusSection({ owner, repo, pullNumber, + prTitle, + firstCommitMessage, }: { scope: GitHubQueryScope; owner: string; repo: string; pullNumber: number; + prTitle: string; + firstCommitMessage?: string; }) { const statusQuery = useQuery({ ...githubPullStatusQueryOptions(scope, { owner, repo, pullNumber }), @@ -314,6 +325,8 @@ function MergeStatusSection({ owner={owner} repo={repo} pullNumber={pullNumber} + prTitle={prTitle} + firstCommitMessage={firstCommitMessage} /> ); } @@ -408,11 +421,15 @@ function MergeStatusCard({ owner, repo, pullNumber, + prTitle, + firstCommitMessage, }: { status: PullStatus; owner: string; repo: string; pullNumber: number; + prTitle: string; + firstCommitMessage?: string; }) { const { checks, @@ -508,6 +525,8 @@ function MergeStatusCard({ owner={owner} repo={repo} pullNumber={pullNumber} + prTitle={prTitle} + firstCommitMessage={firstCommitMessage} /> ); @@ -585,6 +604,11 @@ function ReviewsSection({ + {allReviews.length === 0 && ( +
+

No reviewers added

+
+ )} {allReviews.length > 0 && (
{allReviews.map((review) => ( @@ -1009,6 +1033,8 @@ function MergeFooter({ owner, repo, pullNumber, + prTitle, + firstCommitMessage, }: { isMergeBlocked: boolean; canMerge: boolean; @@ -1016,17 +1042,37 @@ function MergeFooter({ owner: string; repo: string; pullNumber: number; + prTitle: string; + firstCommitMessage?: string; }) { const [mergeMethod, setMergeMethod] = useState<"merge" | "squash" | "rebase">( "squash", ); const [isMerging, setIsMerging] = useState(false); + const [showCommitForm, setShowCommitForm] = useState(false); + const [commitTitle, setCommitTitle] = useState(""); + const [commitDescription, setCommitDescription] = useState(""); const queryClient = useQueryClient(); const currentStrategy = MERGE_STRATEGIES.find((s) => s.value === mergeMethod) ?? MERGE_STRATEGIES[0]; + const getDefaultCommitTitle = useCallback(() => { + if (mergeMethod === "squash") { + return ( + firstCommitMessage?.split("\n")[0] ?? `${prTitle} (#${pullNumber})` + ); + } + return `Merge pull request #${pullNumber}`; + }, [mergeMethod, firstCommitMessage, prTitle, pullNumber]); + + const openCommitForm = () => { + setCommitTitle(getDefaultCommitTitle()); + setCommitDescription(""); + setShowCommitForm(true); + }; + const handleMerge = async () => { setIsMerging(true); try { @@ -1037,6 +1083,8 @@ function MergeFooter({ pullNumber, mergeMethod, bypassProtections: bypass.shouldBypass, + commitTitle: commitTitle || undefined, + commitMessage: commitDescription || undefined, }, }); if (result.ok) { @@ -1057,6 +1105,63 @@ function MergeFooter({ if (!canMerge) return null; + if (showCommitForm) { + return ( +
+
+ + setCommitTitle(e.target.value)} + className="flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + /> +
+
+ +