fix(web): support cloning existing repositories
- Make the add repository dialog explicitly support cloning from a remote git server - Keep blank repo creation as a fallback option - Add validation and tests for the clone-first flow Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
This commit is contained in:
@@ -32,7 +32,7 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("RepositoriesSettingsTab", () => {
|
describe("RepositoriesSettingsTab", () => {
|
||||||
it("opens create dialog and submits a repository", async () => {
|
it("opens create dialog and clones an existing repository", async () => {
|
||||||
const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories);
|
const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories);
|
||||||
const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]);
|
const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]);
|
||||||
|
|
||||||
@@ -46,14 +46,34 @@ 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.getByRole("button", { name: /create/i }));
|
fireEvent.change(screen.getByPlaceholderText(/github.com\/user\/repo.git/i), {
|
||||||
|
target: { value: "https://github.com/user/repo.git" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /clone repository/i }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(createMock).toHaveBeenCalledWith("proj-1", {
|
expect(createMock).toHaveBeenCalledWith("proj-1", {
|
||||||
name: "New Repo",
|
name: "New Repo",
|
||||||
remote_url: undefined,
|
remote_url: "https://github.com/user/repo.git",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(listMock).toHaveBeenCalledTimes(2);
|
expect(listMock).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows validation when cloning without a remote url", async () => {
|
||||||
|
vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories);
|
||||||
|
render(<RepositoriesSettingsTab />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Main Repo")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /add repository/i }));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/repository-name/i), {
|
||||||
|
target: { value: "New Repo" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /clone repository/i }));
|
||||||
|
|
||||||
|
expect(screen.getByText(/remote url is required/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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 [formName, setFormName] = useState("");
|
||||||
const [formRemoteUrl, setFormRemoteUrl] = useState("");
|
const [formRemoteUrl, setFormRemoteUrl] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -49,6 +50,10 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
setError("Repository name is required");
|
setError("Repository name is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (createMode === "clone" && !formRemoteUrl.trim()) {
|
||||||
|
setError("Remote URL is required to clone an existing repository");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const input: GitRepositoryCreate = {
|
const input: GitRepositoryCreate = {
|
||||||
@@ -57,6 +62,7 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
};
|
};
|
||||||
await createRepository(projectId, input);
|
await createRepository(projectId, input);
|
||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
|
setCreateMode("clone");
|
||||||
setFormName("");
|
setFormName("");
|
||||||
setFormRemoteUrl("");
|
setFormRemoteUrl("");
|
||||||
await loadRepositories();
|
await loadRepositories();
|
||||||
@@ -82,7 +88,11 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
<div className="repositories-settings-tab">
|
<div className="repositories-settings-tab">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h2>Repositories</h2>
|
<h2>Repositories</h2>
|
||||||
<button className="primary-button" onClick={() => setShowCreate(true)} type="button">
|
<button
|
||||||
|
className="primary-button"
|
||||||
|
onClick={() => setShowCreate(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<Icon name="add" size="sm" />
|
<Icon name="add" size="sm" />
|
||||||
Add Repository
|
Add Repository
|
||||||
</button>
|
</button>
|
||||||
@@ -117,9 +127,32 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||||
<div className="dialog">
|
<div className="dialog">
|
||||||
<h3>Add Repository</h3>
|
<h3>Add Repository</h3>
|
||||||
|
<p className="muted">
|
||||||
|
Clone an existing repository from a git server, or create a blank bare repo here.
|
||||||
|
</p>
|
||||||
<form onSubmit={handleCreate} className="stack">
|
<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">
|
<label className="form-field">
|
||||||
Name
|
Repository name
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formName}
|
value={formName}
|
||||||
@@ -128,7 +161,7 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-field">
|
<label className="form-field">
|
||||||
Remote URL (optional)
|
Remote URL {createMode === "clone" ? "(required)" : "(optional)"}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formRemoteUrl}
|
value={formRemoteUrl}
|
||||||
@@ -141,7 +174,7 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button className="primary-button" type="submit">
|
<button className="primary-button" type="submit">
|
||||||
Create
|
{createMode === "clone" ? "Clone Repository" : "Create Blank Repository"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user