"use client"; import { useEffect, useMemo, useState } from "react"; import { Button } from "@kandev/ui/button"; import { Card, CardContent } from "@kandev/ui/card"; import { Input } from "@kandev/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import { useToast } from "@/components/toast-provider"; import { SettingsSection } from "@/components/settings/settings-section"; import { fetchGitHubWorkspaceSettings, updateGitHubWorkspaceSettings, } from "@/lib/api/domains/github-api"; import type { GitHubRepoScopeMode, RepoFilter } from "@/lib/types/github"; function splitCSV(value: string): string[] { return value .split(",") .map((part) => part.trim()) .filter(Boolean); } function parseRepoFilters(value: string): RepoFilter[] { return splitCSV(value) .map((repo) => { const [owner, name, ...rest] = repo.split("/"); if (!owner || !name || rest.length > 0) return null; return { owner, name }; }) .filter((repo): repo is RepoFilter => repo !== null); } function repoFiltersToInput(repos: RepoFilter[]): string { return repos.map((repo) => `${repo.owner}/${repo.name}`).join(", "); } type ScopeFieldsProps = { mode: GitHubRepoScopeMode; orgs: string; repos: string; loading: boolean; invalidRepos: boolean; onModeChange: (mode: GitHubRepoScopeMode) => void; onOrgsChange: (orgs: string) => void; onReposChange: (repos: string) => void; }; function RepositoryScopeFields({ mode, orgs, repos, loading, invalidRepos, onModeChange, onOrgsChange, onReposChange, }: ScopeFieldsProps) { return (
onOrgsChange(event.target.value)} disabled={loading || mode !== "orgs"} placeholder="kdlbs, example-org" data-testid="github-scope-orgs-input" />
onReposChange(event.target.value)} disabled={loading || mode !== "repos"} aria-invalid={invalidRepos} placeholder="kdlbs/kandev, example/api" data-testid="github-scope-repos-input" /> {invalidRepos && (

Use comma-separated owner/repo values.

)}
); } export function GitHubRepoScopeSection({ workspaceId }: { workspaceId: string }) { const { toast } = useToast(); const [mode, setMode] = useState("all"); const [orgs, setOrgs] = useState(""); const [repos, setRepos] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const parsedRepos = useMemo(() => parseRepoFilters(repos), [repos]); const invalidRepos = useMemo(() => { const entries = splitCSV(repos); return entries.length > 0 && parsedRepos.length !== entries.length; }, [parsedRepos.length, repos]); useEffect(() => { let cancelled = false; setLoading(true); void fetchGitHubWorkspaceSettings(workspaceId) .then((settings) => { if (cancelled) return; setMode(settings.repo_scope_mode ?? "all"); setOrgs((settings.repo_scope_orgs ?? []).join(", ")); setRepos(repoFiltersToInput(settings.repo_scope_repos ?? [])); }) .catch(() => { if (!cancelled) toast({ description: "Failed to load GitHub workspace settings", variant: "error" }); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [toast, workspaceId]); const save = async () => { if (invalidRepos) { toast({ description: "Repository filters must use owner/repo format", variant: "error" }); return; } setSaving(true); try { const updated = await updateGitHubWorkspaceSettings({ workspace_id: workspaceId, repo_scope_mode: mode, repo_scope_orgs: splitCSV(orgs), repo_scope_repos: parsedRepos, }); setMode(updated.repo_scope_mode); setOrgs((updated.repo_scope_orgs ?? []).join(", ")); setRepos(repoFiltersToInput(updated.repo_scope_repos ?? [])); toast({ description: "GitHub workspace settings saved", variant: "success" }); } catch { toast({ description: "Failed to save GitHub workspace settings", variant: "error" }); } finally { setSaving(false); } }; return ( {saving ? "Saving..." : "Save"} } > ); }