"use client"; import { useState, useCallback } from "react"; import { IconBrandGithub, IconPlus, IconTrashX } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Card, CardContent } from "@kandev/ui/card"; import { Separator } from "@kandev/ui/separator"; import { TooltipProvider } from "@kandev/ui/tooltip"; import { useToast } from "@/components/toast-provider"; import { SettingsSection } from "@/components/settings/settings-section"; import { GitHubStatusCard } from "./github-status"; import { ReviewWatchTable } from "./review-watch-table"; import { ReviewWatchDialog } from "./review-watch-dialog"; import { IssueWatchTable } from "./issue-watch-table"; import { IssueWatchDialog } from "./issue-watch-dialog"; import { ActionPresetsSection } from "./action-presets-section"; import { DefaultQueriesSection } from "./default-queries-section"; import { PRStatsPanel } from "./pr-stats"; import { useReviewWatches } from "@/hooks/domains/github/use-review-watches"; import { useIssueWatches } from "@/hooks/domains/github/use-issue-watches"; import { WorkspaceScopedSection } from "@/components/integrations/workspace-scoped-section"; import { ResetWatchDialog, useWatchResetController } from "@/components/watches/reset-watch-dialog"; import { cleanupMergedReviewTasks, cleanupClosedIssueTasks } from "@/lib/api/domains/github-api"; import type { ReviewWatch, IssueWatch } from "@/lib/types/github"; // CleanupNowButton runs a manual global sweep over the dedup tables. Useful // for users who upgraded with a pile of legacy merged-PR / closed-issue // review tasks that pre-date the cleanup_policy work — clicking once drains // them according to each watch's policy. function CleanupNowButton({ label, run, }: { label: string; run: () => Promise<{ deleted: number }>; }) { const { toast } = useToast(); const [busy, setBusy] = useState(false); return ( ); } function useWatchActions(workspaceId?: string | null) { const { items: watches, create, update, remove, trigger, previewReset, reset, } = useReviewWatches(workspaceId); const { toast } = useToast(); const handleDelete = useCallback( async (id: string) => { try { await remove(id); toast({ description: "Review watch deleted", variant: "success" }); } catch { toast({ description: "Failed to delete review watch", variant: "error" }); } }, [remove, toast], ); const handleTrigger = useCallback( async (id: string) => { try { const result = await trigger(id); const count = result?.new_prs_found ?? 0; if (count > 0) { toast({ description: `Found ${count} new PR${count > 1 ? "s" : ""}`, variant: "success", }); } else { toast({ description: "No new PRs found" }); } } catch { toast({ description: "Failed to check for PRs", variant: "error" }); } }, [trigger, toast], ); const handleToggleEnabled = useCallback( async (watch: ReviewWatch) => { try { await update(watch.id, { enabled: !watch.enabled }); toast({ description: watch.enabled ? "Watch paused" : "Watch enabled", variant: "success", }); } catch { toast({ description: "Failed to update watch", variant: "error" }); } }, [update, toast], ); const handleReset = useCallback( async (id: string, workspaceId: string) => { try { const { tasksDeleted } = await reset(id, workspaceId); toast({ description: tasksDeleted > 0 ? `Reset complete — deleted ${tasksDeleted} task(s); next poll will re-import.` : "Reset complete — next poll will re-import matches.", variant: "success", }); } catch { toast({ description: "Failed to reset review watch", variant: "error" }); throw new Error("reset failed"); } }, [reset, toast], ); return { watches, create, update, previewReset, handleDelete, handleTrigger, handleReset, handleToggleEnabled, }; } function useIssueWatchActions(workspaceId?: string | null) { const { items: watches, create, update, remove, trigger, previewReset, reset, } = useIssueWatches(workspaceId); const { toast } = useToast(); const handleDelete = useCallback( async (id: string) => { try { await remove(id); toast({ description: "Issue watch deleted", variant: "success" }); } catch { toast({ description: "Failed to delete issue watch", variant: "error" }); } }, [remove, toast], ); const handleTrigger = useCallback( async (id: string) => { try { const result = await trigger(id); const count = result?.new_issues_found ?? 0; if (count > 0) { toast({ description: `Found ${count} new issue${count > 1 ? "s" : ""}`, variant: "success", }); } else { toast({ description: "No new issues found" }); } } catch { toast({ description: "Failed to check for issues", variant: "error" }); } }, [trigger, toast], ); const handleToggleEnabled = useCallback( async (watch: IssueWatch) => { try { await update(watch.id, { enabled: !watch.enabled }); toast({ description: watch.enabled ? "Watch paused" : "Watch enabled", variant: "success", }); } catch { toast({ description: "Failed to update watch", variant: "error" }); } }, [update, toast], ); const handleReset = useCallback( async (id: string, workspaceId: string) => { try { const { tasksDeleted } = await reset(id, workspaceId); toast({ description: tasksDeleted > 0 ? `Reset complete — deleted ${tasksDeleted} task(s); next poll will re-import.` : "Reset complete — next poll will re-import matches.", variant: "success", }); } catch { toast({ description: "Failed to reset issue watch", variant: "error" }); throw new Error("reset failed"); } }, [reset, toast], ); return { watches, create, update, previewReset, handleDelete, handleTrigger, handleReset, handleToggleEnabled, }; } export function GitHubConnectionSection() { return ( <>
Connect Kandev to GitHub. Authentication is shared across all workspaces; PR/issue watchers and presets are configured per workspace.