feat: unified git repo hook + fix branch dropdown in workspace creation
- New useGitRepo hook: centralizes all git operations (branches, status, history, commit, push, pull, fetch, checkout, create/delete branch, merge) for a given project+repo. Auto-refreshes after mutating ops. - Fix WorkspaceCreateForm branch dropdown: - Always renders <select> (never input fallback) - Uses useGitRepo for branch fetching with loading/error states - Shows 'Loading branches...' while fetching - Shows 'Enter branch name manually...' if API fails - '+ Create new branch...' option with text input reveal - Auto-selects default branch on load - ProjectsPage uses updated form props (defaultProjectId/defaultRepoId) Quality gates: tsc --noEmit clean, eslint clean
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Icon } from "./icon";
|
||||
import { listProjects } from "../api/projects";
|
||||
import { listRepositories, listRepositoryBranches } from "../api/git_repositories";
|
||||
import { listRepositories } from "../api/git_repositories";
|
||||
import { createWorkspaceTopLevel } from "../api/workspaces";
|
||||
import { useGitRepo } from "../hooks/use-git-repo";
|
||||
import type { ProjectWithRepos } from "../types";
|
||||
import type { GitRepository } from "../api/git_repositories";
|
||||
|
||||
@@ -29,17 +30,36 @@ export function WorkspaceCreateForm({
|
||||
|
||||
const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
|
||||
const [repos, setRepos] = useState<GitRepository[]>([]);
|
||||
const [branches, setBranches] = useState<string[]>([]);
|
||||
const [selectedProject, setSelectedProject] = useState(defaultProjectId ?? "");
|
||||
const [selectedProject, setSelectedProject] = useState(
|
||||
defaultProjectId ?? "",
|
||||
);
|
||||
const [selectedRepo, setSelectedRepo] = useState(defaultRepoId ?? "");
|
||||
const [selectedBranch, setSelectedBranch] = useState("");
|
||||
const [newBranchName, setNewBranchName] = useState("");
|
||||
const [isNewBranch, setIsNewBranch] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [fetching, setFetching] = useState(!isContextual);
|
||||
const [fetchingProjects, setFetchingProjects] = useState(!isContextual);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/* Git repo hook handles branch fetching, loading, errors */
|
||||
const git = useGitRepo(
|
||||
selectedProject || undefined,
|
||||
selectedRepo || undefined,
|
||||
);
|
||||
|
||||
/* Sync local branch state with hook data */
|
||||
useEffect(() => {
|
||||
if (git.branches.length > 0 && !selectedBranch) {
|
||||
const preferred =
|
||||
git.defaultBranch && git.branches.includes(git.defaultBranch)
|
||||
? git.defaultBranch
|
||||
: git.branches[0];
|
||||
setSelectedBranch(preferred);
|
||||
setIsNewBranch(false);
|
||||
}
|
||||
}, [git.branches, git.defaultBranch, selectedBranch]);
|
||||
|
||||
/* ── Load projects (standalone mode only) ── */
|
||||
const loadProjects = useCallback(async () => {
|
||||
if (isContextual) return;
|
||||
@@ -52,7 +72,7 @@ export function WorkspaceCreateForm({
|
||||
} catch {
|
||||
setError("Failed to load projects");
|
||||
} finally {
|
||||
setFetching(false);
|
||||
setFetchingProjects(false);
|
||||
}
|
||||
}, [isContextual, defaultProjectId]);
|
||||
|
||||
@@ -81,42 +101,15 @@ export function WorkspaceCreateForm({
|
||||
void loadRepos();
|
||||
}, [selectedProject, defaultRepoId]);
|
||||
|
||||
/* ── Load branches when repo changes ── */
|
||||
useEffect(() => {
|
||||
if (!selectedProject || !selectedRepo) {
|
||||
setBranches([]);
|
||||
setSelectedBranch("");
|
||||
setIsNewBranch(false);
|
||||
return;
|
||||
}
|
||||
const loadBranches = async () => {
|
||||
try {
|
||||
const data = await listRepositoryBranches(selectedProject, selectedRepo);
|
||||
const branchNames = data.branches.map((b) => b.name);
|
||||
setBranches(branchNames);
|
||||
if (branchNames.length >= 1) {
|
||||
// Prefer default branch, else first branch
|
||||
const preferred = data.default_branch && branchNames.includes(data.default_branch)
|
||||
? data.default_branch
|
||||
: branchNames[0];
|
||||
setSelectedBranch(preferred);
|
||||
setIsNewBranch(false);
|
||||
}
|
||||
} catch {
|
||||
// Fallback to free-text branch input
|
||||
setBranches([]);
|
||||
setIsNewBranch(true);
|
||||
setSelectedBranch("__new__");
|
||||
}
|
||||
};
|
||||
void loadBranches();
|
||||
}, [selectedProject, selectedRepo]);
|
||||
|
||||
const handleBranchChange = (value: string) => {
|
||||
if (value === "__new__") {
|
||||
setIsNewBranch(true);
|
||||
setSelectedBranch("__new__");
|
||||
setNewBranchName("");
|
||||
} else if (value === "__manual__") {
|
||||
setIsNewBranch(true);
|
||||
setSelectedBranch("__manual__");
|
||||
setNewBranchName("");
|
||||
} else {
|
||||
setIsNewBranch(false);
|
||||
setSelectedBranch(value);
|
||||
@@ -156,7 +149,10 @@ export function WorkspaceCreateForm({
|
||||
}
|
||||
};
|
||||
|
||||
if (fetching) {
|
||||
/* Show single combined error */
|
||||
const displayError = error || git.error;
|
||||
|
||||
if (fetchingProjects) {
|
||||
return (
|
||||
<div className="card workspace-create-inline">
|
||||
<p className="muted">Loading projects...</p>
|
||||
@@ -164,6 +160,9 @@ export function WorkspaceCreateForm({
|
||||
);
|
||||
}
|
||||
|
||||
const branchSelectDisabled =
|
||||
!selectedRepo || submitting || (git.loading && git.branches.length === 0);
|
||||
|
||||
return (
|
||||
<div className="card workspace-create-inline">
|
||||
<h3>
|
||||
@@ -176,7 +175,10 @@ export function WorkspaceCreateForm({
|
||||
<label>Project</label>
|
||||
<select
|
||||
value={selectedProject}
|
||||
onChange={(e) => setSelectedProject(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setSelectedProject(e.target.value);
|
||||
setSelectedBranch("");
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">Select project...</option>
|
||||
@@ -195,7 +197,10 @@ export function WorkspaceCreateForm({
|
||||
<label>Repository</label>
|
||||
<select
|
||||
value={selectedRepo}
|
||||
onChange={(e) => setSelectedRepo(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setSelectedRepo(e.target.value);
|
||||
setSelectedBranch("");
|
||||
}}
|
||||
required
|
||||
disabled={!selectedProject || repos.length === 0}
|
||||
>
|
||||
@@ -231,53 +236,52 @@ export function WorkspaceCreateForm({
|
||||
<label>
|
||||
<Icon name="branch" size="sm" /> Branch
|
||||
</label>
|
||||
{branches.length > 0 ? (
|
||||
<>
|
||||
<select
|
||||
value={selectedBranch}
|
||||
onChange={(e) => handleBranchChange(e.target.value)}
|
||||
required
|
||||
disabled={!selectedRepo || submitting}
|
||||
>
|
||||
<option value="">Select branch...</option>
|
||||
{branches.map((b) => (
|
||||
<option key={b} value={b}>
|
||||
{b}
|
||||
</option>
|
||||
))}
|
||||
<option value="__new__">+ Create new branch...</option>
|
||||
</select>
|
||||
{isNewBranch && (
|
||||
<input
|
||||
type="text"
|
||||
value={newBranchName}
|
||||
onChange={(e) => setNewBranchName(e.target.value)}
|
||||
placeholder="new-branch-name"
|
||||
required
|
||||
style={{ marginTop: "0.5rem" }}
|
||||
disabled={submitting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<select
|
||||
value={selectedBranch}
|
||||
onChange={(e) => handleBranchChange(e.target.value)}
|
||||
required
|
||||
disabled={branchSelectDisabled}
|
||||
>
|
||||
<option value="">
|
||||
{git.loading && git.branches.length === 0
|
||||
? "Loading branches..."
|
||||
: !selectedRepo
|
||||
? "Select a repository first"
|
||||
: "Select branch..."}
|
||||
</option>
|
||||
|
||||
{git.branches.map((b) => (
|
||||
<option key={b} value={b}>
|
||||
{b}
|
||||
{b === git.defaultBranch ? " (default)" : ""}
|
||||
</option>
|
||||
))}
|
||||
|
||||
<option value="__new__">+ Create new branch...</option>
|
||||
|
||||
{/* If branch fetch failed, offer manual entry */}
|
||||
{git.error && git.branches.length === 0 && (
|
||||
<option value="__manual__">✎ Enter branch name manually...</option>
|
||||
)}
|
||||
</select>
|
||||
|
||||
{/* New branch text input */}
|
||||
{isNewBranch && (
|
||||
<input
|
||||
type="text"
|
||||
value={isNewBranch ? newBranchName : selectedBranch}
|
||||
onChange={(e) => {
|
||||
setIsNewBranch(true);
|
||||
setNewBranchName(e.target.value);
|
||||
setSelectedBranch("__new__");
|
||||
}}
|
||||
placeholder="main"
|
||||
value={newBranchName}
|
||||
onChange={(e) => setNewBranchName(e.target.value)}
|
||||
placeholder="new-branch-name"
|
||||
required
|
||||
disabled={!selectedRepo || submitting}
|
||||
style={{ marginTop: "0.5rem" }}
|
||||
disabled={submitting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
{displayError && (
|
||||
<div className="form-error" style={{ gridColumn: "1 / -1" }}>
|
||||
{error}
|
||||
{displayError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user