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 (
No reviewers added
+- 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.
diff --git a/apps/dashboard/src/components/pulls/detail/use-merge-bypass.ts b/apps/dashboard/src/components/pulls/detail/use-merge-bypass.ts index 3657dcc..e75de17 100644 --- a/apps/dashboard/src/components/pulls/detail/use-merge-bypass.ts +++ b/apps/dashboard/src/components/pulls/detail/use-merge-bypass.ts @@ -3,21 +3,11 @@ import { useEffect, useState } from "react"; export function useMergeBypass({ isMergeBlocked, canBypassProtections, - hasCheckFailures, - hasReviewIssue, hasConflicts, - isBehind, - allChecksPassed, - totalChecks, }: { isMergeBlocked: boolean; canBypassProtections: boolean; - hasCheckFailures: boolean; - hasReviewIssue: boolean; hasConflicts: boolean; - isBehind: boolean; - allChecksPassed: boolean; - totalChecks: number; }) { const [checked, setChecked] = useState(false); @@ -25,18 +15,14 @@ export function useMergeBypass({ if (!isMergeBlocked) setChecked(false); }, [isMergeBlocked]); - const allCriteriaMet = - !hasCheckFailures && - !hasReviewIssue && - !hasConflicts && - !isBehind && - (totalChecks === 0 || allChecksPassed); - - const auto = canBypassProtections && isMergeBlocked && allCriteriaMet; + // Bypass is only relevant when merge is blocked by branch protection rules, + // not by conflicts — GitHub will always reject merging conflicting branches + // regardless of bypass permissions. + const canBypass = isMergeBlocked && canBypassProtections && !hasConflicts; return { - shouldBypass: auto || checked, - showOption: isMergeBlocked && canBypassProtections && !allCriteriaMet, + shouldBypass: canBypass && checked, + showOption: canBypass, checked, setChecked, }; diff --git a/apps/dashboard/src/lib/github.functions.ts b/apps/dashboard/src/lib/github.functions.ts index f8928db..81ff697 100644 --- a/apps/dashboard/src/lib/github.functions.ts +++ b/apps/dashboard/src/lib/github.functions.ts @@ -4935,6 +4935,35 @@ export const updatePullBody = createServerFn({ method: "POST" }) } }); +type UpdatePullStateInput = PullFromRepoInput & { + state: "open" | "closed"; +}; + +export const updatePullState = createServerFn({ method: "POST" }) + .inputValidator(identityValidator