Skip to content
99 linesCodeBlameRaw
1import { GitBranch, Pencil } from "lucide-react";
2import { useState } from "react";
3import { useFetcher } from "react-router";
4
5import { ErrorText } from "./ui";
6import { Button } from "./ui/button";
7import { Hint } from "./ui/hint";
8import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/command";
9import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
10
11/**
12 * The branch a pull request merges into, and for someone who may change
13 * it, a menu of the repository's branches. The names are fetched when the
14 * menu opens; choosing one posts `action=base` with `base`.
15 */
16export function BaseBranch({
17 base,
18 head,
19 canChange,
20 branchesUrl,
21}: {
22 base: string;
23 /** Its own branch, which it cannot merge into. */
24 head: string | null;
25 canChange: boolean;
26 /** Where the branch names are, as JSON. */
27 branchesUrl: string;
28}) {
29 const names = useFetcher<{ branches: string[] }>();
30 const save = useFetcher<{ error?: string } | null>();
31 const [open, setOpen] = useState(false);
32 const shown = save.state !== "idle" ? String(save.formData?.get("base") ?? base) : base;
33 const choose = (branch: string) => {
34 setOpen(false);
35 if (branch === base) return;
36 const form = new FormData();
37 form.set("action", "base");
38 form.set("base", branch);
39 save.submit(form, { method: "post" });
40 };
41 const name = <span className="font-mono text-fg">{shown}</span>;
42 if (!canChange) return name;
43 return (
44 <>
45 <Popover
46 open={open}
47 onOpenChange={(next) => {
48 setOpen(next);
49 if (next && names.state === "idle" && !names.data) names.load(branchesUrl);
50 }}
51 >
52 <Hint label="Change the branch it merges into">
53 <PopoverTrigger asChild>
54 <Button
55 type="button"
56 variant="ghost"
57 size="inline"
58 className="group gap-1 border border-transparent px-1 font-mono font-normal text-fg hover:border-line"
59 >
60 {shown}
61 <Pencil size={12} className="text-faint group-hover:text-fg" />
62 <span className="sr-only">Change the branch it merges into</span>
63 </Button>
64 </PopoverTrigger>
65 </Hint>
66 <PopoverContent align="start" className="w-72 p-0">
67 <Command>
68 <CommandInput placeholder="Find a branch to merge into" />
69 <CommandList>
70 {names.data ? (
71 <CommandEmpty>No branch by that name.</CommandEmpty>
72 ) : (
73 <p className="px-3 py-4 text-sm text-faint">Reading branches…</p>
74 )}
75 <CommandGroup heading="Merge into">
76 {(names.data?.branches ?? [])
77 .filter((branch) => branch !== head)
78 .map((branch) => (
79 <CommandItem key={branch} value={branch} onSelect={() => choose(branch)}>
80 <GitBranch />
81 <span className="truncate font-mono text-[0.8125rem]">{branch}</span>
82 </CommandItem>
83 ))}
84 </CommandGroup>
85 </CommandList>
86 </Command>
87 <p className="border-t border-line px-3 py-2 text-xs text-faint">
88 Its checks, mergeability and whether it is up to date are worked out against the new base.
89 </p>
90 </PopoverContent>
91 </Popover>
92 {save.data?.error && (
93 <span className="basis-full">
94 <ErrorText>{save.data.error}</ErrorText>
95 </span>
96 )}
97 </>
98 );
99}