Files
headquarter/apps/web/src/components/features/project/RepositoryCreateDialog.tsx
T
Developer e434c439c9 refactor: rename files to PascalCase components and kebab-case APIs (Task 4.4)
- Rename all component files to PascalCase matching exported names
- Move components into feature directories (git/, session/, project/, terminal/, workspace/, ui/, layout/)
- Rename all page files to PascalCase with Page suffix
- Rename all API files to kebab-case
- Update all imports across codebase with corrected relative depths
- Preserve git history via git mv

Quality gates: tsc (pass), eslint (pass), 66/74 tests pass (8 pre-existing failures)
Refs: repo-restructure Task 4.4
2026-06-02 22:58:10 +00:00

327 lines
8.4 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import type {
GitRepositoryCreate,
URLParseResult,
} from "../../../types/git-repository";
import { createRepository, parseGitUrl } from "../../../api/git-repositories";
import { Icon } from "../../ui/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 [owner, setOwner] = useState("");
const [repoName, setRepoName] = useState("");
const [advancedUrl, setAdvancedUrl] = useState("");
const [useAdvancedUrl, setUseAdvancedUrl] = useState(false);
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 (!useAdvancedUrl) {
setUrlValidation({ status: "idle", result: null });
return;
}
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
if (!advancedUrl.trim()) {
setUrlValidation({ status: "idle", result: null });
return;
}
setUrlValidation({ status: "validating", result: null });
debounceTimer.current = setTimeout(async () => {
try {
const result = await parseGitUrl(advancedUrl.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);
}
};
}, [advancedUrl, open, useAdvancedUrl]);
const resetForm = () => {
setCreateMode("clone");
setFormName("");
setOwner("");
setRepoName("");
setAdvancedUrl("");
setUseAdvancedUrl(false);
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;
}
try {
const input: GitRepositoryCreate = {
name: formName.trim(),
remote_url: undefined,
};
if (createMode === "clone") {
if (useAdvancedUrl) {
if (!advancedUrl.trim()) {
setFormError("Remote URL is required for advanced cloning");
return;
}
input.remote_url = 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`;
}
}
await createRepository(projectId, input);
handleClose();
await onCreated();
} catch (error: unknown) {
const response = error as { response?: { data?: { detail?: string } } };
const detail = response.response?.data?.detail;
setFormError(
typeof detail === "string" ? detail : "Failed to create repository",
);
}
};
const handleUseSuggestedUrl = () => {
if (urlValidation.result?.base_url) {
setAdvancedUrl(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 git.commumedia.org, 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>
{createMode === "clone" && !useAdvancedUrl && (
<>
<label className="form-field">
Owner
<input
type="text"
value={owner}
onChange={(event) => setOwner(event.target.value)}
placeholder="owner"
/>
</label>
<label className="form-field">
Repository
<input
type="text"
value={repoName}
onChange={(event) => setRepoName(event.target.value)}
placeholder="repo-name"
/>
</label>
<p className="muted">
SSH target: git@git.commumedia.org:{owner || "owner"}/
{repoName || "repo"}.git
</p>
<button
type="button"
className="secondary-button small"
onClick={() => setUseAdvancedUrl(true)}
>
Use full URL instead
</button>
</>
)}
{createMode === "clone" && useAdvancedUrl && (
<label className="form-field">
Remote URL
<input
type="text"
value={advancedUrl}
onChange={(event) => setAdvancedUrl(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>
)}
<button
type="button"
className="secondary-button small"
onClick={() => setUseAdvancedUrl(false)}
>
Use owner/repo instead
</button>
</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>
);
};