| 1 | "use client"; |
| 2 | |
| 3 | import { CircleCheck, CircleX |
| 4 | import Link from "next/link |
| 5 | import { usePathname, useRouter |
| 6 | import { useAction } |
| 7 | import { toast } |
| 8 | import { closePullRequest, setDraft |
| 9 | import { Button } |
| 10 | import { Spinner } |
| 11 | import { UserLink } |
| 12 | import { cn } |
| 13 | import { MergeButton } |
| 14 | |
| 15 | export function PullRequestNav({ |
| 16 | base, |
| 17 | commitCount, |
| 18 | changedFiles, |
| 19 | additions, |
| 20 | deletions, |
| 21 | }: { |
| 22 | base: string |
| 23 | commitCount: number |
| 24 | changedFiles: number |
| 25 | additions: number |
| 26 | deletions: number |
| 27 | }) { |
| 28 | const pathname = |
| 29 | const tabs = |
| 30 | { href: base, |
| 31 | { href: `${base |
| 32 | { href: `${base |
| 33 | ]; |
| 34 | |
| 35 | return ( |
| 36 | // the tab labels carry counts, so on a phone the row wraps instead of pushing the whole page sideways |
| 37 | <nav className="flex flex-wrap items-center gap-1 border-b |
| 38 | {tabs.map(( |
| 39 | <Link |
| 40 | key={tab.href |
| 41 | href={tab.href |
| 42 | className={cn( |
| 43 | "-mb-px whitespace-nowrap border-b-2 px-3 py-2 text-sm", |
| 44 | pathname === tab. |
| 45 | ? "border-primary font-medium" |
| 46 | : "border-transparent text-muted-foreground hover:text-foreground", |
| 47 | )} |
| 48 | > |
| 49 | {tab.label} |
| 50 | {tab.count !== |
| 51 | <span className="ml-1.5 rounded-full bg-muted px-1.5 py-0.5 text-xs tabular-nums |
| 52 | {tab.count} |
| 53 | </span> |
| 54 | )} |
| 55 | </Link> |
| 56 | ))} |
| 57 | <div className="ml-auto flex flex-row items-center justify-center gap-1 text-xs |
| 58 | <span className="text-green-400 |
| 59 | <span className="text-red-400 |
| 60 | </div> |
| 61 | </nav> |
| 62 | ); |
| 63 | } |
| 64 | |
| 65 | export function MergePanel({ |
| 66 | username, |
| 67 | repo, |
| 68 | number, |
| 69 | state, |
| 70 | mergeable, |
| 71 | conflicts, |
| 72 | canMerge, |
| 73 | isAuthor, |
| 74 | mergeCommitSha, |
| 75 | draft, |
| 76 | squash, |
| 77 | reviewers, |
| 78 | }: { |
| 79 | username: string |
| 80 | repo: string |
| 81 | number: number |
| 82 | state: string |
| 83 | mergeable: boolean |
| 84 | /** Paths that conflict with the base right now. */ |
| 85 | conflicts: string |
| 86 | canMerge: boolean |
| 87 | isAuthor: boolean |
| 88 | mergeCommitSha: string |
| 89 | draft: boolean |
| 90 | squash: { |
| 91 | reviewers: { |
| 92 | username: string |
| 93 | image: string |
| 94 | state: "approved |
| 95 | }[]; |
| 96 | }) { |
| 97 | const router = |
| 98 | |
| 99 | const draftToggle = |
| 100 | onSuccess: ({ input |
| 101 | toast.success( |
| 102 | input.draft |
| 103 | ? "Pull request converted to a draft." |
| 104 | : "Pull request marked ready for review.", |
| 105 | ); |
| 106 | router.refresh(); |
| 107 | }, |
| 108 | onError: ({ error |
| 109 | toast.error(error |
| 110 | }); |
| 111 | |
| 112 | const close = |
| 113 | onSuccess: () => |
| 114 | toast.success("Pull request closed. |
| 115 | router.refresh(); |
| 116 | }, |
| 117 | onError: ({ error |
| 118 | toast.error(error |
| 119 | }); |
| 120 | |
| 121 | if (state === |
| 122 | return ( |
| 123 | <div className="rounded-lg border px-4 py-3 text-sm |
| 124 | <p className="flex items-center gap-2 font-medium text-violet-500 |
| 125 | <GitMerge className=" |
| 126 | Merged |
| 127 | </p> |
| 128 | {mergeCommitSha && ( |
| 129 | <p className="mt-1 text-xs text-muted-foreground |
| 130 | Merged as{" "} |
| 131 | <Link |
| 132 | href={`/${username |
| 133 | className="font-mono hover:underline" |
| 134 | > |
| 135 | {mergeCommitSha.slice(0 |
| 136 | </Link> |
| 137 | </p> |
| 138 | )} |
| 139 | </div> |
| 140 | ); |
| 141 | } |
| 142 | |
| 143 | if (state === |
| 144 | return ( |
| 145 | <div className="rounded-lg border px-4 py-3 text-sm text-muted-foreground |
| 146 | This pull request was closed without merging. |
| 147 | </div> |
| 148 | ); |
| 149 | } |
| 150 | |
| 151 | return ( |
| 152 | <div className="flex flex-col gap-3 rounded-lg border px-4 py-3 |
| 153 | {reviewers.length > |
| 154 | <ul className="flex flex-col gap-1 text-sm |
| 155 | {reviewers.map(( |
| 156 | <li key={reviewer |
| 157 | {reviewer.state === |
| 158 | <CircleCheck className=" |
| 159 | ) : ( |
| 160 | <CircleX className=" |
| 161 | )} |
| 162 | <UserLink |
| 163 | username={reviewer.username |
| 164 | image={reviewer.image |
| 165 | avatar="sm" |
| 166 | /> |
| 167 | <span className="text-muted-foreground |
| 168 | {reviewer.state === |
| 169 | ? "approved" |
| 170 | : "requested changes"} |
| 171 | </span> |
| 172 | </li> |
| 173 | ))} |
| 174 | </ul> |
| 175 | )} |
| 176 | |
| 177 | <p className="text-sm |
| 178 | {draft |
| 179 | ? "This pull request is still a draft and cannot be merged." |
| 180 | : mergeable |
| 181 | ? "This branch has no conflicts with the base branch." |
| 182 | : "This branch has conflicts that must be resolved locally."} |
| 183 | </p> |
| 184 | |
| 185 | {conflicts.length > |
| 186 | <ul className="flex flex-col gap-1 rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs |
| 187 | {conflicts.map(( |
| 188 | <li key={path |
| 189 | {path} |
| 190 | </li> |
| 191 | ))} |
| 192 | </ul> |
| 193 | )} |
| 194 | |
| 195 | <div className="flex flex-wrap items-start gap-2 |
| 196 | <div className="flex min-w-0 flex-1 |
| 197 | {draft |
| 198 | ? (canMerge || |
| 199 | <Button |
| 200 | size="sm" |
| 201 | disabled={draftToggle.isExecuting |
| 202 | onClick={() => |
| 203 | draftToggle.execute({ |
| 204 | username, |
| 205 | repo, |
| 206 | number, |
| 207 | draft: false, |
| 208 | }) |
| 209 | } |
| 210 | > |
| 211 | {draftToggle.isExecuting && |
| 212 | Ready for review |
| 213 | </Button> |
| 214 | ) |
| 215 | : canMerge && ( |
| 216 | <MergeButton |
| 217 | username={username} |
| 218 | repo={repo} |
| 219 | number={number} |
| 220 | disabled={!mergeable} |
| 221 | squash={squash} |
| 222 | /> |
| 223 | )} |
| 224 | </div> |
| 225 | |
| 226 | {(canMerge || isAuthor |
| 227 | <div className="flex gap-2 |
| 228 | {!draft && ( |
| 229 | <Button |
| 230 | size="sm" |
| 231 | variant="ghost" |
| 232 | disabled={draftToggle.isExecuting |
| 233 | onClick={() => |
| 234 | draftToggle.execute({ |
| 235 | } |
| 236 | > |
| 237 | {draftToggle.isExecuting && |
| 238 | Convert to draft |
| 239 | </Button> |
| 240 | )} |
| 241 | <Button |
| 242 | size="sm" |
| 243 | variant="outline" |
| 244 | disabled={close.isExecuting |
| 245 | onClick={() => |
| 246 | > |
| 247 | {close.isExecuting && |
| 248 | Close pull request |
| 249 | </Button> |
| 250 | </div> |
| 251 | )} |
| 252 | </div> |
| 253 | </div> |
| 254 | ); |
| 255 | } |