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 8a9b020..e9e2642 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx @@ -82,6 +82,7 @@ import { dismissPullReview, getCommentPage, getTimelineEventPage, + markPullReadyForReview, mergePullRequest, replyToReviewComment, requestPullReviewers, @@ -254,6 +255,8 @@ export function PullDetailActivitySection({ pullNumber={pullNumber} prTitle={pr.title} firstCommitMessage={commits?.[0]?.message} + isDraft={pr.isDraft} + pullId={pr.graphqlId} /> )} @@ -328,6 +331,8 @@ function MergeStatusSection({ pullNumber, prTitle, firstCommitMessage, + isDraft, + pullId, }: { scope: GitHubQueryScope; owner: string; @@ -335,6 +340,8 @@ function MergeStatusSection({ pullNumber: number; prTitle: string; firstCommitMessage?: string; + isDraft: boolean; + pullId: string | undefined; }) { const input = useMemo( () => ({ owner, repo, pullNumber }), @@ -389,6 +396,8 @@ function MergeStatusSection({ pullNumber={pullNumber} prTitle={prTitle} firstCommitMessage={firstCommitMessage} + isDraft={isDraft} + pullId={pullId} /> ); } @@ -486,6 +495,8 @@ function MergeStatusCard({ pullNumber, prTitle, firstCommitMessage, + isDraft, + pullId, }: { scope: GitHubQueryScope; status: PullStatus; @@ -494,6 +505,8 @@ function MergeStatusCard({ pullNumber: number; prTitle: string; firstCommitMessage?: string; + isDraft: boolean; + pullId: string | undefined; }) { const { checks, @@ -585,18 +598,27 @@ function MergeStatusCard({ /> )} - {/* Merge action footer */} - + {/* Merge action footer (or "ready for review" CTA when draft) */} + {isDraft ? ( + + ) : ( + + )} ); } @@ -1330,6 +1352,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 21c26f4..8122bd4 100644 --- a/apps/dashboard/src/lib/github.functions.ts +++ b/apps/dashboard/src/lib/github.functions.ts @@ -6402,6 +6402,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<{