Skip to content

Commit 18ca5bf

Browse files
committed
feat(BranchList, branchStore): add branch creation functionality
- Add createBranch method to branchStore for API integration - Implement "Create New Branch" modal in BranchList component - Add branch name input with validation and helper text - Add source branch selector dropdown with current branch indicator - Add error handling and display for branch creation failures - Add loading state during branch creation with disabled button - Add PlusIcon button to branch header for creating new branches - Add @types/react-syntax-highlighter dependency for type support - Improve header layout with flex justify-between for button placement
1 parent 03db427 commit 18ca5bf

3 files changed

Lines changed: 157 additions & 11 deletions

File tree

apps/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@types/node": "^24.10.1",
3030
"@types/react": "^19.2.5",
3131
"@types/react-dom": "^19.2.3",
32+
"@types/react-syntax-highlighter": "15.5.13",
3233
"@vitejs/plugin-react": "^5.1.1",
3334
"eslint": "^9.39.1",
3435
"eslint-plugin-react-hooks": "^7.0.1",

apps/frontend/src/pages/repo/components/BranchList.tsx

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {
22
CodeBracketIcon,
33
MagnifyingGlassIcon,
4+
PlusIcon,
45
} from "@heroicons/react/24/outline";
56
import React, {useMemo, useState} from "react";
6-
import {Badge, Button, Modal} from "~/components/ui";
7+
import {Badge, Button, Input, Modal} from "~/components/ui";
8+
import {useBranchStore} from "~/stores/branchStore";
9+
import {useRepoStore} from "~/stores/repoStore";
710

