feat: unify workspace creation component with branch dropdown
- Rewrite WorkspaceCreateForm as unified component used in both pages - Standalone mode (WorkspacesPage): shows project/repo/branch selectors - Contextual mode (ProjectsPage): accepts defaultProjectId/defaultRepoId, skips project/repo selectors, shows only name + branch dropdown - Branch dropdown fetched from repo via listRepositoryBranches API - Auto-selects first/only option for project, repo, and branch - '+ Create new branch...' option reveals text input for custom branch - Falls back to free-text branch input if branch API fails - Removes duplicated inline creation logic from WorkspacesPage - TypeScript + eslint clean
This commit is contained in:
@@ -1,37 +1,152 @@
|
|||||||
/** Form for creating a new workspace. */
|
/** Unified workspace creation form with project/repo/branch selectors. */
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import type { CreateWorkspaceRequest } from "../types/workspace";
|
import { listProjects } from "../api/projects";
|
||||||
|
import { listRepositories, listRepositoryBranches } from "../api/git_repositories";
|
||||||
|
import { createWorkspaceTopLevel } from "../api/workspaces";
|
||||||
|
import type { ProjectWithRepos } from "../types";
|
||||||
|
import type { GitRepository } from "../api/git_repositories";
|
||||||
|
|
||||||
export interface WorkspaceCreateFormProps {
|
export interface WorkspaceCreateFormProps {
|
||||||
projectId: string;
|
/** Called after successful creation. */
|
||||||
repoId: string;
|
onSubmit: () => void | Promise<void>;
|
||||||
defaultBranch?: string;
|
/** Cancel callback. */
|
||||||
onSubmit: (data: CreateWorkspaceRequest) => Promise<void>;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
/** Optional: pre-selected project ID (hides project selector). */
|
||||||
|
defaultProjectId?: string;
|
||||||
|
/** Optional: pre-selected repo ID (hides repo selector). */
|
||||||
|
defaultRepoId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceCreateForm({
|
export function WorkspaceCreateForm({
|
||||||
defaultBranch = "main",
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
defaultProjectId,
|
||||||
|
defaultRepoId,
|
||||||
}: WorkspaceCreateFormProps) {
|
}: WorkspaceCreateFormProps) {
|
||||||
|
const isContextual = Boolean(defaultProjectId && defaultRepoId);
|
||||||
|
|
||||||
|
const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
|
||||||
|
const [repos, setRepos] = useState<GitRepository[]>([]);
|
||||||
|
const [branches, setBranches] = useState<string[]>([]);
|
||||||
|
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 [name, setName] = useState("");
|
||||||
const [branch, setBranch] = useState(defaultBranch);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [fetching, setFetching] = useState(!isContextual);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
/* ── Load projects (standalone mode only) ── */
|
||||||
|
const loadProjects = useCallback(async () => {
|
||||||
|
if (isContextual) return;
|
||||||
|
try {
|
||||||
|
const data = await listProjects();
|
||||||
|
setProjects(data);
|
||||||
|
if (data.length === 1 && !defaultProjectId) {
|
||||||
|
setSelectedProject(data[0].id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError("Failed to load projects");
|
||||||
|
} finally {
|
||||||
|
setFetching(false);
|
||||||
|
}
|
||||||
|
}, [isContextual, defaultProjectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadProjects();
|
||||||
|
}, [loadProjects]);
|
||||||
|
|
||||||
|
/* ── Load repos when project changes ── */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedProject) {
|
||||||
|
setRepos([]);
|
||||||
|
if (!defaultRepoId) setSelectedRepo("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loadRepos = async () => {
|
||||||
|
try {
|
||||||
|
const data = await listRepositories(selectedProject);
|
||||||
|
setRepos(data);
|
||||||
|
if (data.length === 1 && !defaultRepoId) {
|
||||||
|
setSelectedRepo(data[0].id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError("Failed to load repositories");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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 {
|
||||||
|
setIsNewBranch(false);
|
||||||
|
setSelectedBranch(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!selectedRepo) {
|
||||||
|
setError("Please select a repository");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
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;
|
||||||
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await onSubmit({ name: name.trim(), branch: branch.trim() });
|
await createWorkspaceTopLevel({
|
||||||
|
repo_id: selectedRepo,
|
||||||
|
name: name.trim(),
|
||||||
|
branch: branchName,
|
||||||
|
});
|
||||||
|
await onSubmit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
err instanceof Error ? err.message : "Failed to create workspace",
|
err instanceof Error ? err.message : "Failed to create workspace",
|
||||||
@@ -41,49 +156,149 @@ export function WorkspaceCreateForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (fetching) {
|
||||||
|
return (
|
||||||
|
<div className="card workspace-create-inline">
|
||||||
|
<p className="muted">Loading projects...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="workspace-create-form card" onSubmit={handleSubmit}>
|
<div className="card workspace-create-inline">
|
||||||
<h3>
|
<h3>
|
||||||
<Icon name="add" size="sm" /> Create Workspace
|
<Icon name="add" size="sm" /> Create Workspace
|
||||||
</h3>
|
</h3>
|
||||||
<div className="form-group">
|
<form onSubmit={handleSubmit} className="workspace-create-form-grid">
|
||||||
<label htmlFor="ws-name">Name</label>
|
{/* Project selector (standalone only) */}
|
||||||
<input
|
{!isContextual && (
|
||||||
id="ws-name"
|
<div className="form-group">
|
||||||
type="text"
|
<label>Project</label>
|
||||||
value={name}
|
<select
|
||||||
onChange={(e) => setName(e.target.value)}
|
value={selectedProject}
|
||||||
placeholder="e.g., feature-branch"
|
onChange={(e) => setSelectedProject(e.target.value)}
|
||||||
disabled={submitting}
|
required
|
||||||
/>
|
>
|
||||||
</div>
|
<option value="">Select project...</option>
|
||||||
<div className="form-group">
|
{projects.map((p) => (
|
||||||
<label htmlFor="ws-branch">
|
<option key={p.id} value={p.id}>
|
||||||
<Icon name="branch" size="sm" /> Branch
|
{p.name}
|
||||||
</label>
|
</option>
|
||||||
<input
|
))}
|
||||||
id="ws-branch"
|
</select>
|
||||||
type="text"
|
</div>
|
||||||
value={branch}
|
)}
|
||||||
onChange={(e) => setBranch(e.target.value)}
|
|
||||||
placeholder="main"
|
{/* Repo selector (standalone only) */}
|
||||||
disabled={submitting}
|
{!isContextual && (
|
||||||
/>
|
<div className="form-group">
|
||||||
</div>
|
<label>Repository</label>
|
||||||
{error && <p className="form-error">{error}</p>}
|
<select
|
||||||
<div className="form-actions">
|
value={selectedRepo}
|
||||||
<button
|
onChange={(e) => setSelectedRepo(e.target.value)}
|
||||||
type="button"
|
required
|
||||||
className="btn btn-secondary"
|
disabled={!selectedProject || repos.length === 0}
|
||||||
onClick={onCancel}
|
>
|
||||||
disabled={submitting}
|
<option value="">
|
||||||
>
|
{!selectedProject
|
||||||
Cancel
|
? "Select a project first"
|
||||||
</button>
|
: repos.length === 0
|
||||||
<button type="submit" className="btn btn-primary" disabled={submitting}>
|
? "No repositories"
|
||||||
{submitting ? "Creating..." : "Create"}
|
: "Select repository..."}
|
||||||
</button>
|
</option>
|
||||||
</div>
|
{repos.map((r) => (
|
||||||
</form>
|
<option key={r.id} value={r.id}>
|
||||||
|
{r.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="e.g., feature-branch"
|
||||||
|
required
|
||||||
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={isNewBranch ? newBranchName : selectedBranch}
|
||||||
|
onChange={(e) => {
|
||||||
|
setIsNewBranch(true);
|
||||||
|
setNewBranchName(e.target.value);
|
||||||
|
setSelectedBranch("__new__");
|
||||||
|
}}
|
||||||
|
placeholder="main"
|
||||||
|
required
|
||||||
|
disabled={!selectedRepo || submitting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="form-error" style={{ gridColumn: "1 / -1" }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-actions" style={{ gridColumn: "1 / -1" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={submitting || !selectedRepo}
|
||||||
|
>
|
||||||
|
{submitting ? "Creating..." : "Create Workspace"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
type ProjectUpdateInput,
|
type ProjectUpdateInput,
|
||||||
} from "../api/projects";
|
} from "../api/projects";
|
||||||
import {
|
import {
|
||||||
createWorkspace,
|
|
||||||
deleteWorkspace,
|
deleteWorkspace,
|
||||||
syncWorkspace,
|
syncWorkspace,
|
||||||
} from "../api/workspaces";
|
} from "../api/workspaces";
|
||||||
@@ -112,22 +111,7 @@ export const ProjectsPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateWorkspace = async (
|
|
||||||
projectId: string,
|
|
||||||
repoId: string,
|
|
||||||
data: { name: string; branch: string },
|
|
||||||
) => {
|
|
||||||
setWorkspaceLoading(repoId);
|
|
||||||
try {
|
|
||||||
await createWorkspace(projectId, repoId, data);
|
|
||||||
setCreatingWorkspace(null);
|
|
||||||
reload();
|
|
||||||
} catch (err) {
|
|
||||||
alert(err instanceof Error ? err.message : "Failed to create workspace");
|
|
||||||
} finally {
|
|
||||||
setWorkspaceLoading(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSyncWorkspace = async (
|
const handleSyncWorkspace = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -218,9 +202,10 @@ export const ProjectsPage = () => {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onCancelCreate={() => setCreatingWorkspace(null)}
|
onCancelCreate={() => setCreatingWorkspace(null)}
|
||||||
onSubmitCreate={async (repoId, data) =>
|
onCreated={() => {
|
||||||
await handleCreateWorkspace(project.id, repoId, data)
|
setCreatingWorkspace(null);
|
||||||
}
|
reload();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -299,7 +284,7 @@ function ProjectCard({
|
|||||||
workspaceLoading,
|
workspaceLoading,
|
||||||
showCreateForm,
|
showCreateForm,
|
||||||
onCancelCreate,
|
onCancelCreate,
|
||||||
onSubmitCreate,
|
onCreated,
|
||||||
}: {
|
}: {
|
||||||
project: ProjectWithRepos;
|
project: ProjectWithRepos;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
@@ -318,10 +303,7 @@ function ProjectCard({
|
|||||||
workspaceLoading: string | null;
|
workspaceLoading: string | null;
|
||||||
onCancelCreate: () => void;
|
onCancelCreate: () => void;
|
||||||
showCreateForm: string | null;
|
showCreateForm: string | null;
|
||||||
onSubmitCreate: (
|
onCreated: () => void;
|
||||||
repoId: string,
|
|
||||||
data: { name: string; branch: string },
|
|
||||||
) => Promise<void>;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<article className="card project-card">
|
<article className="card project-card">
|
||||||
@@ -399,9 +381,9 @@ function ProjectCard({
|
|||||||
</div>
|
</div>
|
||||||
{showCreateForm === repo.id && (
|
{showCreateForm === repo.id && (
|
||||||
<WorkspaceCreateForm
|
<WorkspaceCreateForm
|
||||||
projectId={project.id}
|
defaultProjectId={project.id}
|
||||||
repoId={repo.id}
|
defaultRepoId={repo.id}
|
||||||
onSubmit={(data) => onSubmitCreate(repo.id, data)}
|
onSubmit={onCreated}
|
||||||
onCancel={onCancelCreate}
|
onCancel={onCancelCreate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
/** Workspaces list page with direct creation. */
|
/** Workspaces list page. */
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
import { useWorkspaces } from "../hooks/use-workspaces";
|
import { useWorkspaces } from "../hooks/use-workspaces";
|
||||||
import { useWorkspaceActions } from "../hooks/use-workspace-actions";
|
import { useWorkspaceActions } from "../hooks/use-workspace-actions";
|
||||||
import { WorkspaceCard } from "../components/workspace-card";
|
import { WorkspaceCard } from "../components/workspace-card";
|
||||||
|
import { WorkspaceCreateForm } from "../components/workspace-create-form";
|
||||||
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 { listRepositories, listRepositoryBranches } from "../api/git_repositories";
|
|
||||||
import { createWorkspaceTopLevel } from "../api/workspaces";
|
|
||||||
import type { Workspace } from "../types/workspace";
|
import type { Workspace } from "../types/workspace";
|
||||||
import type { ProjectWithRepos } from "../types";
|
|
||||||
import type { GitRepository } from "../api/git_repositories";
|
|
||||||
|
|
||||||
export function WorkspacesPage() {
|
export function WorkspacesPage() {
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
@@ -94,10 +90,10 @@ export function WorkspacesPage() {
|
|||||||
{error && <div className="alert alert-error">{error}</div>}
|
{error && <div className="alert alert-error">{error}</div>}
|
||||||
|
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<WorkspaceCreateInline
|
<WorkspaceCreateForm
|
||||||
onCreated={() => {
|
onSubmit={async () => {
|
||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
refresh();
|
await refresh();
|
||||||
}}
|
}}
|
||||||
onCancel={() => setShowCreate(false)}
|
onCancel={() => setShowCreate(false)}
|
||||||
/>
|
/>
|
||||||
@@ -140,274 +136,3 @@ export function WorkspacesPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Inline Workspace Creation Form ─── */
|
|
||||||
|
|
||||||
function WorkspaceCreateInline({
|
|
||||||
onCreated,
|
|
||||||
onCancel,
|
|
||||||
}: {
|
|
||||||
onCreated: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}) {
|
|
||||||
const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
|
|
||||||
const [repos, setRepos] = useState<GitRepository[]>([]);
|
|
||||||
const [branches, setBranches] = useState<string[]>([]);
|
|
||||||
const [selectedProject, setSelectedProject] = useState("");
|
|
||||||
const [selectedRepo, setSelectedRepo] = useState("");
|
|
||||||
const [selectedBranch, setSelectedBranch] = useState("");
|
|
||||||
const [newBranchName, setNewBranchName] = useState("");
|
|
||||||
const [isNewBranch, setIsNewBranch] = useState(false);
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [fetching, setFetching] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const loadProjects = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const data = await listProjects();
|
|
||||||
setProjects(data);
|
|
||||||
if (data.length === 1) {
|
|
||||||
setSelectedProject(data[0].id);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setError("Failed to load projects");
|
|
||||||
} finally {
|
|
||||||
setFetching(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void loadProjects();
|
|
||||||
}, [loadProjects]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedProject) {
|
|
||||||
setRepos([]);
|
|
||||||
setSelectedRepo("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loadRepos = async () => {
|
|
||||||
try {
|
|
||||||
const data = await listRepositories(selectedProject);
|
|
||||||
setRepos(data);
|
|
||||||
if (data.length === 1) {
|
|
||||||
setSelectedRepo(data[0].id);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setError("Failed to load repositories");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
void loadRepos();
|
|
||||||
}, [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) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!selectedRepo) {
|
|
||||||
setError("Please select a repository");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!name.trim()) {
|
|
||||||
setError("Workspace name is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const branchName = isNewBranch ? newBranchName.trim() : selectedBranch;
|
|
||||||
if (!branchName) {
|
|
||||||
setError("Please select or enter a branch");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await createWorkspaceTopLevel({
|
|
||||||
repo_id: selectedRepo,
|
|
||||||
name: name.trim(),
|
|
||||||
branch: branchName,
|
|
||||||
});
|
|
||||||
onCreated();
|
|
||||||
} catch (err) {
|
|
||||||
setError(
|
|
||||||
err instanceof Error ? err.message : "Failed to create workspace",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fetching) {
|
|
||||||
return (
|
|
||||||
<div className="card workspace-create-inline">
|
|
||||||
<p className="muted">Loading projects...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card workspace-create-inline">
|
|
||||||
<h3>
|
|
||||||
<Icon name="add" size="sm" /> Create Workspace
|
|
||||||
</h3>
|
|
||||||
<form onSubmit={handleSubmit} className="workspace-create-form-grid">
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Project</label>
|
|
||||||
<select
|
|
||||||
value={selectedProject}
|
|
||||||
onChange={(e) => setSelectedProject(e.target.value)}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Select project...</option>
|
|
||||||
{projects.map((p) => (
|
|
||||||
<option key={p.id} value={p.id}>
|
|
||||||
{p.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Repository</label>
|
|
||||||
<select
|
|
||||||
value={selectedRepo}
|
|
||||||
onChange={(e) => setSelectedRepo(e.target.value)}
|
|
||||||
required
|
|
||||||
disabled={!selectedProject || repos.length === 0}
|
|
||||||
>
|
|
||||||
<option value="">
|
|
||||||
{!selectedProject
|
|
||||||
? "Select a project first"
|
|
||||||
: repos.length === 0
|
|
||||||
? "No repositories"
|
|
||||||
: "Select repository..."}
|
|
||||||
</option>
|
|
||||||
{repos.map((r) => (
|
|
||||||
<option key={r.id} value={r.id}>
|
|
||||||
{r.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
placeholder="e.g., feature-branch"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label>
|
|
||||||
<Icon name="branch" size="sm" /> Branch
|
|
||||||
</label>
|
|
||||||
{branches.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<select
|
|
||||||
value={selectedBranch}
|
|
||||||
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>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="form-error" style={{ gridColumn: "1 / -1" }}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-actions" style={{ gridColumn: "1 / -1" }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-primary"
|
|
||||||
disabled={loading || !selectedRepo}
|
|
||||||
>
|
|
||||||
{loading ? "Creating..." : "Create Workspace"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user