feat: allow creating external repositories directly from git mount editor
- Add createExternalRepository API function - Update GitMountEditor with "+ Add new repository..." option - Show form to enter repo name and remote URL - Auto-create external repo and refresh list on success - Update config-profiles page to pass onCreateRepository handler
This commit is contained in:
@@ -60,6 +60,13 @@ export async function createRepository(
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createExternalRepository(
|
||||||
|
data: GitRepositoryCreate
|
||||||
|
): Promise<GitRepository> {
|
||||||
|
const response = await apiClient.post<GitRepository>("/repositories", data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteRepository(projectId: string, repoId: string): Promise<void> {
|
export async function deleteRepository(projectId: string, repoId: string): Promise<void> {
|
||||||
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
|
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ interface GitMountEditorProps {
|
|||||||
mounts: GitMount[];
|
mounts: GitMount[];
|
||||||
repositories: GitRepository[];
|
repositories: GitRepository[];
|
||||||
onChange: (mounts: GitMount[]) => void;
|
onChange: (mounts: GitMount[]) => void;
|
||||||
|
onCreateRepository?: (name: string, remoteUrl: string) => Promise<GitRepository>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GitMountEditor = ({ mounts, repositories, onChange }: GitMountEditorProps) => {
|
export const GitMountEditor = ({ mounts, repositories, onChange, onCreateRepository }: GitMountEditorProps) => {
|
||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
const [newMount, setNewMount] = useState<GitMount>({
|
const [newMount, setNewMount] = useState<GitMount>({
|
||||||
repo_id: "",
|
repo_id: "",
|
||||||
@@ -58,6 +59,7 @@ export const GitMountEditor = ({ mounts, repositories, onChange }: GitMountEdito
|
|||||||
onSave={(updated) => handleUpdate(index, updated)}
|
onSave={(updated) => handleUpdate(index, updated)}
|
||||||
onCancel={() => setEditingIndex(null)}
|
onCancel={() => setEditingIndex(null)}
|
||||||
validatePath={validatePath}
|
validatePath={validatePath}
|
||||||
|
onCreateRepository={onCreateRepository}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="git-mount-display">
|
<div className="git-mount-display">
|
||||||
@@ -105,6 +107,7 @@ export const GitMountEditor = ({ mounts, repositories, onChange }: GitMountEdito
|
|||||||
onSave={handleAdd}
|
onSave={handleAdd}
|
||||||
onCancel={() => setNewMount({ repo_id: "", source_path: ".", target_path: "", branch: "" })}
|
onCancel={() => setNewMount({ repo_id: "", source_path: ".", target_path: "", branch: "" })}
|
||||||
validatePath={validatePath}
|
validatePath={validatePath}
|
||||||
|
onCreateRepository={onCreateRepository}
|
||||||
isNew
|
isNew
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,12 +121,17 @@ interface GitMountFormProps {
|
|||||||
onSave: (mount: GitMount) => void;
|
onSave: (mount: GitMount) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
validatePath: (path: string, isTarget: boolean) => string | null;
|
validatePath: (path: string, isTarget: boolean) => string | null;
|
||||||
|
onCreateRepository?: (name: string, remoteUrl: string) => Promise<GitRepository>;
|
||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, isNew }: GitMountFormProps) => {
|
const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, onCreateRepository, isNew }: GitMountFormProps) => {
|
||||||
const [form, setForm] = useState<GitMount>({ ...mount });
|
const [form, setForm] = useState<GitMount>({ ...mount });
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
const [isCreatingRepo, setIsCreatingRepo] = useState(false);
|
||||||
|
const [newRepoName, setNewRepoName] = useState("");
|
||||||
|
const [newRepoUrl, setNewRepoUrl] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleChange = (field: keyof GitMount, value: string) => {
|
const handleChange = (field: keyof GitMount, value: string) => {
|
||||||
setForm((prev) => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
@@ -136,6 +144,26 @@ const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, isN
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateRepo = async () => {
|
||||||
|
if (!onCreateRepository || !newRepoName.trim() || !newRepoUrl.trim()) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const repo = await onCreateRepository(newRepoName.trim(), newRepoUrl.trim());
|
||||||
|
handleChange("repo_id", repo.id);
|
||||||
|
setIsCreatingRepo(false);
|
||||||
|
setNewRepoName("");
|
||||||
|
setNewRepoUrl("");
|
||||||
|
} catch (err) {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
repo_id: err instanceof Error ? err.message : "Failed to create repository",
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -164,19 +192,71 @@ const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, isN
|
|||||||
<div className="git-mount-form">
|
<div className="git-mount-form">
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label>Repository</label>
|
<label>Repository</label>
|
||||||
<select
|
{!isCreatingRepo ? (
|
||||||
value={form.repo_id}
|
<>
|
||||||
onChange={(e) => handleChange("repo_id", e.target.value)}
|
<select
|
||||||
className={errors.repo_id ? "error" : ""}
|
value={form.repo_id}
|
||||||
>
|
onChange={(e) => {
|
||||||
<option value="">Select a repository...</option>
|
if (e.target.value === "__new__") {
|
||||||
{repositories.map((repo) => (
|
setIsCreatingRepo(true);
|
||||||
<option key={repo.id} value={repo.id}>
|
} else {
|
||||||
{repo.name}
|
handleChange("repo_id", e.target.value);
|
||||||
</option>
|
}
|
||||||
))}
|
}}
|
||||||
</select>
|
className={errors.repo_id ? "error" : ""}
|
||||||
{errors.repo_id && <span className="error-text">{errors.repo_id}</span>}
|
>
|
||||||
|
<option value="">Select a repository...</option>
|
||||||
|
{repositories.map((repo) => (
|
||||||
|
<option key={repo.id} value={repo.id}>
|
||||||
|
{repo.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
{onCreateRepository && (
|
||||||
|
<option value="__new__">+ Add new repository...</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{errors.repo_id && <span className="error-text">{errors.repo_id}</span>}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="new-repo-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newRepoName}
|
||||||
|
onChange={(e) => setNewRepoName(e.target.value)}
|
||||||
|
placeholder="Repository name"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newRepoUrl}
|
||||||
|
onChange={(e) => setNewRepoUrl(e.target.value)}
|
||||||
|
placeholder="https://github.com/user/repo.git"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<div className="new-repo-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="primary-button small"
|
||||||
|
onClick={handleCreateRepo}
|
||||||
|
disabled={isSubmitting || !newRepoName.trim() || !newRepoUrl.trim()}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Creating..." : "Create Repository"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary-button small"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCreatingRepo(false);
|
||||||
|
setNewRepoName("");
|
||||||
|
setNewRepoUrl("");
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
type ResolvedProfile,
|
type ResolvedProfile,
|
||||||
} from "../api/config_profiles";
|
} from "../api/config_profiles";
|
||||||
import { listProjects } from "../api/projects";
|
import { listProjects } from "../api/projects";
|
||||||
import { listAllUserRepositories, type GitRepository } from "../api/git_repositories";
|
import { listAllUserRepositories, createExternalRepository, type GitRepository } from "../api/git_repositories";
|
||||||
import type { Project } from "../types";
|
import type { Project } from "../types";
|
||||||
import { listToolTypes, type ToolType } from "../api/tool_types";
|
import { listToolTypes, type ToolType } from "../api/tool_types";
|
||||||
import { GitMountEditor } from "../components/git-mount-editor";
|
import { GitMountEditor } from "../components/git-mount-editor";
|
||||||
@@ -1259,6 +1259,14 @@ export const ConfigProfilesPage = () => {
|
|||||||
mounts={formData.git_mounts || []}
|
mounts={formData.git_mounts || []}
|
||||||
repositories={repositories}
|
repositories={repositories}
|
||||||
onChange={(git_mounts) => updateFormField("git_mounts", git_mounts)}
|
onChange={(git_mounts) => updateFormField("git_mounts", git_mounts)}
|
||||||
|
onCreateRepository={async (name, remoteUrl) => {
|
||||||
|
const repo = await createExternalRepository({
|
||||||
|
name,
|
||||||
|
remote_url: remoteUrl,
|
||||||
|
});
|
||||||
|
setRepositories((prev) => [...prev, repo]);
|
||||||
|
return repo;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4131,3 +4131,28 @@ a.nav-item,
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-repo-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo-form input {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user