mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 16:20:35 +02:00
* feat(github): GitHub App backend for PR ↔ issue linking - New tables: github_installation (workspace ↔ App install), github_pull_request (mirrored PR state), issue_pull_request (M:N link). - Webhook handler verifies HMAC-SHA256, upserts PR rows, parses issue identifiers from PR title/body/branch and auto-links them. Merging a linked PR moves the issue to done. - Connect/setup endpoints power the zero-config "Connect GitHub" install flow; state token is HMAC-signed so the setup callback can recover the workspace. - Workspace-scoped admin routes for listing/disconnecting installations, plus a per-issue `pull-requests` list endpoint. Co-authored-by: multica-agent <github@multica.ai> * feat(github): UI for connecting GitHub and viewing linked PRs - Settings → Integrations: new tab with Connect GitHub / installations list / disconnect, gated on the deployment having the App configured. - Issue detail sidebar: Pull requests section showing linked PR title, repo, state (open/draft/merged/closed), and author, with deep link to GitHub. - Real-time refresh: github_installation:* and pull_request:* events invalidate the matching TanStack Query caches. Co-authored-by: multica-agent <github@multica.ai> * fix(github): address review — null actor, role gating, configured guard, scoped uninstall broadcast - listeners: use optionalUUID(e.ActorID) so the system actor on the github-driven issue:updated event no longer panics activity / notification listeners; merged-PR → issue done now produces a status_changed activity and inbox entry. - IntegrationsTab: gate the admin-only installations query on canManage so members no longer hit /github/installations 403; the configured/not-configured copy is also scoped to admins. - backend: introduce isGitHubConfigured() requiring both GITHUB_APP_SLUG and GITHUB_WEBHOOK_SECRET, and surface that single flag from list-installations + connect endpoints so the frontend Connect button stays disabled until both are set. - DeleteGitHubInstallationByInstallationID now RETURNs workspace_id; webhook handler publishes github_installation:deleted scoped to the right workspace so already-open Settings tabs invalidate in real time. ErrNoRows on a re-fired delete short-circuits cleanly. - tests: focused webhook integration coverage (auto-link + merge → done, cancelled preservation, uninstall returns workspace). Co-authored-by: multica-agent <github@multica.ai> * fix(github): i18n the new GitHub UI strings to satisfy lint CI flagged every literal string in the Integrations tab, the Pull requests sidebar section, and the per-PR row label. Move them through useT() and add the matching `integrations.*` block to settings.json (en / zh-Hans) plus `detail.section_pull_requests` / `detail.pull_request_state_*` / loading + empty copy under `issues.json`. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
GitPullRequest,
|
|
GitPullRequestArrow,
|
|
GitPullRequestClosed,
|
|
GitMerge,
|
|
GitPullRequestDraft,
|
|
} from "lucide-react";
|
|
import { issuePullRequestsOptions } from "@multica/core/github/queries";
|
|
import type { GitHubPullRequest, GitHubPullRequestState } from "@multica/core/types";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
import { useT } from "../../i18n";
|
|
|
|
const STATE_ICON: Record<
|
|
GitHubPullRequestState,
|
|
{ icon: React.ComponentType<{ className?: string }>; className: string }
|
|
> = {
|
|
open: { icon: GitPullRequestArrow, className: "text-emerald-600 dark:text-emerald-400" },
|
|
draft: { icon: GitPullRequestDraft, className: "text-muted-foreground" },
|
|
merged: { icon: GitMerge, className: "text-violet-600 dark:text-violet-400" },
|
|
closed: { icon: GitPullRequestClosed, className: "text-rose-600 dark:text-rose-400" },
|
|
};
|
|
|
|
export function PullRequestList({ issueId }: { issueId: string }) {
|
|
const { t } = useT("issues");
|
|
const { data, isLoading } = useQuery(issuePullRequestsOptions(issueId));
|
|
const prs = data?.pull_requests ?? [];
|
|
|
|
if (isLoading) {
|
|
return <p className="text-xs text-muted-foreground px-2">{t(($) => $.detail.pull_requests_loading)}</p>;
|
|
}
|
|
if (prs.length === 0) {
|
|
return (
|
|
<p className="text-xs text-muted-foreground px-2">
|
|
{t(($) => $.detail.pull_requests_empty)}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{prs.map((pr) => (
|
|
<PullRequestRow key={pr.id} pr={pr} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PullRequestRow({ pr }: { pr: GitHubPullRequest }) {
|
|
const { t } = useT("issues");
|
|
const cfg = STATE_ICON[pr.state] ?? { icon: GitPullRequest, className: "" };
|
|
const Icon = cfg.icon;
|
|
const label =
|
|
pr.state === "open"
|
|
? t(($) => $.detail.pull_request_state_open)
|
|
: pr.state === "draft"
|
|
? t(($) => $.detail.pull_request_state_draft)
|
|
: pr.state === "merged"
|
|
? t(($) => $.detail.pull_request_state_merged)
|
|
: pr.state === "closed"
|
|
? t(($) => $.detail.pull_request_state_closed)
|
|
: pr.state;
|
|
return (
|
|
<a
|
|
href={pr.html_url}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="flex items-start gap-2 rounded-md px-2 py-1.5 -mx-2 hover:bg-accent/50 transition-colors group"
|
|
>
|
|
<Icon className={cn("h-3.5 w-3.5 mt-0.5 shrink-0", cfg.className)} />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs font-medium truncate group-hover:text-foreground">{pr.title}</p>
|
|
<p className="text-[11px] text-muted-foreground truncate">
|
|
{pr.repo_owner}/{pr.repo_name}#{pr.number} · {label}
|
|
{pr.author_login ? ` · @${pr.author_login}` : null}
|
|
</p>
|
|
</div>
|
|
</a>
|
|
);
|
|
}
|