"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 ( <>

GitHub Integration

Connect Kandev to GitHub. Authentication is shared across all workspaces; PR/issue watchers and presets are configured per workspace.

); } // PerWorkspaceSection wraps the still-per-workspace surfaces (action presets, // PR analytics) under a workspace switcher. Watchers don't go in here — they // list flat across every workspace via their own sections. function PerWorkspaceSection({ workspaceId }: { workspaceId: string }) { return (
); } export function GitHubIntegrationPage() { return (
{(ws) => }
); } // ReviewWatchSection lists every review watch across every workspace in a // single flat table. Pass `undefined` to the hook so it fetches all rows; the // dialog's create flow asks the user to pick the workspace. function ReviewWatchSection() { const { watches, create, update, previewReset, handleDelete, handleTrigger, handleReset, handleToggleEnabled, } = useWatchActions(undefined); const { toast } = useToast(); const [dialogOpen, setDialogOpen] = useState(false); const [editingWatch, setEditingWatch] = useState(null); const resetCtrl = useWatchResetController({ preview: (w) => previewReset(w.id, w.workspace_id), reset: (w) => handleReset(w.id, w.workspace_id), }); const handleEdit = useCallback((watch: ReviewWatch) => { setEditingWatch(watch); setDialogOpen(true); }, []); const { setResetting: setReviewResetting } = resetCtrl; const onResetClick = useCallback( (id: string) => { const w = watches.find((item) => item.id === id); if (w) setReviewResetting(w); }, [watches, setReviewResetting], ); return ( <> } > { await create(req); toast({ description: "Review watch created", variant: "success" }); }} onUpdate={async (id, req) => { await update(id, req); toast({ description: "Review watch updated", variant: "success" }); }} /> {resetCtrl.resetting && ( )} ); } function IssueWatchSection() { const issueActions = useIssueWatchActions(undefined); const { toast } = useToast(); const [dialogOpen, setDialogOpen] = useState(false); const [editingWatch, setEditingIssueWatch] = useState(null); const resetCtrl = useWatchResetController({ preview: (w) => issueActions.previewReset(w.id, w.workspace_id), reset: (w) => issueActions.handleReset(w.id, w.workspace_id), }); const handleEdit = useCallback((watch: IssueWatch) => { setEditingIssueWatch(watch); setDialogOpen(true); }, []); const { setResetting: setIssueResetting } = resetCtrl; const onResetClick = useCallback( (id: string) => { const w = issueActions.watches.find((item) => item.id === id); if (w) setIssueResetting(w); }, [issueActions.watches, setIssueResetting], ); return ( <> } > { await issueActions.create(req); toast({ description: "Issue watch created", variant: "success" }); }} onUpdate={async (id, req) => { await issueActions.update(id, req); toast({ description: "Issue watch updated", variant: "success" }); }} /> {resetCtrl.resetting && ( )} ); }