Pipeline/frontend/src/components/git/CreateForgejoIssueDialog.tsx

174 lines
5.3 KiB
TypeScript

"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import type { ForgejoRepository } from "@/lib/api-forgejo";
import {
createForgejoIssue,
ISSUE_TEMPLATE,
type CreateIssueResponse,
} from "@/lib/api-forgejo";
type CreateForgejoIssueDialogProps = {
repositories: ForgejoRepository[];
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: (result: CreateIssueResponse) => void;
defaultRepositoryId?: string;
};
export function CreateForgejoIssueDialog({
repositories,
open,
onOpenChange,
onSuccess,
defaultRepositoryId,
}: CreateForgejoIssueDialogProps) {
const [repositoryId, setRepositoryId] = useState(
defaultRepositoryId ?? repositories[0]?.id ?? "",
);
const [title, setTitle] = useState("");
const [body, setBody] = useState(ISSUE_TEMPLATE);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const reset = () => {
setTitle("");
setBody(ISSUE_TEMPLATE);
setRepositoryId(defaultRepositoryId ?? repositories[0]?.id ?? "");
setError(null);
};
const handleOpenChange = (next: boolean) => {
if (!isSubmitting) {
if (!next) reset();
onOpenChange(next);
}
};
const handleSubmit = async () => {
if (!title.trim() || !repositoryId) return;
setIsSubmitting(true);
setError(null);
try {
const result = await createForgejoIssue({
repository_id: repositoryId,
title: title.trim(),
body,
});
reset();
onSuccess(result);
onOpenChange(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create issue");
} finally {
setIsSubmitting(false);
}
};
const selectedRepo = repositories.find((r) => r.id === repositoryId);
const repoLabel = selectedRepo
? selectedRepo.display_name || `${selectedRepo.owner}/${selectedRepo.repo}`
: "—";
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Create issue</DialogTitle>
<DialogDescription>
Open a new issue on the connected Git repository. The body is
pre-filled with the standard issue template.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{repositories.length > 1 ? (
<div className="space-y-1.5">
<label className="text-sm font-medium text-strong">
Repository
</label>
<select
value={repositoryId}
onChange={(e) => setRepositoryId(e.target.value)}
disabled={isSubmitting}
className="w-full rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)]"
>
{repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.display_name || `${repo.owner}/${repo.repo}`}
</option>
))}
</select>
</div>
) : (
<div className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2 text-sm text-muted">
Repository:{" "}
<span className="font-medium text-strong">{repoLabel}</span>
</div>
)}
<div className="space-y-1.5">
<label className="text-sm font-medium text-strong">
Title <span className="text-[color:var(--danger)]">*</span>
</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
disabled={isSubmitting}
placeholder="Short, descriptive issue title"
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-strong">Body</label>
<p className="text-xs text-muted">
Fill in the sections below. Remove any that don&apos;t apply.
</p>
<Textarea
value={body}
onChange={(e) => setBody(e.target.value)}
rows={22}
disabled={isSubmitting}
className="resize-y font-mono text-xs"
/>
</div>
</div>
{error ? (
<div className="rounded-lg border border-[color:rgba(248,113,113,0.35)] bg-[color:rgba(248,113,113,0.08)] p-3 text-xs text-[color:var(--danger)]">
{error}
</div>
) : null}
<DialogFooter>
<Button
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || !title.trim() || !repositoryId}
>
{isSubmitting ? "Creating…" : "Create Issue"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}