811
interface BranchListProps {
912
branches: string[];
@@ -20,6 +23,14 @@ export const BranchList: React.FC<BranchListProps> = ({
2023
}) => {
2124
const [isOpen, setIsOpen] = useState(false);
2225
const [searchQuery, setSearchQuery] = useState("");
26+
const [showCreateModal, setShowCreateModal] = useState(false);
27+
const [newBranchName, setNewBranchName] = useState("");
28+
const [sourceBranch, setSourceBranch] = useState(currentBranch || "");
29+
const [creating, setCreating] = useState(false);
30+
const [createError, setCreateError] = useState<string | null>(null);
31+
32+
const createBranch = useBranchStore((s) => s.createBranch);
33+
const selectedRepo = useRepoStore((s) => s.selectedRepo);
2334

2435
const filteredBranches = useMemo(() => {
2536
if (!searchQuery) return branches;
@@ -30,13 +41,52 @@ export const BranchList: React.FC<BranchListProps> = ({
3041

3142
const previewBranches = branches.slice(0, previewCount);
3243

44+
const handleCreateBranch = async () => {
45+
if (!newBranchName.trim() || !sourceBranch.trim() || !selectedRepo) return;
46+
47+
setCreating(true);
48+
setCreateError(null);
49+
50+
try {
51+
await createBranch(
52+
selectedRepo,
53+
newBranchName.trim(),
54+
sourceBranch.trim(),
55+
);
56+
setNewBranchName("");
57+
setShowCreateModal(false);
58+
} catch (err: unknown) {
59+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
60+
const e = err as any;
61+
const msg =
62+
e?.response?.data?.error || e?.message || "Failed to create branch";
63+
setCreateError(msg);
64+
} finally {
65+
setCreating(false);
66+
}
67+
};
68+
3369
return (
3470
<div>
35-
<div className="flex items-center gap-2 mb-4">
36-
<CodeBracketIcon className="w-4 h-4 text-text-tertiary" />
37-
<h2 className="text-sm font-semibold text-text-primary uppercase tracking-wider">
38-
Branches
39-
</h2>
71+
<div className="flex items-center justify-between mb-4">
72+
<div className="flex items-center gap-2">
73+
<CodeBracketIcon className="w-4 h-4 text-text-tertiary" />
74+
<h2 className="text-sm font-semibold text-text-primary uppercase tracking-wider">
75+
Branches
76+
</h2>
77+
</div>
78+
<Button
79+
variant="secondary"
80+
size="sm"
81+
onClick={() => {
82+
setSourceBranch(currentBranch || branches[0] || "");
83+
setShowCreateModal(true);
84+
}}
85+
className="text-xs"
86+
>
87+
<PlusIcon className="w-3.5 h-3.5" />
88+
New
89+
</Button>
4090
</div>
4191

4292
{branches.length === 0 ? (
@@ -67,6 +117,7 @@ export const BranchList: React.FC<BranchListProps> = ({
67117
</div>
68118
)}
69119

120+
{/* View All Branches Modal */}
70121
<Modal
71122
isOpen={isOpen}
72123
onClose={() => setIsOpen(false)}
@@ -108,6 +159,79 @@ export const BranchList: React.FC<BranchListProps> = ({
108159
</div>
109160
</div>
110161
</Modal>
162+
163+
{/* Create Branch Modal */}
164+
<Modal
165+
isOpen={showCreateModal}
166+
onClose={() => {
167+
setShowCreateModal(false);
168+
setCreateError(null);
169+
setNewBranchName("");
170+
}}
171+
title="Create New Branch"
172+
size="sm"
173+
closeOnBackdrop={true}
174+
footer={
175+
<div className="flex gap-3 w-full sm:w-auto">
176+
<Button
177+
variant="secondary"
178+
onClick={() => {
179+
setShowCreateModal(false);
180+
setCreateError(null);
181+
setNewBranchName("");
182+
}}
183+
className="flex-1 sm:flex-none"
184+
>
185+
Cancel
186+
</Button>
187+
<Button
188+
onClick={handleCreateBranch}
189+
disabled={creating || !newBranchName.trim()}
190+
className="flex-1 sm:flex-none"
191+
>
192+
{creating ? "Creating..." : "Create Branch"}
193+
</Button>
194+
</div>
195+
}
196+
>
197+
<div className="space-y-4">
198+
{createError && (
199+
<div className="bg-error/10 border border-error/20 text-error text-sm p-3 rounded-lg">
200+
{createError}
201+
</div>
202+
)}
203+
204+
<Input
205+
label="Branch Name"
206+
placeholder="feature/my-new-branch"
207+
value={newBranchName}
208+
onChange={(e) => setNewBranchName(e.target.value)}
209+
required
210+
helperText="Use lowercase with hyphens or slashes."
211+
/>
212+
213+
<div>
214+
<label className="block text-sm font-medium text-text-primary mb-1.5">
215+
Source Branch
216+
</label>
217+
<select
218+
value={sourceBranch}
219+
onChange={(e) => setSourceBranch(e.target.value)}
220+
className="w-full h-9 px-3 bg-app-surface border border-app-border rounded text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-app-accent focus:border-app-accent transition-colors"
221+
>
222+
{branches.map((b) => (
223+
<option key={b} value={b}>
224+
{b}
225+
{b === currentBranch ? " (current)" : ""}
226+
</option>
227+
))}
228+
</select>
229+
<p className="text-xs text-text-tertiary mt-1">
230+
The new branch will be created from this branch.
231+
</p>
232+
</div>
233+
</div>
234+
</Modal>
111235
</div>
112236
);
113237
};

apps/frontend/src/stores/branchStore.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import axios from "axios";
2-
import { create } from "zustand";
3-
import { createJSONStorage, persist } from "zustand/middleware";
2+
import {create} from "zustand";
3+
import {createJSONStorage, persist} from "zustand/middleware";
44

55
interface BranchStore {
66
branches: string[];
77
currentBranch: string | null;
88
setCurrentBranch: (branch: string | null) => void;
99
fetchBranches: (repo: string) => Promise<void>;
10+
createBranch: (
11+
repo: string,
12+
newBranch: string,
13+
sourceBranch: string,
14+
) => Promise<void>;
1015
}
1116

1217
const STORAGE_KEY = "branch-store-v1";
@@ -17,16 +22,19 @@ export const useBranchStore = create<BranchStore>()(
1722
branches: [],
1823
currentBranch: null,
1924

20-
setCurrentBranch: (branch) => set({ currentBranch: branch }),
25+
setCurrentBranch: (branch) => set({currentBranch: branch}),
2126

2227
fetchBranches: async (repo) => {
2328
try {
2429
const repoWithGit = repo.includes(".git") ? repo : `${repo}.git`;
2530
const res = await axios.get(`/api/repos/${repoWithGit}/branches`);
2631

27-
const branches: string[] = Array.isArray(res.data?.branches) ? res.data.branches : [];
32+
const branches: string[] = Array.isArray(res.data?.branches)
33+
? res.data.branches
34+
: [];
2835

29-
const apiCurrent: string | null = typeof res.data?.current === "string" ? res.data.current : null;
36+
const apiCurrent: string | null =
37+
typeof res.data?.current === "string" ? res.data.current : null;
3038

3139
// Keep persisted currentBranch if API doesn't provide one (or provides invalid)
3240
const nextCurrent =
@@ -48,6 +56,19 @@ export const useBranchStore = create<BranchStore>()(
4856
});
4957
}
5058
},
59+
60+
createBranch: async (repo, newBranch, sourceBranch) => {
61+
const repoWithGit = repo.includes(".git") ? repo : `${repo}.git`;
62+
await axios.post(
63+
`/api/repos/${encodeURIComponent(repoWithGit)}/branches`,
64+
{
65+
newBranch,
66+
sourceBranch,
67+
},
68+
);
69+
// Refresh branches after creation
70+
await get().fetchBranches(repo);
71+
},
5172
}),
5273
{
5374
name: STORAGE_KEY,

0 commit comments

Comments
 (0)