"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { IconInfoCircle } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { CardContent } from "@kandev/ui/card"; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@kandev/ui/drawer"; import { Input } from "@kandev/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { useToast } from "@/components/toast-provider"; import { SettingsSection } from "@/components/settings/settings-section"; import { SettingsCard } from "@/components/settings/settings-card"; import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider"; import { useTouchDrawer } from "@/hooks/use-compact-task-chrome"; import { fetchGitHubWorkspaceSettings, updateGitHubWorkspaceSettings, } from "@/lib/api/domains/github-api"; import type { GitHubRepoScopeMode, RepoFilter, UpdateGitHubWorkspaceSettingsRequest, } 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(", "); } const repositoryScopeHelp = "Limits the GitHub pull requests and issues Kandev discovers for this workspace, including My GitHub results and review and issue watches. It does not change GitHub permissions or repository access."; function RepositoryScopeHelp() { const usesTouchDrawer = useTouchDrawer(); const [open, setOpen] = useState(false); const button = ( ); const drawerTrigger = {button}; const trigger = usesTouchDrawer ? ( drawerTrigger ) : ( {drawerTrigger} {repositoryScopeHelp} ); return ( {trigger} Repository Scope {repositoryScopeHelp} ); } type ScopeFieldsProps = { mode: GitHubRepoScopeMode; orgs: string; repos: string; baseline: { 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, baseline, 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.

)}
); } function useGitHubRepoScopeDraft(workspaceId: string) { const { toast } = useToast(); const [mode, setMode] = useState("all"); const [orgs, setOrgs] = useState(""); const [repos, setRepos] = useState(""); const [baseline, setBaseline] = useState({ mode: "all" as GitHubRepoScopeMode, orgs: "", repos: "", }); const [loading, setLoading] = useState(true); const parsedRepos = useMemo(() => parseRepoFilters(repos), [repos]); const invalidRepos = useMemo(() => { const entries = splitCSV(repos); return mode === "repos" && entries.length > 0 && parsedRepos.length !== entries.length; }, [mode, parsedRepos.length, repos]); useEffect(() => { let cancelled = false; setLoading(true); void fetchGitHubWorkspaceSettings(workspaceId) .then((settings) => { if (cancelled) return; const next = { mode: settings.repo_scope_mode ?? "all", orgs: (settings.repo_scope_orgs ?? []).join(", "), repos: repoFiltersToInput(settings.repo_scope_repos ?? []), }; setBaseline(next); setMode(next.mode); setOrgs(next.orgs); setRepos(next.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 = useCallback(async () => { const submitted = { mode, orgs, repos }; try { const payload: UpdateGitHubWorkspaceSettingsRequest = { workspace_id: workspaceId, repo_scope_mode: mode, }; if (mode === "orgs") { payload.repo_scope_orgs = splitCSV(orgs); } if (mode === "repos") { payload.repo_scope_repos = parsedRepos; } const updated = await updateGitHubWorkspaceSettings(payload); const saved = { mode: updated.repo_scope_mode, orgs: (updated.repo_scope_orgs ?? []).join(", "), repos: repoFiltersToInput(updated.repo_scope_repos ?? []), }; setBaseline(saved); setMode((current) => (current === submitted.mode ? saved.mode : current)); setOrgs((current) => (current === submitted.orgs ? saved.orgs : current)); setRepos((current) => (current === submitted.repos ? saved.repos : current)); toast({ description: "GitHub workspace settings saved", variant: "success" }); } catch { toast({ description: "Failed to save GitHub workspace settings", variant: "error" }); throw new Error("Failed to save GitHub workspace settings"); } }, [mode, orgs, parsedRepos, repos, toast, workspaceId]); const discard = useCallback(() => { setMode(baseline.mode); setOrgs(baseline.orgs); setRepos(baseline.repos); }, [baseline]); const revision = JSON.stringify([mode, orgs, repos]); const dirty = revision !== JSON.stringify([baseline.mode, baseline.orgs, baseline.repos]); useSettingsSaveContributor({ id: `github-repo-scope:${workspaceId}`, revision, isDirty: dirty, canSave: !loading && !invalidRepos, invalidReason: invalidRepos ? "Use comma-separated owner/repo values." : undefined, save, discard, }); return { mode, orgs, repos, baseline, loading, invalidRepos, setMode, setOrgs, setRepos, }; } export function GitHubRepoScopeSection({ workspaceId }: { workspaceId: string }) { const draft = useGitHubRepoScopeDraft(workspaceId); return ( } description="Limits GitHub pull requests and issues shown or imported in this workspace." > ); }