diff --git a/apps/web/app/(dashboard)/settings/_components/integrations-tab.tsx b/apps/web/app/(dashboard)/settings/_components/integrations-tab.tsx new file mode 100644 index 000000000..1b3625953 --- /dev/null +++ b/apps/web/app/(dashboard)/settings/_components/integrations-tab.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { GitBranch, Trash2, ExternalLink, Plus } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { toast } from "sonner"; +import { useAuthStore } from "@/features/auth"; +import { useWorkspaceStore } from "@/features/workspace"; +import { api } from "@/shared/api"; +import type { GitHubInstallation } from "@/shared/types"; +import { useWSEvent } from "@/features/realtime"; + +export function IntegrationsTab() { + const user = useAuthStore((s) => s.user); + const workspace = useWorkspaceStore((s) => s.workspace); + const members = useWorkspaceStore((s) => s.members); + + const [installations, setInstallations] = useState([]); + const [loading, setLoading] = useState(true); + + const currentMember = members.find((m) => m.user_id === user?.id) ?? null; + const canManage = currentMember?.role === "owner" || currentMember?.role === "admin"; + + const fetchInstallations = useCallback(async () => { + try { + const data = await api.listGitHubInstallations(); + setInstallations(data); + } catch { + // Silently handle — workspace may not have any installations + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchInstallations(); + }, [fetchInstallations]); + + // Real-time updates + useWSEvent( + "github_installation:created", + useCallback(() => fetchInstallations(), [fetchInstallations]), + ); + useWSEvent( + "github_installation:deleted", + useCallback(() => fetchInstallations(), [fetchInstallations]), + ); + + const handleConnect = () => { + const appSlug = process.env.NEXT_PUBLIC_GITHUB_APP_SLUG; + if (!appSlug) { + toast.error("GitHub App not configured. Set NEXT_PUBLIC_GITHUB_APP_SLUG."); + return; + } + // The callback URL is configured in the GitHub App settings. + // We pass workspace_id as state so the backend knows which workspace to link. + const callbackUrl = `${window.location.origin}/api/github/callback`; + const installUrl = `https://github.com/apps/${appSlug}/installations/new?state=${workspace?.id}`; + window.open(installUrl, "_blank", "noopener,noreferrer"); + }; + + const handleDisconnect = async (installation: GitHubInstallation) => { + try { + await api.deleteGitHubInstallation(installation.installation_id); + setInstallations((prev) => prev.filter((i) => i.id !== installation.id)); + toast.success(`Disconnected ${installation.account_login}`); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Failed to disconnect"); + } + }; + + if (!workspace) return null; + + return ( +
+
+

GitHub Integration

+ + + +

+ Connect your GitHub repositories to automatically link pull requests to issues. + When a PR branch or description contains an issue identifier (e.g. MUL-82), + the PR status will appear on the issue and merging will mark it as done. +

+ + {loading ? ( +
Loading...
+ ) : installations.length > 0 ? ( +
+ {installations.map((gi) => ( +
+
+ +
+
{gi.account_login}
+
+ {gi.account_type} · Connected{" "} + {new Date(gi.created_at).toLocaleDateString()} +
+
+
+
+ + + + {canManage && ( + + )} +
+
+ ))} +
+ ) : ( +
+ +

+ No GitHub accounts connected +

+

+ Install the Multica GitHub App to link PRs with issues automatically. +

+
+ )} + + {canManage && ( +
+ +
+ )} + + {!canManage && ( +

+ Only admins and owners can manage integrations. +

+ )} +
+
+
+
+ ); +} diff --git a/apps/web/app/(dashboard)/settings/page.tsx b/apps/web/app/(dashboard)/settings/page.tsx index 7042ba808..7173f2126 100644 --- a/apps/web/app/(dashboard)/settings/page.tsx +++ b/apps/web/app/(dashboard)/settings/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { User, Palette, Key, Settings, Users, FolderGit2 } from "lucide-react"; +import { User, Palette, Key, Settings, Users, FolderGit2, Plug } from "lucide-react"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { useWorkspaceStore } from "@/features/workspace"; import { AccountTab } from "./_components/account-tab"; @@ -9,6 +9,7 @@ import { TokensTab } from "./_components/tokens-tab"; import { WorkspaceTab } from "./_components/workspace-tab"; import { MembersTab } from "./_components/members-tab"; import { RepositoriesTab } from "./_components/repositories-tab"; +import { IntegrationsTab } from "./_components/integrations-tab"; const accountTabs = [ { value: "profile", label: "Profile", icon: User }, @@ -19,6 +20,7 @@ const accountTabs = [ const workspaceTabs = [ { value: "workspace", label: "General", icon: Settings }, { value: "repositories", label: "Repositories", icon: FolderGit2 }, + { value: "integrations", label: "Integrations", icon: Plug }, { value: "members", label: "Members", icon: Users }, ]; @@ -63,6 +65,7 @@ export default function SettingsPage() { + diff --git a/apps/web/features/issues/components/issue-detail.tsx b/apps/web/features/issues/components/issue-detail.tsx index 43c388850..80cde2f6f 100644 --- a/apps/web/features/issues/components/issue-detail.tsx +++ b/apps/web/features/issues/components/issue-detail.tsx @@ -63,6 +63,7 @@ import { StatusIcon, PriorityIcon, DueDatePicker, AssigneePicker, canAssignAgent import { CommentCard } from "./comment-card"; import { CommentInput } from "./comment-input"; import { AgentLiveCard, TaskRunHistory } from "./agent-live-card"; +import { PRStatusCard } from "./pr-status-card"; import { api } from "@/shared/api"; import { useAuthStore } from "@/features/auth"; import { useWorkspaceStore, useActorName } from "@/features/workspace"; @@ -1030,6 +1031,9 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo } + {/* Pull Requests section */} + + {/* Details section */}