feat: add SSH URL validation, inline radio buttons in repo creation dialog
RepositoryCreateDialog (already unified, used everywhere): - Add isSshUrl() helper to detect git@ and ssh:// URLs - Require SSH key selection when URL is SSH; show error otherwise - Inline existing/new radio buttons with smaller styling (.repo-mode-radios) Tests: - Update repositories-settings-tab.test.tsx to select SSH key for owner/repo mode - Mock listSSHKeys in tests Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
This commit is contained in:
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { RepositoriesSettingsTab } from "./repositories-settings-tab";
|
||||
import * as gitRepositoriesApi from "../../../api/git-repositories";
|
||||
import * as sshKeysApi from "../../../api/ssh-keys";
|
||||
|
||||
const mockRepositories = [
|
||||
{
|
||||
@@ -35,6 +36,9 @@ describe("RepositoriesSettingsTab", () => {
|
||||
it("opens create dialog and clones an existing repository", async () => {
|
||||
const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories);
|
||||
const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]);
|
||||
vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([
|
||||
{ id: "key-1", name: "My Key", public_key: "ssh-ed25519 AAA...", created_at: "2024-01-01" },
|
||||
]);
|
||||
|
||||
render(<RepositoriesSettingsTab />);
|
||||
|
||||
@@ -52,12 +56,17 @@ describe("RepositoriesSettingsTab", () => {
|
||||
fireEvent.change(screen.getByPlaceholderText(/repo-name/i), {
|
||||
target: { value: "demo" },
|
||||
});
|
||||
const sshSelect = screen.getByRole("combobox", { name: /ssh key/i });
|
||||
await waitFor(() => expect(sshSelect.querySelector('option[value="key-1"]')).toBeTruthy());
|
||||
(sshSelect as HTMLSelectElement).value = "key-1";
|
||||
fireEvent.change(sshSelect);
|
||||
fireEvent.click(screen.getByRole("button", { name: /clone repository/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith("proj-1", {
|
||||
name: "New Repo",
|
||||
remote_url: "git@git.commumedia.org:alice/demo.git",
|
||||
ssh_key_id: "key-1",
|
||||
});
|
||||
});
|
||||
expect(listMock).toHaveBeenCalledTimes(2);
|
||||
@@ -66,6 +75,7 @@ describe("RepositoriesSettingsTab", () => {
|
||||
it("uses advanced url fallback when requested", async () => {
|
||||
const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories);
|
||||
const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]);
|
||||
vi.spyOn(sshKeysApi, "listSSHKeys").mockResolvedValue([]);
|
||||
|
||||
render(<RepositoriesSettingsTab />);
|
||||
|
||||
|
||||
@@ -124,6 +124,11 @@ export const RepositoryCreateDialog = ({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isSshUrl = (url: string): boolean => {
|
||||
const u = url.trim().toLowerCase();
|
||||
return u.startsWith("git@") || u.startsWith("ssh://");
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setFormError(null);
|
||||
@@ -140,19 +145,27 @@ export const RepositoryCreateDialog = ({
|
||||
};
|
||||
|
||||
if (createMode === "clone") {
|
||||
let remoteUrl: string;
|
||||
if (useAdvancedUrl) {
|
||||
if (!advancedUrl.trim()) {
|
||||
setFormError("Remote URL is required for advanced cloning");
|
||||
return;
|
||||
}
|
||||
input.remote_url = advancedUrl.trim();
|
||||
remoteUrl = advancedUrl.trim();
|
||||
} else {
|
||||
if (!owner.trim() || !repoName.trim()) {
|
||||
setFormError("Owner and repository name are required");
|
||||
return;
|
||||
}
|
||||
input.remote_url = `git@git.commumedia.org:${owner.trim()}/${repoName.trim()}.git`;
|
||||
remoteUrl = `git@git.commumedia.org:${owner.trim()}/${repoName.trim()}.git`;
|
||||
}
|
||||
|
||||
if (isSshUrl(remoteUrl) && !selectedSshKey) {
|
||||
setFormError("An SSH key is required for SSH URLs");
|
||||
return;
|
||||
}
|
||||
|
||||
input.remote_url = remoteUrl;
|
||||
if (selectedSshKey) {
|
||||
input.ssh_key_id = selectedSshKey;
|
||||
}
|
||||
@@ -202,24 +215,24 @@ export const RepositoryCreateDialog = ({
|
||||
blank bare repo here.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="stack">
|
||||
<div className="form-field">
|
||||
<label>
|
||||
<div className="form-field repo-mode-radios">
|
||||
<label className="repo-mode-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="repository-mode"
|
||||
checked={createMode === "clone"}
|
||||
onChange={() => setCreateMode("clone")}
|
||||
/>
|
||||
Clone existing repository
|
||||
<span>Clone existing</span>
|
||||
</label>
|
||||
<label>
|
||||
<label className="repo-mode-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="repository-mode"
|
||||
checked={createMode === "blank"}
|
||||
onChange={() => setCreateMode("blank")}
|
||||
/>
|
||||
Create blank repository
|
||||
<span>Create blank</span>
|
||||
</label>
|
||||
</div>
|
||||
<label className="form-field">
|
||||
|
||||
@@ -137,3 +137,28 @@
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Inline radio buttons for repository creation mode */
|
||||
.repo-mode-radios {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
flex-wrap: wrap;
|
||||
padding: var(--space-2) 0;
|
||||
}
|
||||
|
||||
.repo-mode-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.repo-mode-label input[type="radio"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--brand);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user