Skip to content

Commit

Permalink
If branch not found, show option to create new branch from input (#4741)
Browse files Browse the repository at this point in the history
  • Loading branch information
bilalabbad authored Oct 25, 2024
1 parent 2fbadce commit d7784f5
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 30 deletions.
6 changes: 6 additions & 0 deletions changelog/new-layout.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Reworked branch selector:
- Redesigned the UI
- Added filter for branch
- Improved accessibility & keyboard navigation
- Improved UX on new branch form
- Added quick link to view all branches
87 changes: 58 additions & 29 deletions frontend/app/src/components/branch-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@ import { Branch } from "@/generated/graphql";
import { branchesState, currentBranchAtom } from "@/state/atoms/branches.atom";
import { branchesToSelectOptions } from "@/utils/branches";
import { Icon } from "@iconify-icon/react";
import { useAtomValue } from "jotai/index";
import { useAtomValue, useSetAtom } from "jotai";
import { useEffect, useState } from "react";
import { StringParam, useQueryParam } from "use-query-params";

import { ComboboxItem } from "@/components/ui/combobox";
import { Command, CommandEmpty, CommandInput, CommandList } from "@/components/ui/command";
import { Command, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import graphqlClient from "@/graphql/graphqlClientApollo";
import { useAuth } from "@/hooks/useAuth";
import { constructPath } from "@/utils/fetch";
import { useSetAtom } from "jotai";
import { useCommandState } from "cmdk";
import { Button, ButtonWithTooltip, LinkButton } from "./buttons/button-primitive";
import BranchCreateForm from "./form/branch-create-form";

type DisplayForm = {
open: boolean;
defaultBranchName?: string;
};

export default function BranchSelector() {
const currentBranch = useAtomValue(currentBranchAtom);
const [isOpen, setIsOpen] = useState(false);
const [displayForm, setDisplayForm] = useState(false);
const [displayForm, setDisplayForm] = useState<DisplayForm>({ open: false });

useEffect(() => {
if (isOpen) graphqlClient.refetchQueries({ include: ["GetBranches"] });
Expand All @@ -30,7 +35,7 @@ export default function BranchSelector() {
<Popover
open={isOpen}
onOpenChange={(open) => {
setDisplayForm(false);
setDisplayForm({ open: false });
setIsOpen(open);
}}
>
Expand All @@ -50,15 +55,14 @@ export default function BranchSelector() {
</PopoverTrigger>

<PopoverContent align="start">
{displayForm ? (
{displayForm.open ? (
<BranchCreateForm
onCancel={() => {
setDisplayForm(false);
}}
onCancel={() => setDisplayForm({ open: false })}
onSuccess={() => {
setDisplayForm(false);
setDisplayForm({ open: false });
setIsOpen(false);
}}
defaultBranchName={displayForm.defaultBranchName}
data-testid="branch-create-form"
/>
) : (
Expand All @@ -74,18 +78,14 @@ function BranchSelect({
setFormOpen,
}: {
setPopoverOpen: (open: boolean) => void;
setFormOpen: (open: boolean) => void;
setFormOpen: (displayForm: DisplayForm) => void;
}) {
const branches = useAtomValue(branchesState);
const setCurrentBranch = useSetAtom(currentBranchAtom);
const [, setBranchInQueryString] = useQueryParam(QSP.BRANCH, StringParam);

const handleBranchChange = (branch: Branch) => {
if (branch.is_default) {
setBranchInQueryString(undefined); // undefined is needed to remove a parameter from the QSP
} else {
setBranchInQueryString(branch.name);
}
setBranchInQueryString(branch.is_default ? undefined : branch.name);
setCurrentBranch(branch);
setPopoverOpen(false);
};
Expand All @@ -110,7 +110,10 @@ function BranchSelect({
</div>

<CommandList className="p-0" data-testid="branch-list">
<CommandEmpty>No branch found</CommandEmpty>
<BranchNotFound
onSelect={(defaultBranchName) => setFormOpen({ open: true, defaultBranchName })}
/>

{branchesToSelectOptions(branches).map((branch) => (
<BranchOption
key={branch.name}
Expand Down Expand Up @@ -161,28 +164,54 @@ function BranchOption({ branch, onChange }: { branch: Branch; onChange: () => vo
);
}

export const BranchFormTriggerButton = ({ setOpen }: { setOpen: (open: boolean) => void }) => {
export const BranchFormTriggerButton = ({
setOpen,
}: {
setOpen: (displayForm: DisplayForm) => void;
}) => {
const { isAuthenticated } = useAuth();

const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
setOpen({ open: true });
};

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.stopPropagation();
setOpen({ open: true });
}
};

return (
<ButtonWithTooltip
disabled={!isAuthenticated}
tooltipEnabled={!isAuthenticated}
tooltipContent={"You need to be authenticated."}
tooltipContent="You need to be authenticated."
className="h-8 w-8 shadow-none"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.stopPropagation();
setOpen(true);
}
}}
onClick={(e) => {
e.stopPropagation();
setOpen(true);
}}
onKeyDown={handleKeyDown}
onClick={handleClick}
data-testid="create-branch-button"
>
<Icon icon="mdi:plus" />
</ButtonWithTooltip>
);
};

const BranchNotFound = ({ onSelect }: { onSelect: (branchName: string) => void }) => {
const filteredCount = useCommandState((state) => state.filtered.count);
const search = useCommandState((state) => state.search);

if (filteredCount !== 0) return null;

return (
<CommandItem
forceMount
value="create"
onSelect={() => onSelect(search)}
className="text-neutral-600 truncate gap-1"
>
Create branch <span className="font-semibold text-neutral-800">{search}</span>
</CommandItem>
);
};
7 changes: 6 additions & 1 deletion frontend/app/src/components/form/branch-create-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ type BranchFormData = {
type BranchCreateFormProps = {
onCancel?: () => void;
onSuccess?: (branch: Branch) => void;
defaultBranchName?: string;
};

const BranchCreateForm = ({ onCancel, onSuccess }: BranchCreateFormProps) => {
const BranchCreateForm = ({ defaultBranchName, onCancel, onSuccess }: BranchCreateFormProps) => {
const [branches, setBranches] = useAtom(branchesState);
const [, setBranchInQueryString] = useQueryParam(QSP.BRANCH, StringParam);
const [createBranch] = useMutation(BRANCH_CREATE);
Expand Down Expand Up @@ -60,6 +61,10 @@ const BranchCreateForm = ({ onCancel, onSuccess }: BranchCreateFormProps) => {
<InputField
name="name"
label="New branch name"
defaultValue={
defaultBranchName ? { source: { type: "user" }, value: defaultBranchName } : undefined
}
autoFocus
rules={{
required: true,
validate: {
Expand Down
8 changes: 8 additions & 0 deletions frontend/app/tests/e2e/branches.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,12 @@ test.describe("Branches creation and deletion", () => {
await expect(page.getByTestId("branch-list")).not.toContainText("test123");
});
});

test("allow to create a branch with a name that does not exists", async ({ page }) => {
await page.goto("/");
await page.getByTestId("branch-selector-trigger").click();
await page.getByTestId("branch-search-input").fill("quick-branch-form");
await page.getByRole("option", { name: "Create branch quick-branch-form" }).click();
await expect(page.getByLabel("New branch name *")).toHaveValue("quick-branch-form");
});
});

0 comments on commit d7784f5

Please sign in to comment.