"use client"; import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { ExternalLink, GitCommitHorizontal, Link2, PanelRight } from "lucide-react"; import { Button } from "@multica/ui/components/ui/button"; import { Card, CardContent } from "@multica/ui/components/ui/card"; import { Label } from "@multica/ui/components/ui/label"; import { Switch } from "@multica/ui/components/ui/switch"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@multica/ui/components/ui/alert-dialog"; import { useAuthStore } from "@multica/core/auth"; import { useWorkspaceId } from "@multica/core/hooks"; import { useCurrentWorkspace } from "@multica/core/paths"; import { memberListOptions, workspaceKeys } from "@multica/core/workspace/queries"; import { deriveGitHubSettings, githubInstallationsOptions, } from "@multica/core/github"; import { api } from "@multica/core/api"; import type { Workspace } from "@multica/core/types"; import { useNavigation } from "../../navigation"; import { useT } from "../../i18n"; import { GitHubMark } from "./github-mark"; type SettingsKey = | "github_enabled" | "github_pr_sidebar_enabled" | "co_authored_by_enabled" | "github_auto_link_prs_enabled"; export function GitHubTab() { const { t } = useT("settings"); const workspace = useCurrentWorkspace(); const wsId = useWorkspaceId(); const qc = useQueryClient(); const navigation = useNavigation(); const user = useAuthStore((s) => s.user); const { data: members = [] } = useQuery(memberListOptions(wsId)); const currentMember = members.find((m) => m.user_id === user?.id) ?? null; // `canView` gates the read-only installation list (every workspace member // sees it after MUL-2413); `canManage` gates the Connect / Disconnect // actions and comes from the backend response (`can_manage`) so the // frontend never claims management rights the server would reject. const canView = !!currentMember; const { data: installationData } = useQuery({ ...githubInstallationsOptions(wsId), enabled: !!wsId && canView, }); const installations = installationData?.installations ?? []; const configured = installationData?.configured ?? false; const canManage = installationData?.can_manage === true; const connected = installations.length > 0; const primaryInstallation = installations[0] ?? null; const flags = deriveGitHubSettings(workspace); const [savingKey, setSavingKey] = useState(null); const [connecting, setConnecting] = useState(false); const [disconnectTarget, setDisconnectTarget] = useState(null); const [disconnecting, setDisconnecting] = useState(false); async function persistSetting(key: SettingsKey, next: boolean) { if (!workspace || savingKey) return; setSavingKey(key); try { const merged = { ...((workspace.settings as Record) ?? {}), [key]: next, }; const updated = await api.updateWorkspace(workspace.id, { settings: merged }); qc.setQueryData(workspaceKeys.list(), (old: Workspace[] | undefined) => old?.map((ws) => (ws.id === updated.id ? updated : ws)), ); } catch (e) { toast.error(e instanceof Error ? e.message : t(($) => $.github.toast_failed)); } finally { setSavingKey(null); } } async function handleConnect() { setConnecting(true); try { const resp = await api.getGitHubConnectURL(wsId); if (!resp.configured || !resp.url) { toast.error(t(($) => $.github.toast_not_configured)); return; } window.open(resp.url, "_blank", "noopener"); } catch (e) { toast.error(e instanceof Error ? e.message : t(($) => $.github.toast_open_failed)); } finally { setConnecting(false); } } async function handleDisconnect() { if (!disconnectTarget || disconnecting) return; setDisconnecting(true); try { await api.deleteGitHubInstallation(wsId, disconnectTarget); await qc.invalidateQueries({ queryKey: ["github", wsId] }); toast.success(t(($) => $.github.toast_disconnected)); setDisconnectTarget(null); } catch (e) { toast.error(e instanceof Error ? e.message : t(($) => $.github.toast_disconnect_failed)); } finally { setDisconnecting(false); } } if (!workspace) return null; const repositoriesHref = `${navigation.pathname}?tab=repositories`; return (

{t(($) => $.github.page_description)}

{flags.enabled ? t(($) => $.github.master_description_on) : t(($) => $.github.master_description_off)}

persistSetting("github_enabled", v)} disabled={!canManage || savingKey === "github_enabled"} />

{t(($) => $.github.section_connection)}

{t(($) => $.github.connection_title)}

{connected ? ( <>

{t(($) => $.github.connected_to, { login: installations.map((i) => i.account_login).join(", "), })}

{primaryInstallation?.connected_by && (

{t(($) => $.github.connected_by, { name: primaryInstallation.connected_by!, })}

)} ) : canManage ? (

{t(($) => $.github.connection_description_prefix)}{" "} {t(($) => $.github.connection_identifier_example)} {" "} {t(($) => $.github.connection_description_suffix)}{" "} {t(($) => $.github.connection_description_done)}.

) : (

{t(($) => $.github.contact_admin_to_connect)}

)}
{canManage && (
{connected && primaryInstallation ? ( // Disconnect must stay reachable even when the master switch // is off — disconnect is a separate intent (revoke the App // grant) from hiding the feature. ) : ( )}
)}
{canManage && !configured && (

{t(($) => $.github.not_configured)}{" "} GITHUB_APP_SLUG{" "} {t(($) => $.github.not_configured_and)}{" "} GITHUB_WEBHOOK_SECRET.

)} {!canManage && connected && (

{t(($) => $.github.read_only_hint)}

)}

{t(($) => $.github.section_features)}

} label={t(($) => $.github.feature_pr_sidebar_label)} description={

{t(($) => $.github.feature_pr_sidebar_description)}

} checked={flags.prSidebar} disabled={!canManage || !flags.enabled || savingKey === "github_pr_sidebar_enabled"} onCheckedChange={(v) => persistSetting("github_pr_sidebar_enabled", v)} /> } label={t(($) => $.github.feature_co_author_label)} description={

{t(($) => $.github.feature_co_author_description_prefix)}{" "} {"Co-authored-by: multica-agent "} {" "} {t(($) => $.github.feature_co_author_description_suffix)}

} checked={flags.coAuthor} disabled={!canManage || !flags.enabled || savingKey === "co_authored_by_enabled"} onCheckedChange={(v) => persistSetting("co_authored_by_enabled", v)} /> } label={t(($) => $.github.feature_auto_link_label)} description={

{t(($) => $.github.feature_auto_link_description)}

} checked={flags.autoLinkPRs} disabled={!canManage || !flags.enabled || savingKey === "github_auto_link_prs_enabled"} onCheckedChange={(v) => persistSetting("github_auto_link_prs_enabled", v)} />

{t(($) => $.github.section_repositories)}

{t(($) => $.github.repositories_shortcut_label)}

{ if (!v && !disconnecting) setDisconnectTarget(null); }} > {t(($) => $.github.disconnect_confirm_title)} {t(($) => $.github.disconnect_confirm_description)} {t(($) => $.github.disconnect_confirm_cancel)} {disconnecting ? t(($) => $.github.disconnecting) : t(($) => $.github.disconnect_confirm_action)}
); } function FeatureRow({ id, icon, label, description, checked, disabled, onCheckedChange, }: { id: string; icon: React.ReactNode; label: string; description: React.ReactNode; checked: boolean; disabled: boolean; onCheckedChange: (v: boolean) => void; }) { return (
{icon}
{description}
); }