feat: workspace creation with branch dropdown and auto-select
- Fetch branches from selected repo via listRepositoryBranches API - Branch dropdown with default branch pre-selected - '+ Create new branch...' option reveals text input for custom branch - Auto-select first option when only one available: - Project: auto-selects when only 1 project - Repo: auto-selects when only 1 repo - Branch: auto-selects when only 1 branch, otherwise defaults to remote default - Falls back to free-text branch input if branch API fails - TypeScript + eslint clean
This commit is contained in:
@@ -8,7 +8,7 @@ import { WorkspaceCard } from "../components/workspace-card";
|
|||||||
import { StartToolModal } from "../components/start-tool-modal";
|
import { StartToolModal } from "../components/start-tool-modal";
|
||||||
import { createInstance, startInstance } from "../api/sessions";
|
import { createInstance, startInstance } from "../api/sessions";
|
||||||
import { listProjects } from "../api/projects";
|
import { listProjects } from "../api/projects";
|
||||||
import { listRepositories } from "../api/git_repositories";
|
import { listRepositories, listRepositoryBranches } from "../api/git_repositories";
|
||||||
import { createWorkspaceTopLevel } from "../api/workspaces";
|
import { createWorkspaceTopLevel } from "../api/workspaces";
|
||||||
import type { Workspace } from "../types/workspace";
|
import type { Workspace } from "../types/workspace";
|
||||||
import type { ProjectWithRepos } from "../types";
|
import type { ProjectWithRepos } from "../types";
|
||||||
@@ -152,10 +152,13 @@ function WorkspaceCreateInline({
|
|||||||
}) {
|
}) {
|
||||||
const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
|
const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
|
||||||
const [repos, setRepos] = useState<GitRepository[]>([]);
|
const [repos, setRepos] = useState<GitRepository[]>([]);
|
||||||
|
const [branches, setBranches] = useState<string[]>([]);
|
||||||
const [selectedProject, setSelectedProject] = useState("");
|
const [selectedProject, setSelectedProject] = useState("");
|
||||||
const [selectedRepo, setSelectedRepo] = useState("");
|
const [selectedRepo, setSelectedRepo] = useState("");
|
||||||
|
const [selectedBranch, setSelectedBranch] = useState("");
|
||||||
|
const [newBranchName, setNewBranchName] = useState("");
|
||||||
|
const [isNewBranch, setIsNewBranch] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [branch, setBranch] = useState("main");
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [fetching, setFetching] = useState(true);
|
const [fetching, setFetching] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -198,6 +201,45 @@ function WorkspaceCreateInline({
|
|||||||
void loadRepos();
|
void loadRepos();
|
||||||
}, [selectedProject]);
|
}, [selectedProject]);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
setSelectedBranch(branchNames[0]);
|
||||||
|
setIsNewBranch(false);
|
||||||
|
} else if (data.default_branch) {
|
||||||
|
setSelectedBranch(data.default_branch);
|
||||||
|
setIsNewBranch(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If branch fetch fails, fall back to free-text
|
||||||
|
setBranches([]);
|
||||||
|
setIsNewBranch(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void loadBranches();
|
||||||
|
}, [selectedProject, selectedRepo]);
|
||||||
|
|
||||||
|
const handleBranchChange = (value: string) => {
|
||||||
|
if (value === "__new__") {
|
||||||
|
setIsNewBranch(true);
|
||||||
|
setSelectedBranch("__new__");
|
||||||
|
setNewBranchName("");
|
||||||
|
} else {
|
||||||
|
setIsNewBranch(false);
|
||||||
|
setSelectedBranch(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!selectedRepo) {
|
if (!selectedRepo) {
|
||||||
@@ -208,17 +250,24 @@ function WorkspaceCreateInline({
|
|||||||
setError("Workspace name is required");
|
setError("Workspace name is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const branchName = isNewBranch ? newBranchName.trim() : selectedBranch;
|
||||||
|
if (!branchName) {
|
||||||
|
setError("Please select or enter a branch");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await createWorkspaceTopLevel({
|
await createWorkspaceTopLevel({
|
||||||
repo_id: selectedRepo,
|
repo_id: selectedRepo,
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
branch: branch.trim() || "main",
|
branch: branchName,
|
||||||
});
|
});
|
||||||
onCreated();
|
onCreated();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to create workspace");
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to create workspace",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -292,12 +341,47 @@ function WorkspaceCreateInline({
|
|||||||
<label>
|
<label>
|
||||||
<Icon name="branch" size="sm" /> Branch
|
<Icon name="branch" size="sm" /> Branch
|
||||||
</label>
|
</label>
|
||||||
<input
|
{branches.length > 0 ? (
|
||||||
type="text"
|
<>
|
||||||
value={branch}
|
<select
|
||||||
onChange={(e) => setBranch(e.target.value)}
|
value={selectedBranch}
|
||||||
placeholder="main"
|
onChange={(e) => handleBranchChange(e.target.value)}
|
||||||
/>
|
required
|
||||||
|
disabled={!selectedRepo}
|
||||||
|
>
|
||||||
|
<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" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={isNewBranch ? newBranchName : selectedBranch}
|
||||||
|
onChange={(e) => {
|
||||||
|
setIsNewBranch(true);
|
||||||
|
setNewBranchName(e.target.value);
|
||||||
|
setSelectedBranch("__new__");
|
||||||
|
}}
|
||||||
|
placeholder="main"
|
||||||
|
required
|
||||||
|
disabled={!selectedRepo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
Reference in New Issue
Block a user