refactor(web): reuse repository create dialog
- Extract the repository create dialog into a shared component - Reuse the same clone/validation flow in project settings and repository management pages - Keep the shared UI covered with focused tests Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
This commit is contained in:
@@ -46,6 +46,7 @@ describe("RepositoriesSettingsTab", () => {
|
|||||||
fireEvent.change(screen.getByPlaceholderText(/repository-name/i), {
|
fireEvent.change(screen.getByPlaceholderText(/repository-name/i), {
|
||||||
target: { value: "New Repo" },
|
target: { value: "New Repo" },
|
||||||
});
|
});
|
||||||
|
fireEvent.click(screen.getByLabelText(/clone existing repository/i));
|
||||||
fireEvent.change(screen.getByPlaceholderText(/github.com\/user\/repo.git/i), {
|
fireEvent.change(screen.getByPlaceholderText(/github.com\/user\/repo.git/i), {
|
||||||
target: { value: "https://github.com/user/repo.git" },
|
target: { value: "https://github.com/user/repo.git" },
|
||||||
});
|
});
|
||||||
@@ -72,6 +73,7 @@ describe("RepositoriesSettingsTab", () => {
|
|||||||
fireEvent.change(screen.getByPlaceholderText(/repository-name/i), {
|
fireEvent.change(screen.getByPlaceholderText(/repository-name/i), {
|
||||||
target: { value: "New Repo" },
|
target: { value: "New Repo" },
|
||||||
});
|
});
|
||||||
|
fireEvent.click(screen.getByLabelText(/clone existing repository/i));
|
||||||
fireEvent.click(screen.getByRole("button", { name: /clone repository/i }));
|
fireEvent.click(screen.getByRole("button", { name: /clone repository/i }));
|
||||||
|
|
||||||
expect(screen.getByText(/remote url is required/i)).toBeInTheDocument();
|
expect(screen.getByText(/remote url is required/i)).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import { deleteRepository, listRepositories, type GitRepository } from "../api/git_repositories";
|
||||||
createRepository,
|
import { RepositoryCreateDialog } from "./repository-create-dialog";
|
||||||
deleteRepository,
|
|
||||||
listRepositories,
|
|
||||||
type GitRepository,
|
|
||||||
type GitRepositoryCreate,
|
|
||||||
} from "../api/git_repositories";
|
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
export const RepositoriesSettingsTab: React.FC = () => {
|
export const RepositoriesSettingsTab: React.FC = () => {
|
||||||
@@ -15,9 +10,6 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
const [createMode, setCreateMode] = useState<"clone" | "blank">("clone");
|
|
||||||
const [formName, setFormName] = useState("");
|
|
||||||
const [formRemoteUrl, setFormRemoteUrl] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const loadRepositories = useCallback(async () => {
|
const loadRepositories = useCallback(async () => {
|
||||||
@@ -41,36 +33,6 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
void loadRepositories();
|
void loadRepositories();
|
||||||
}, [loadRepositories]);
|
}, [loadRepositories]);
|
||||||
|
|
||||||
const handleCreate = async (event: React.FormEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
if (!projectId) return;
|
|
||||||
if (!formName.trim()) {
|
|
||||||
setError("Repository name is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (createMode === "clone" && !formRemoteUrl.trim()) {
|
|
||||||
setError("Remote URL is required to clone an existing repository");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const input: GitRepositoryCreate = {
|
|
||||||
name: formName.trim(),
|
|
||||||
remote_url: formRemoteUrl.trim() || undefined,
|
|
||||||
};
|
|
||||||
await createRepository(projectId, input);
|
|
||||||
setShowCreate(false);
|
|
||||||
setCreateMode("clone");
|
|
||||||
setFormName("");
|
|
||||||
setFormRemoteUrl("");
|
|
||||||
await loadRepositories();
|
|
||||||
} catch {
|
|
||||||
setError("Failed to create repository");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (repoId: string) => {
|
const handleDelete = async (repoId: string) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
if (!window.confirm("Are you sure you want to delete this repository?")) return;
|
if (!window.confirm("Are you sure you want to delete this repository?")) return;
|
||||||
@@ -124,62 +86,13 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
<RepositoryCreateDialog
|
||||||
<div className="dialog">
|
projectId={projectId!}
|
||||||
<h3>Add Repository</h3>
|
open={showCreate}
|
||||||
<p className="muted">
|
title="Add Repository"
|
||||||
Clone an existing repository from a git server, or create a blank bare repo here.
|
onClose={() => setShowCreate(false)}
|
||||||
</p>
|
onCreated={loadRepositories}
|
||||||
<form onSubmit={handleCreate} className="stack">
|
/>
|
||||||
<div className="form-field">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="repository-mode"
|
|
||||||
checked={createMode === "clone"}
|
|
||||||
onChange={() => setCreateMode("clone")}
|
|
||||||
/>
|
|
||||||
Clone existing repository
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="repository-mode"
|
|
||||||
checked={createMode === "blank"}
|
|
||||||
onChange={() => setCreateMode("blank")}
|
|
||||||
/>
|
|
||||||
Create blank repository
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label className="form-field">
|
|
||||||
Repository name
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formName}
|
|
||||||
onChange={(event) => setFormName(event.target.value)}
|
|
||||||
placeholder="repository-name"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="form-field">
|
|
||||||
Remote URL {createMode === "clone" ? "(required)" : "(optional)"}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formRemoteUrl}
|
|
||||||
onChange={(event) => setFormRemoteUrl(event.target.value)}
|
|
||||||
placeholder="https://github.com/user/repo.git"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="dialog-actions">
|
|
||||||
<button className="secondary-button" onClick={() => setShowCreate(false)} type="button">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button className="primary-button" type="submit">
|
|
||||||
{createMode === "clone" ? "Clone Repository" : "Create Blank Repository"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { createRepository, parseGitUrl, type GitRepositoryCreate, type URLParseResult } from "../api/git_repositories";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
|
type CreateMode = "clone" | "blank";
|
||||||
|
type UrlValidationStatus = "idle" | "validating" | "valid" | "needs-parsing" | "invalid";
|
||||||
|
|
||||||
|
interface RepositoryCreateDialogProps {
|
||||||
|
projectId: string;
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreated: () => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCreated }: RepositoryCreateDialogProps) => {
|
||||||
|
const [createMode, setCreateMode] = useState<CreateMode>("clone");
|
||||||
|
const [formName, setFormName] = useState("");
|
||||||
|
const [formRemoteUrl, setFormRemoteUrl] = useState("");
|
||||||
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
const [urlValidation, setUrlValidation] = useState<{
|
||||||
|
status: UrlValidationStatus;
|
||||||
|
result: URLParseResult | null;
|
||||||
|
}>({ status: "idle", result: null });
|
||||||
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open && debounceTimer.current) {
|
||||||
|
clearTimeout(debounceTimer.current);
|
||||||
|
debounceTimer.current = null;
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
if (debounceTimer.current) {
|
||||||
|
clearTimeout(debounceTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formRemoteUrl.trim()) {
|
||||||
|
setUrlValidation({ status: "idle", result: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUrlValidation({ status: "validating", result: null });
|
||||||
|
|
||||||
|
debounceTimer.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await parseGitUrl(formRemoteUrl.trim());
|
||||||
|
if (result.is_valid_clone_url) {
|
||||||
|
setUrlValidation({ status: "valid", result });
|
||||||
|
} else if (result.needs_parsing) {
|
||||||
|
setUrlValidation({ status: "needs-parsing", result });
|
||||||
|
} else {
|
||||||
|
setUrlValidation({ status: "invalid", result });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setUrlValidation({ status: "invalid", result: null });
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (debounceTimer.current) {
|
||||||
|
clearTimeout(debounceTimer.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [formRemoteUrl, open]);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setCreateMode("clone");
|
||||||
|
setFormName("");
|
||||||
|
setFormRemoteUrl("");
|
||||||
|
setFormError(null);
|
||||||
|
setUrlValidation({ status: "idle", result: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
resetForm();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setFormError(null);
|
||||||
|
|
||||||
|
if (!formName.trim()) {
|
||||||
|
setFormError("Repository name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createMode === "clone" && !formRemoteUrl.trim()) {
|
||||||
|
setFormError("Remote URL is required to clone an existing repository");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const input: GitRepositoryCreate = {
|
||||||
|
name: formName.trim(),
|
||||||
|
remote_url: formRemoteUrl.trim() || undefined,
|
||||||
|
};
|
||||||
|
await createRepository(projectId, input);
|
||||||
|
handleClose();
|
||||||
|
await onCreated();
|
||||||
|
} catch {
|
||||||
|
setFormError("Failed to create repository");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUseSuggestedUrl = () => {
|
||||||
|
if (urlValidation.result?.base_url) {
|
||||||
|
setFormRemoteUrl(urlValidation.result.base_url);
|
||||||
|
setUrlValidation({ status: "idle", result: null });
|
||||||
|
setFormError(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUrlInputClass = () => {
|
||||||
|
switch (urlValidation.status) {
|
||||||
|
case "valid":
|
||||||
|
return "valid-url";
|
||||||
|
case "needs-parsing":
|
||||||
|
return "needs-parsing-url";
|
||||||
|
case "invalid":
|
||||||
|
return "invalid-url";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||||
|
<div className="dialog">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<p className="muted">
|
||||||
|
Clone an existing repository from a git server, or create a blank bare repo here.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubmit} className="stack">
|
||||||
|
<div className="form-field">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="repository-mode"
|
||||||
|
checked={createMode === "clone"}
|
||||||
|
onChange={() => setCreateMode("clone")}
|
||||||
|
/>
|
||||||
|
Clone existing repository
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="repository-mode"
|
||||||
|
checked={createMode === "blank"}
|
||||||
|
onChange={() => setCreateMode("blank")}
|
||||||
|
/>
|
||||||
|
Create blank repository
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className="form-field">
|
||||||
|
Repository name
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formName}
|
||||||
|
onChange={(event) => setFormName(event.target.value)}
|
||||||
|
placeholder="repository-name"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="form-field">
|
||||||
|
Remote URL {createMode === "clone" ? "(required)" : "(optional)"}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formRemoteUrl}
|
||||||
|
onChange={(event) => setFormRemoteUrl(event.target.value)}
|
||||||
|
placeholder="https://github.com/user/repo.git"
|
||||||
|
className={getUrlInputClass()}
|
||||||
|
/>
|
||||||
|
{urlValidation.status === "validating" && (
|
||||||
|
<span className="validation-status validating">Validating...</span>
|
||||||
|
)}
|
||||||
|
{urlValidation.status === "valid" && (
|
||||||
|
<span className="validation-status valid">
|
||||||
|
<Icon name="success" size="sm" /> Valid git URL
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{urlValidation.status === "needs-parsing" && urlValidation.result && (
|
||||||
|
<div className="url-suggestion">
|
||||||
|
<span className="validation-status warning">
|
||||||
|
<Icon name="warning" size="sm" /> This looks like a browser URL
|
||||||
|
</span>
|
||||||
|
<div className="suggestion-actions">
|
||||||
|
<span className="suggested-url">Suggested: {urlValidation.result.base_url}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary-button small"
|
||||||
|
onClick={handleUseSuggestedUrl}
|
||||||
|
>
|
||||||
|
Use Suggested
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{urlValidation.status === "invalid" && (
|
||||||
|
<span className="validation-status invalid">
|
||||||
|
<Icon name="error" size="sm" /> Invalid URL
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
{formError && (
|
||||||
|
<div className="error-message">
|
||||||
|
<p className="error-text">{formError}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="dialog-actions">
|
||||||
|
<button className="secondary-button" onClick={handleClose} type="button">
|
||||||
|
<Icon name="cancel" size="sm" />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button className="primary-button" type="submit">
|
||||||
|
<Icon name="add" size="sm" />
|
||||||
|
{createMode === "clone" ? "Clone Repository" : "Create Blank Repository"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createRepository,
|
|
||||||
deleteRepository,
|
deleteRepository,
|
||||||
listRepositories,
|
listRepositories,
|
||||||
parseGitUrl,
|
|
||||||
type GitRepositoryCreate,
|
|
||||||
type URLParseResult,
|
|
||||||
} from "../api/git_repositories";
|
} from "../api/git_repositories";
|
||||||
import type { GitRepository } from "../api/git_repositories";
|
import type { GitRepository } from "../api/git_repositories";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
|
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
|
||||||
|
|
||||||
type RepoStatus = "loading" | "ready" | "error";
|
type RepoStatus = "loading" | "ready" | "error";
|
||||||
type UrlValidationStatus = "idle" | "validating" | "valid" | "needs-parsing" | "invalid";
|
|
||||||
|
|
||||||
export const GitRepositoriesPage = () => {
|
export const GitRepositoriesPage = () => {
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
@@ -21,19 +17,8 @@ export const GitRepositoriesPage = () => {
|
|||||||
const [status, setStatus] = useState<RepoStatus>("loading");
|
const [status, setStatus] = useState<RepoStatus>("loading");
|
||||||
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
const [formName, setFormName] = useState("");
|
|
||||||
const [formRemoteUrl, setFormRemoteUrl] = useState("");
|
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
|
||||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||||
|
|
||||||
// URL validation state
|
|
||||||
const [urlValidation, setUrlValidation] = useState<{
|
|
||||||
status: UrlValidationStatus;
|
|
||||||
result: URLParseResult | null;
|
|
||||||
}>({ status: "idle", result: null });
|
|
||||||
|
|
||||||
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
const loadRepositories = useCallback(async () => {
|
const loadRepositories = useCallback(async () => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
setStatus("loading");
|
setStatus("loading");
|
||||||
@@ -51,98 +36,6 @@ export const GitRepositoriesPage = () => {
|
|||||||
void loadRepositories();
|
void loadRepositories();
|
||||||
}, [loadRepositories]);
|
}, [loadRepositories]);
|
||||||
|
|
||||||
// Validate URL with debounce
|
|
||||||
useEffect(() => {
|
|
||||||
if (debounceTimer.current) {
|
|
||||||
clearTimeout(debounceTimer.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formRemoteUrl.trim()) {
|
|
||||||
setUrlValidation({ status: "idle", result: null });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUrlValidation({ status: "validating", result: null });
|
|
||||||
|
|
||||||
debounceTimer.current = setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
const result = await parseGitUrl(formRemoteUrl.trim());
|
|
||||||
if (result.is_valid_clone_url) {
|
|
||||||
setUrlValidation({ status: "valid", result });
|
|
||||||
} else if (result.needs_parsing) {
|
|
||||||
setUrlValidation({ status: "needs-parsing", result });
|
|
||||||
} else {
|
|
||||||
setUrlValidation({ status: "invalid", result });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setUrlValidation({ status: "invalid", result: null });
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (debounceTimer.current) {
|
|
||||||
clearTimeout(debounceTimer.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [formRemoteUrl]);
|
|
||||||
|
|
||||||
const getUrlInputClass = () => {
|
|
||||||
switch (urlValidation.status) {
|
|
||||||
case "valid":
|
|
||||||
return "valid-url";
|
|
||||||
case "needs-parsing":
|
|
||||||
return "needs-parsing-url";
|
|
||||||
case "invalid":
|
|
||||||
return "invalid-url";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setFormError(null);
|
|
||||||
|
|
||||||
if (!formName.trim()) {
|
|
||||||
setFormError("Repository name is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!projectId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const input: GitRepositoryCreate = {
|
|
||||||
name: formName.trim(),
|
|
||||||
remote_url: formRemoteUrl.trim() || undefined,
|
|
||||||
};
|
|
||||||
await createRepository(projectId, input);
|
|
||||||
setShowCreate(false);
|
|
||||||
setFormName("");
|
|
||||||
setFormRemoteUrl("");
|
|
||||||
setUrlValidation({ status: "idle", result: null });
|
|
||||||
await loadRepositories();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const axiosError = err as { response?: { status: number; data: { detail: { suggested_url: string; message: string } } } };
|
|
||||||
if (axiosError.response?.status === 422 && axiosError.response?.data?.detail?.suggested_url) {
|
|
||||||
// Show URL correction suggestion
|
|
||||||
const detail = axiosError.response.data.detail;
|
|
||||||
setFormError(
|
|
||||||
`${detail.message}\nSuggested: ${detail.suggested_url}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setFormError("Failed to create repository");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUseSuggestedUrl = () => {
|
|
||||||
if (urlValidation.result?.base_url) {
|
|
||||||
setFormRemoteUrl(urlValidation.result.base_url);
|
|
||||||
setUrlValidation({ status: "idle", result: null });
|
|
||||||
setFormError(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (repoId: string) => {
|
const handleDelete = async (repoId: string) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
try {
|
try {
|
||||||
@@ -233,81 +126,13 @@ export const GitRepositoriesPage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showCreate && (
|
{showCreate && (
|
||||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
<RepositoryCreateDialog
|
||||||
<div className="dialog">
|
projectId={projectId!}
|
||||||
<h2>Create Repository</h2>
|
open={showCreate}
|
||||||
<form onSubmit={handleSubmit} className="stack">
|
title="Create Repository"
|
||||||
<label className="form-field">
|
onClose={() => setShowCreate(false)}
|
||||||
Name
|
onCreated={loadRepositories}
|
||||||
<input
|
/>
|
||||||
type="text"
|
|
||||||
value={formName}
|
|
||||||
onChange={(e) => setFormName(e.target.value)}
|
|
||||||
placeholder="repository-name"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="form-field">
|
|
||||||
Remote URL (optional)
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formRemoteUrl}
|
|
||||||
onChange={(e) => setFormRemoteUrl(e.target.value)}
|
|
||||||
placeholder="https://github.com/user/repo.git"
|
|
||||||
className={getUrlInputClass()}
|
|
||||||
/>
|
|
||||||
{urlValidation.status === "validating" && (
|
|
||||||
<span className="validation-status validating">Validating...</span>
|
|
||||||
)}
|
|
||||||
{urlValidation.status === "valid" && (
|
|
||||||
<span className="validation-status valid">
|
|
||||||
<Icon name="success" size="sm" /> Valid git URL
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{urlValidation.status === "needs-parsing" && urlValidation.result && (
|
|
||||||
<div className="url-suggestion">
|
|
||||||
<span className="validation-status warning">
|
|
||||||
<Icon name="warning" size="sm" /> This looks like a browser URL
|
|
||||||
</span>
|
|
||||||
<div className="suggestion-actions">
|
|
||||||
<span className="suggested-url">
|
|
||||||
Suggested: {urlValidation.result.base_url}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary-button small"
|
|
||||||
onClick={handleUseSuggestedUrl}
|
|
||||||
>
|
|
||||||
Use Suggested
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{urlValidation.status === "invalid" && (
|
|
||||||
<span className="validation-status invalid">
|
|
||||||
<Icon name="error" size="sm" /> Invalid URL
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
{formError && (
|
|
||||||
<div className="error-message">
|
|
||||||
{formError.split("\n").map((line, i) => (
|
|
||||||
<p key={i} className="error-text">{line}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="dialog-actions">
|
|
||||||
<button className="secondary-button" onClick={() => setShowCreate(false)} type="button">
|
|
||||||
<Icon name="cancel" size="sm" />
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button className="primary-button" type="submit">
|
|
||||||
<Icon name="add" size="sm" />
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user