"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 { 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 } = 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], ); return { watches, create, update, handleDelete, handleTrigger, handleToggleEnabled }; } function useIssueWatchActions(workspaceId?: string | null) { const { items: watches, create, update, remove, trigger } = 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], ); return { watches, create, update, handleDelete, handleTrigger, 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, handleDelete, handleTrigger, handleToggleEnabled } = useWatchActions(undefined); const { toast } = useToast(); const [dialogOpen, setDialogOpen] = useState(false); const [editingWatch, setEditingWatch] = useState(null); const handleEdit = useCallback((watch: ReviewWatch) => { setEditingWatch(watch); setDialogOpen(true); }, []); 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" }); }} /> ); } function IssueWatchSection() { const issueActions = useIssueWatchActions(undefined); const { toast } = useToast(); const [dialogOpen, setDialogOpen] = useState(false); const [editingWatch, setEditingIssueWatch] = useState(null); const handleEdit = useCallback((watch: IssueWatch) => { setEditingIssueWatch(watch); setDialogOpen(true); }, []); 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" }); }} /> ); }