From 8f5ec9816f63f8f1a836387a97f995976d0a6cbe Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Sat, 25 Apr 2026 16:05:10 -0400 Subject: [PATCH] feat(dashboard): show "Ready for review" CTA in place of merge button on draft PRs Draft PRs can't be merged on GitHub, so swap the merge footer for a "Ready for review" action that calls the markPullRequestReadyForReview GraphQL mutation. Reviews/checks/conflicts rows still render. --- .../pulls/detail/pull-detail-activity.tsx | 109 ++++++++++++++++-- apps/dashboard/src/lib/github.functions.ts | 29 +++++ 2 files changed, 127 insertions(+), 11 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 4988af9..b7855fe 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx @@ -83,6 +83,7 @@ import { dismissPullReview, getCommentPage, getTimelineEventPage, + markPullReadyForReview, mergePullRequest, replyToReviewComment, requestPullReviewers, @@ -252,6 +253,8 @@ export function PullDetailActivitySection({ pullNumber={pullNumber} prTitle={pr.title} firstCommitMessage={commits?.[0]?.message} + isDraft={pr.isDraft} + pullId={pr.graphqlId} /> )} @@ -326,6 +329,8 @@ function MergeStatusSection({ pullNumber, prTitle, firstCommitMessage, + isDraft, + pullId, }: { scope: GitHubQueryScope; owner: string; @@ -333,6 +338,8 @@ function MergeStatusSection({ pullNumber: number; prTitle: string; firstCommitMessage?: string; + isDraft: boolean; + pullId: string | undefined; }) { const statusQuery = useQuery({ ...githubPullStatusQueryOptions(scope, { owner, repo, pullNumber }), @@ -350,6 +357,8 @@ function MergeStatusSection({ pullNumber={pullNumber} prTitle={prTitle} firstCommitMessage={firstCommitMessage} + isDraft={isDraft} + pullId={pullId} /> ); } @@ -446,6 +455,8 @@ function MergeStatusCard({ pullNumber, prTitle, firstCommitMessage, + isDraft, + pullId, }: { status: PullStatus; owner: string; @@ -453,6 +464,8 @@ function MergeStatusCard({ pullNumber: number; prTitle: string; firstCommitMessage?: string; + isDraft: boolean; + pullId: string | undefined; }) { const { checks, @@ -543,17 +556,26 @@ function MergeStatusCard({ /> )} - {/* Merge action footer */} - + {/* Merge action footer (or "ready for review" CTA when draft) */} + {isDraft ? ( + + ) : ( + + )} ); } @@ -1287,6 +1309,71 @@ function UpdateBranchButton({ ); } +// ── Ready for review footer ───────────────────────────────────────── + +function ReadyForReviewFooter({ + owner, + repo, + pullNumber, + pullId, +}: { + owner: string; + repo: string; + pullNumber: number; + pullId: string | undefined; +}) { + const [isMarking, setIsMarking] = useState(false); + const queryClient = useQueryClient(); + + const handleMarkReady = async () => { + if (!pullId) { + toast.error("Missing pull request id"); + return; + } + setIsMarking(true); + try { + const result = await markPullReadyForReview({ + data: { owner, repo, pullNumber, pullId }, + }); + if (result.ok) { + await queryClient.invalidateQueries({ queryKey: ["github"] }); + } else { + toast.error(result.error); + checkPermissionWarning(result, `${owner}/${repo}`); + setIsMarking(false); + } + } catch { + toast.error("Failed to mark as ready for review"); + setIsMarking(false); + } + }; + + return ( +
+ +

+ This pull request is a draft. Mark it as ready for review to allow + merging. +

+
+ ); +} + // ── Merge footer ──────────────────────────────────────────────────── const MERGE_STRATEGIES = [ diff --git a/apps/dashboard/src/lib/github.functions.ts b/apps/dashboard/src/lib/github.functions.ts index 580ed1f..b504e2d 100644 --- a/apps/dashboard/src/lib/github.functions.ts +++ b/apps/dashboard/src/lib/github.functions.ts @@ -6279,6 +6279,35 @@ export const mergePullRequest = createServerFn({ method: "POST" }) } }); +export type ReadyForReviewInput = PullFromRepoInput & { + /** GraphQL node id of the pull request (e.g. `PR_kwD...`). */ + pullId: string; +}; + +export const markPullReadyForReview = createServerFn({ method: "POST" }) + .inputValidator(identityValidator) + .handler(async ({ data }): Promise => { + const context = await getGitHubUserContextForRepository(data); + if (!context) { + return { ok: false, error: "Not authenticated" }; + } + + try { + await context.octokit.graphql( + `mutation($pullRequestId: ID!) { + markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) { + pullRequest { isDraft } + } + }`, + { pullRequestId: data.pullId }, + ); + await bustPullDetailCaches(context.session.user.id, data); + return { ok: true }; + } catch (error) { + return toMutationError("mark pull request as ready for review", error); + } + }); + export const deleteBranch = createServerFn({ method: "POST" }) .inputValidator( identityValidator<{