From 98ba9ccedb63e90e582e26e8e2322e813c727062 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 24 Sep 2026 14:54:56 -0700 Subject: [PATCH] feat(desktop): a pull request pane is keyed by repository and number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pane's data was `{ prNumber }` and it borrowed the repository from whatever workspace it was open in, so it needed a project to render and showed "Choose a project" on a cloud workspace whose host row had not arrived. A number only means something inside a repository; with multi-repo workspaces the borrowed one is ambiguous, and with cloud boxes it is absent. The pane, the intent store and every opener now carry the PR's own identity, `{ repoFullName, number }`, parsed from the PR's URL where an opener only had that. One vocabulary for what a PR looks like lives in @superset/shared/pull-request; the API builds it once from the webhook row (getByBranches now returns it normalized) and the host's `gh pr view` output maps into it through one adapter. A new integration.github. getPullRequest serves a PR's content by repository and number with the installation's token. The pane reads through the workspace's host when its project is the PR's repository — the person's own gh, no GitHub App required, unchanged for every local and remote host — and through the API otherwise, which is what a closed cloud box or a PR in a repository nobody has checked out needs. Threads and the close/merge actions stay host-side for the workspace's linked PR. Claude-Session: https://claude.ai/code/session_01RL946q6aRucLVJs659sW24 --- .../OpenBrowserPageInAppButton.tsx | 18 ++-- .../getPullRequestTarget.test.ts | 30 ++++-- .../getPullRequestTarget.ts | 27 ++++-- .../src/renderer/lib/github/pullRequestRef.ts | 24 +++++ .../DashboardSidebarExpandedWorkspaceRow.tsx | 8 +- .../toSidebarPullRequest.ts | 69 +------------ .../PullRequestDetailHeader.tsx | 21 ++-- .../usePullRequestDetail.ts | 12 ++- .../fromHostPullRequestContent.ts | 44 +++++++++ .../utils/fromHostPullRequestContent/index.ts | 1 + .../ChangesControl/ChangesControl.tsx | 3 +- .../PRStatusGroup/PRStatusGroup.tsx | 12 ++- .../components/ShipControl/ShipControl.tsx | 8 +- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 3 +- .../components/PRHeader/PRHeader.tsx | 12 ++- .../ReviewTabContent/ReviewTabContent.tsx | 3 +- .../hooks/useReviewTab/useReviewTab.tsx | 3 +- .../replaceBrowserPane.test.ts | 2 +- .../DiffPanePRLink/DiffPanePRLink.tsx | 10 +- .../PullRequestPane/PullRequestPane.tsx | 87 ++++++++--------- .../PullRequestPaneHeaderExtras.tsx | 74 +++++++------- .../hooks/usePullRequestPaneDetail/index.ts | 1 + .../usePullRequestPaneDetail.ts | 48 ++++++++++ .../hooks/usePullRequestPaneProject/index.ts | 1 - .../usePullRequestPaneProject.ts | 22 ----- .../hooks/usePaneRegistry/usePaneRegistry.tsx | 2 +- .../usePullRequestPaneIntentOpener.ts | 8 +- .../useWorkspacePaneOpeners.ts | 7 +- .../v2-workspace/$workspaceId/types.ts | 4 +- .../openPullRequestPaneInStore.test.ts | 52 +++++++--- .../openPullRequestPaneInStore.ts | 22 +++-- .../V2WorkspaceRow/V2WorkspaceRow.tsx | 8 +- .../stores/pull-request-pane-intent.ts | 3 +- packages/i18n/locales/cs/messages.po | 10 +- packages/i18n/locales/de/messages.po | 10 +- packages/i18n/locales/en/messages.po | 10 +- packages/i18n/locales/es/messages.po | 10 +- packages/i18n/locales/fr/messages.po | 10 +- packages/i18n/locales/id/messages.po | 10 +- packages/i18n/locales/it/messages.po | 10 +- packages/i18n/locales/ja/messages.po | 10 +- packages/i18n/locales/ko/messages.po | 10 +- packages/i18n/locales/nl/messages.po | 10 +- packages/i18n/locales/pl/messages.po | 10 +- packages/i18n/locales/pt-BR/messages.po | 10 +- packages/i18n/locales/ru/messages.po | 10 +- packages/i18n/locales/tr/messages.po | 10 +- packages/i18n/locales/vi/messages.po | 10 +- packages/i18n/locales/zh-CN/messages.po | 10 +- packages/i18n/locales/zh-TW/messages.po | 10 +- packages/i18n/src/server-errors.ts | 6 ++ packages/shared/package.json | 4 + packages/shared/src/pull-request.ts | 48 ++++++++++ .../src/router/integration/github/github.ts | 96 ++++++++++++++++++- .../integration/github/pull-request-shape.ts | 72 ++++++++++++++ 55 files changed, 731 insertions(+), 314 deletions(-) create mode 100644 apps/desktop/src/renderer/lib/github/pullRequestRef.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/fromHostPullRequestContent.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/usePullRequestPaneDetail.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/usePullRequestPaneProject.ts create mode 100644 packages/shared/src/pull-request.ts create mode 100644 packages/trpc/src/router/integration/github/pull-request-shape.ts diff --git a/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/OpenBrowserPageInAppButton.tsx b/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/OpenBrowserPageInAppButton.tsx index 9d7f3a7e3f4..602997a8012 100644 --- a/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/OpenBrowserPageInAppButton.tsx +++ b/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/OpenBrowserPageInAppButton.tsx @@ -24,7 +24,11 @@ export function OpenBrowserPageInAppButton({ const target = getPullRequestTarget(currentUrl, projects); const openPage = useOpenPage(); const pageSlug = parseSupersetPageUrl(currentUrl, env.NEXT_PUBLIC_WEB_URL); - if (!target && !pageSlug) return null; + const canOpen = + pageSlug !== null || + (target !== null && + (onOpenInPane !== undefined || target.projectId !== null)); + if (!canOpen) return null; return ( @@ -38,24 +42,18 @@ export function OpenBrowserPageInAppButton({ if (pageSlug) onOpenInPane({ kind: "page", data: { slug: pageSlug } }); else if (target) - onOpenInPane({ - kind: "pull-request", - data: { - prNumber: Number(target.prNumber), - projectId: target.projectId, - }, - }); + onOpenInPane({ kind: "pull-request", data: target.ref }); return; } if (pageSlug) { openPage({ slug: pageSlug }); return; } - if (!target) return; + if (!target?.projectId) return; usePullRequestsSplitViewStore.getState().expandDetail(); void navigate({ to: "/pull-requests/$prNumber", - params: { prNumber: target.prNumber }, + params: { prNumber: String(target.ref.number) }, search: { project: target.projectId }, }); }} diff --git a/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.test.ts b/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.test.ts index 04672080e97..dfc91e9b55c 100644 --- a/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.test.ts +++ b/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.test.ts @@ -5,6 +5,7 @@ const projects = [ { projectKey: "other", repoOwner: "other", repoName: "repo" }, { projectKey: "matching", repoOwner: "superset-sh", repoName: "superset" }, ]; +const ref = { repoFullName: "superset-sh/superset", number: 42 }; describe("getPullRequestTarget", () => { it.each([ @@ -20,16 +21,19 @@ describe("getPullRequestTarget", () => { `https://github.com/superset-sh/superset/pull/42${suffix}`, projects, ), - ).toEqual({ projectId: "matching", prNumber: "42" }); + ).toEqual({ ref, projectId: "matching" }); }); - it("matches repository names case-insensitively", () => { + it("matches the project case-insensitively and keeps the URL's spelling", () => { expect( getPullRequestTarget( - "https://www.github.com/SUPERSET-SH/Superset/pull/42", + "https://github.com/SUPERSET-SH/Superset/pull/42", projects, ), - ).toEqual({ projectId: "matching", prNumber: "42" }); + ).toEqual({ + ref: { repoFullName: "SUPERSET-SH/Superset", number: 42 }, + projectId: "matching", + }); }); it.each([ @@ -37,22 +41,28 @@ describe("getPullRequestTarget", () => { "https://github.com/superset-sh/superset/issues/42", "https://github.com/superset-sh/superset/pulls", "https://github.com/superset-sh/superset/pull/new", - "https://github.com/superset-sh/superset/pull/0", "https://github.com/superset-sh/superset/pull/42oops", - "https://github.com/superset-sh/superset/pull/9007199254740992", "https://github.com.evil.com/superset-sh/superset/pull/42", "https://example.com/superset-sh/superset/pull/42", - "https://github.com/untracked/repo/pull/42", - ])("does not offer an incorrect destination: %s", (url) => { + ])("is not a pull request: %s", (url) => { expect(getPullRequestTarget(url, projects)).toBeNull(); }); - it("waits for matching projects to be available", () => { + it("names the pull request even when no project has its repository", () => { + expect( + getPullRequestTarget( + "https://github.com/untracked/repo/pull/42", + projects, + ), + ).toEqual({ + ref: { repoFullName: "untracked/repo", number: 42 }, + projectId: null, + }); expect( getPullRequestTarget( "https://github.com/superset-sh/superset/pull/42", [], ), - ).toBeNull(); + ).toEqual({ ref, projectId: null }); }); }); diff --git a/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.ts b/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.ts index 7e548535992..8340d067daf 100644 --- a/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.ts +++ b/apps/desktop/src/renderer/components/OpenBrowserPageInAppButton/utils/getPullRequestTarget/getPullRequestTarget.ts @@ -1,23 +1,30 @@ +import { + type PullRequestRef, + pullRequestRefFromUrl, +} from "renderer/lib/github/pullRequestRef"; + interface Project { projectKey: string; repoOwner: string | null; repoName: string | null; } +/** + * The pull request a GitHub URL points at, and the project that has its + * repository checked out when one does. A pane needs only the ref; the + * project-scoped Pull requests screen needs the project. + */ export function getPullRequestTarget( url: string, projects: readonly Project[], -) { - const match = url.match( - /^https?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/([1-9]\d*)(?:[/?#].*)?$/i, - ); - if (!match) return null; - const [, owner, repo, prNumber] = match; - if (!Number.isSafeInteger(Number(prNumber))) return null; +): { ref: PullRequestRef; projectId: string | null } | null { + const ref = pullRequestRefFromUrl(url); + if (!ref) return null; + const [owner, name] = ref.repoFullName.split("/"); const project = projects.find( (candidate) => - candidate.repoOwner?.toLowerCase() === owner.toLowerCase() && - candidate.repoName?.toLowerCase() === repo.toLowerCase(), + candidate.repoOwner?.toLowerCase() === owner?.toLowerCase() && + candidate.repoName?.toLowerCase() === name?.toLowerCase(), ); - return project ? { projectId: project.projectKey, prNumber } : null; + return { ref, projectId: project?.projectKey ?? null }; } diff --git a/apps/desktop/src/renderer/lib/github/pullRequestRef.ts b/apps/desktop/src/renderer/lib/github/pullRequestRef.ts new file mode 100644 index 00000000000..4b220b40ad8 --- /dev/null +++ b/apps/desktop/src/renderer/lib/github/pullRequestRef.ts @@ -0,0 +1,24 @@ +/** A pull request by its own identity: the repository it lives in and its number. */ +export interface PullRequestRef { + repoFullName: string; + number: number; +} + +const PULL_REQUEST_URL = + /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:[/?#]|$)/; + +export function pullRequestRefFromUrl(url: string): PullRequestRef | null { + const match = PULL_REQUEST_URL.exec(url); + if (!match?.[1] || !match[2]) return null; + return { repoFullName: match[1], number: Number(match[2]) }; +} + +export function isSamePullRequest( + left: PullRequestRef, + right: PullRequestRef, +): boolean { + return ( + left.number === right.number && + left.repoFullName.toLowerCase() === right.repoFullName.toLowerCase() + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx index 96a8781d907..65762175b69 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -18,6 +18,7 @@ import { WorkspaceNameMarquee } from "renderer/components/WorkspaceNameMarquee"; import type { DiffStats } from "renderer/hooks/host-service/useDiffStats"; import { useFocusVisible } from "renderer/hooks/useFocusVisible"; import { HotkeyLabel } from "renderer/hotkeys"; +import { pullRequestRefFromUrl } from "renderer/lib/github/pullRequestRef"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; @@ -210,9 +211,14 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< event.stopPropagation(); // Lands in the workspace with its PR pane open, rather // than on GitHub; the pane keeps the GitHub link. + const ref = pullRequestRefFromUrl(pullRequest.url); + if (!ref) { + window.open(pullRequest.url, "_blank"); + return; + } usePullRequestPaneIntent.getState().request({ workspaceId: workspace.id, - prNumber: pullRequest.number, + ...ref, }); void navigateToV2Workspace(workspace.id, navigate); }} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarCloudPullRequests/toSidebarPullRequest.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarCloudPullRequests/toSidebarPullRequest.ts index 96f4bf19c70..58bf263bef6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarCloudPullRequests/toSidebarPullRequest.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarCloudPullRequests/toSidebarPullRequest.ts @@ -1,79 +1,20 @@ import type { RouterOutputs } from "@superset/trpc"; -import type { - DashboardSidebarWorkspacePullRequest, - DashboardSidebarWorkspacePullRequestCheck, -} from "../../types"; +import type { DashboardSidebarWorkspacePullRequest } from "../../types"; export type CloudPullRequestRow = RouterOutputs["integration"]["github"]["getByBranches"]["pullRequests"][number]; -function toCheckStatus(check: { - status: string; - conclusion: string | null; -}): DashboardSidebarWorkspacePullRequestCheck["status"] { - if (check.status.toLowerCase() !== "completed") return "pending"; - switch (check.conclusion?.toLowerCase()) { - case "success": - return "success"; - case "skipped": - case "neutral": - return "skipped"; - case "cancelled": - return "cancelled"; - case null: - case undefined: - return "pending"; - default: - return "failure"; - } -} - -function toReviewDecision( - value: string | null, -): DashboardSidebarWorkspacePullRequest["reviewDecision"] { - switch (value) { - case "APPROVED": - return "approved"; - case "CHANGES_REQUESTED": - return "changes_requested"; - case "REVIEW_REQUIRED": - return "pending"; - default: - return null; - } -} - -function toChecksStatus( - value: string, -): DashboardSidebarWorkspacePullRequest["checksStatus"] { - return value === "success" || value === "failure" || value === "pending" - ? value - : "none"; -} - /** The host's chip shape; `queued` (merge queue) is host-only, so it shows as open here. */ export function toSidebarPullRequest( row: CloudPullRequestRow, ): DashboardSidebarWorkspacePullRequest { - const state = - row.state === "merged" || row.mergedAt - ? "merged" - : row.state === "closed" - ? "closed" - : row.isDraft - ? "draft" - : "open"; return { url: row.url, number: row.number, title: row.title, - state, - reviewDecision: toReviewDecision(row.reviewDecision), - checksStatus: toChecksStatus(row.checksStatus), - checks: row.checks.map((check) => ({ - name: check.name, - status: toCheckStatus(check), - url: check.detailsUrl ?? null, - })), + state: row.state === "open" && row.isDraft ? "draft" : row.state, + reviewDecision: row.reviewDecision, + checksStatus: row.checksStatus, + checks: row.checks, }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestDetailHeader/PullRequestDetailHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestDetailHeader/PullRequestDetailHeader.tsx index ef2f794bcda..1b3e4e8dda5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestDetailHeader/PullRequestDetailHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/components/PullRequestDetailHeader/PullRequestDetailHeader.tsx @@ -418,14 +418,17 @@ export function PullRequestDetailHeader({ - {data.author.slice(0, 1).toUpperCase()} + {data.author.login.slice(0, 1).toUpperCase()} - {data.author} + {data.author.login} )} @@ -439,14 +442,14 @@ export function PullRequestDetailHeader({ @@ -524,14 +527,14 @@ export function PullRequestDetailHeader({ ) : pendingAction?.kind === "merge" && pendingAction.force ? ( - "{data.title}" will be merged into {data.baseBranch} via{" "} + "{data.title}" will be merged into {data.base.ref} via{" "} {mergeMethodLabels[pendingAction.method].toLowerCase()}. Checks haven't passed yet — this overrides them. This can't be undone from here. ) : pendingAction?.kind === "merge" ? ( - "{data.title}" will be merged into {data.baseBranch} via{" "} + "{data.title}" will be merged into {data.base.ref} via{" "} {mergeMethodLabels[pendingAction.method].toLowerCase()}. This can't be undone from here. diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/hooks/usePullRequestDetail/usePullRequestDetail.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/hooks/usePullRequestDetail/usePullRequestDetail.ts index 0e4330bc7f6..70067541854 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/hooks/usePullRequestDetail/usePullRequestDetail.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/hooks/usePullRequestDetail/usePullRequestDetail.ts @@ -1,11 +1,11 @@ -import type { AppRouter as HostServiceAppRouter } from "@superset/host-service"; +import type { RouterOutputs } from "@superset/trpc"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import type { inferRouterOutputs } from "@trpc/server"; import { useCallback } from "react"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { fromHostPullRequestContent } from "../../utils/fromHostPullRequestContent"; export type PullRequestDetail = - inferRouterOutputs["pullRequests"]["getContent"]; + RouterOutputs["integration"]["github"]["getPullRequest"]; interface PullRequestDetailKey { projectId: string | null; @@ -37,7 +37,11 @@ export function usePullRequestDetail({ queryFn: async () => { if (!hostUrl || !projectId || prNumber === null) return null; const client = getHostServiceClientByUrl(hostUrl); - return client.pullRequests.getContent.query({ projectId, prNumber }); + const content = await client.pullRequests.getContent.query({ + projectId, + prNumber, + }); + return fromHostPullRequestContent(content); }, enabled: enabled && !!hostUrl && !!projectId && prNumber !== null, staleTime: 30_000, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/fromHostPullRequestContent.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/fromHostPullRequestContent.ts new file mode 100644 index 00000000000..f8866d9ad8e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/fromHostPullRequestContent.ts @@ -0,0 +1,44 @@ +import type { AppRouter as HostServiceAppRouter } from "@superset/host-service"; +import type { inferRouterOutputs } from "@trpc/server"; +import { pullRequestRefFromUrl } from "renderer/lib/github/pullRequestRef"; +import type { PullRequestDetail } from "../../hooks/usePullRequestDetail"; + +type HostPullRequestContent = + inferRouterOutputs["pullRequests"]["getContent"]; + +/** + * The host's `gh pr view` output in the shared shape. A host is the source + * whenever it has the repository checked out: it reads as the person, so it + * needs no GitHub App, which most organizations never install. + */ +export function fromHostPullRequestContent( + content: HostPullRequestContent, +): PullRequestDetail { + return { + repoFullName: pullRequestRefFromUrl(content.url)?.repoFullName ?? "", + number: content.number, + url: content.url, + title: content.title, + body: content.body, + state: + content.state === "merged" || content.state === "closed" + ? content.state + : "open", + isDraft: content.isDraft, + author: content.author ? { login: content.author, avatarUrl: null } : null, + head: { + ref: content.branch, + // The host reports only the fork's owner, never its name, so a + // cross-repository head is unknown here rather than half-named. + repoFullName: content.isCrossRepository + ? null + : (pullRequestRefFromUrl(content.url)?.repoFullName ?? null), + }, + base: { ref: content.baseBranch }, + reviewDecision: null, + checksStatus: content.checksStatus, + checks: content.checks, + createdAt: content.createdAt ?? "", + updatedAt: content.updatedAt ?? "", + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/index.ts new file mode 100644 index 00000000000..af9f8ca7057 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pull-requests/utils/fromHostPullRequestContent/index.ts @@ -0,0 +1 @@ +export { fromHostPullRequestContent } from "./fromHostPullRequestContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/ChangesControl.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/ChangesControl.tsx index b69e1d3feab..d7071a62c3f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/ChangesControl.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/ChangesControl.tsx @@ -2,6 +2,7 @@ import { useLingui } from "@lingui/react/macro"; import { cn } from "@superset/ui/utils"; import { GitCompareArrows } from "lucide-react"; import { memo, useMemo } from "react"; +import type { PullRequestRef } from "renderer/lib/github/pullRequestRef"; import { useWorkspaceGitStatus } from "../../providers/WorkspaceGitStatusProvider"; import { changesPillStats } from "./changesPillStats"; import { PRStatusGroup } from "./components/PRStatusGroup"; @@ -15,7 +16,7 @@ interface ChangesControlProps { /** Close the visible Changes pane, or open/focus one when none shows. */ onToggleChanges: () => void; /** Open or focus the pane showing the linked PR's summary. */ - onOpenPullRequest: (prNumber: number) => void; + onOpenPullRequest: (ref: PullRequestRef) => void; } /** diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/PRStatusGroup/PRStatusGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/PRStatusGroup/PRStatusGroup.tsx index 7610b5e563b..317c5f39764 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/PRStatusGroup/PRStatusGroup.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/PRStatusGroup/PRStatusGroup.tsx @@ -23,6 +23,10 @@ import { VscGitPullRequest, VscLoading, } from "react-icons/vsc"; +import { + type PullRequestRef, + pullRequestRefFromUrl, +} from "renderer/lib/github/pullRequestRef"; import { computeChecksRollup } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/computeChecksStatus"; import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { PRIcon, type PRState } from "renderer/screens/main/components/PRIcon"; @@ -44,7 +48,7 @@ interface PRStatusGroupProps { */ onToggleChanges?: () => void; /** Opens the PR's summary pane in the workspace (the menu's "Open pull request"). */ - onOpenPullRequest: (prNumber: number) => void; + onOpenPullRequest: (ref: PullRequestRef) => void; } /** @@ -341,7 +345,11 @@ export function PRStatusGroup({ {!isSession && ( onOpenPullRequest(pr.number)} + onClick={() => { + const ref = pullRequestRefFromUrl(pr.url); + if (ref) onOpenPullRequest(ref); + else window.open(pr.url, "_blank"); + }} > Open pull request diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/ShipControl/ShipControl.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/ShipControl/ShipControl.tsx index cea4d0b1eb3..3a376f21c3e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/ShipControl/ShipControl.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/ChangesControl/components/ShipControl/ShipControl.tsx @@ -21,6 +21,7 @@ import { VscLoading, VscRepoPush, } from "react-icons/vsc"; +import { pullRequestRefFromUrl } from "renderer/lib/github/pullRequestRef"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { usePullRequestPaneIntent } from "renderer/stores/pull-request-pane-intent"; @@ -257,9 +258,14 @@ export function ShipControl({ // workspaces by the time they click. A workspace-scoped intent // plus navigation lands the pane in the right store either way. onClick: () => { + const ref = pullRequestRefFromUrl(created.url); + if (!ref) { + window.open(created.url, "_blank"); + return; + } usePullRequestPaneIntent.getState().request({ workspaceId, - prNumber: created.number, + ...ref, }); void navigateToV2Workspace(workspaceId, navigate); }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx index 19df7f85f3a..fa5ac1b3eed 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -3,6 +3,7 @@ import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { type ReactNode, useEffect, useRef, useState } from "react"; import { LuFile } from "react-icons/lu"; +import type { PullRequestRef } from "renderer/lib/github/pullRequestRef"; import { useWorkspaceGitStatus } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/providers/WorkspaceGitStatusProvider"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { @@ -48,7 +49,7 @@ interface WorkspaceSidebarProps { ) => void; onOpenComment?: (comment: CommentPaneData) => void; /** Opens the linked PR's summary pane; the Review tab's title falls back to GitHub without it. */ - onOpenPullRequest?: (prNumber: number) => void; + onOpenPullRequest?: (ref: PullRequestRef) => void; onSearch?: () => void; selectedFilePath?: string; /** The diff pane's current file, highlighted in the Changes tab. */ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/components/PRHeader/PRHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/components/PRHeader/PRHeader.tsx index 0005e7a18e4..6e7f8a86c2f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/components/PRHeader/PRHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/components/PRHeader/PRHeader.tsx @@ -2,6 +2,10 @@ import { msg } from "@lingui/core/macro"; import { i18n } from "@superset/i18n"; import { cn } from "@superset/ui/utils"; import { LuArrowRight, LuArrowUpRight } from "react-icons/lu"; +import { + type PullRequestRef, + pullRequestRefFromUrl, +} from "renderer/lib/github/pullRequestRef"; import { PRIcon } from "renderer/screens/main/components/PRIcon"; import type { NormalizedPR } from "../../types"; @@ -33,7 +37,7 @@ interface PRHeaderProps { * Opens the PR's summary pane in the workspace. Without it the title is * a plain GitHub link, for hosts that have no pane store to open into. */ - onOpenPullRequest?: (prNumber: number) => void; + onOpenPullRequest?: (ref: PullRequestRef) => void; } const titleClass = @@ -58,7 +62,11 @@ export function PRHeader({ pr, onOpenPullRequest }: PRHeaderProps) { {onOpenPullRequest ? ( - - - Open in Pull Requests - - + {projectId && ( + + + + + + Open in Pull Requests + + + )} ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/index.ts new file mode 100644 index 00000000000..4a7ded2addb --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/index.ts @@ -0,0 +1 @@ +export { usePullRequestPaneDetail } from "./usePullRequestPaneDetail"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/usePullRequestPaneDetail.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/usePullRequestPaneDetail.ts new file mode 100644 index 00000000000..299827bdcbd --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneDetail/usePullRequestPaneDetail.ts @@ -0,0 +1,48 @@ +import { useHostProjects } from "renderer/hooks/host-projects/useHostProjects"; +import { useActiveOrganizationId } from "renderer/hooks/useActiveOrganizationId"; +import { cloudTrpc } from "renderer/lib/cloud-trpc"; +import type { PullRequestRef } from "renderer/lib/github/pullRequestRef"; +import { usePullRequestDetail } from "renderer/routes/_authenticated/_dashboard/pull-requests/hooks/usePullRequestDetail"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; + +/** + * A pull request's content by its own identity, from whichever source can + * answer: the workspace's host when its project is the PR's repository (the + * person's own `gh`, no GitHub App required), else the API with the + * organization's App installation, which is what a closed cloud box or a PR + * in a repository nobody has checked out needs. + */ +export function usePullRequestPaneDetail(ref: PullRequestRef) { + const { workspace, hostUrl } = useWorkspace(); + const organizationId = useActiveOrganizationId(); + const { projects, isReady: projectsReady } = useHostProjects(); + const project = projects.find( + (candidate) => candidate.id === workspace.projectId, + ); + const hostHasRepo = + !!project?.repoOwner && + !!project.repoName && + `${project.repoOwner}/${project.repoName}`.toLowerCase() === + ref.repoFullName.toLowerCase(); + + const fromHost = usePullRequestDetail({ + projectId: hostHasRepo ? (workspace.projectId ?? null) : null, + hostUrl: hostHasRepo ? hostUrl : null, + prNumber: ref.number, + enabled: hostHasRepo, + }); + const fromApi = cloudTrpc.integration.github.getPullRequest.useQuery( + { + organizationId: organizationId ?? "", + repoFullName: ref.repoFullName, + number: ref.number, + }, + { + // Until the projects have answered, which path applies is unknown. + enabled: projectsReady && !hostHasRepo && organizationId !== null, + staleTime: 30_000, + refetchOnWindowFocus: true, + }, + ); + return hostHasRepo ? fromHost : fromApi; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/index.ts deleted file mode 100644 index c2dfbf8676b..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { usePullRequestPaneProject } from "./usePullRequestPaneProject"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/usePullRequestPaneProject.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/usePullRequestPaneProject.ts deleted file mode 100644 index 4e77f2d8d80..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/PullRequestPane/hooks/usePullRequestPaneProject/usePullRequestPaneProject.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl"; -import { useProjectHost } from "renderer/routes/_authenticated/_dashboard/hooks/useProjectHost"; -import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; - -export function usePullRequestPaneProject(explicitProjectId?: string) { - const { workspace, hostUrl: workspaceHostUrl } = useWorkspace(); - const projectId = explicitProjectId ?? workspace.projectId; - const isWorkspaceProject = projectId === workspace.projectId; - const target = useProjectHost(isWorkspaceProject ? null : projectId); - const targetHostUrl = useHostUrl(target.hostId ?? undefined); - return { - workspace, - projectId, - isWorkspaceProject, - hostId: isWorkspaceProject ? workspace.hostId : target.hostId, - hostUrl: isWorkspaceProject ? workspaceHostUrl : targetHostUrl, - isReady: isWorkspaceProject || target.isReady, - hasProject: isWorkspaceProject - ? workspace.type !== "session" - : !!target.project, - }; -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx index 0ee0b47caa7..6988adb852c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx @@ -784,7 +784,7 @@ export function usePaneRegistry({ getIcon: () => , getTitle: (pane) => { const data = pane.data as PullRequestPaneData; - return t({ message: `Pull request #${data.prNumber}` }); + return t({ message: `Pull request #${data.number}` }); }, renderPane: (ctx: RendererContext) => ( void; + openPullRequestPane: (ref: PullRequestRef) => void; }): void { const pendingIntent = usePullRequestPaneIntent((s) => s.intent); @@ -22,6 +23,9 @@ export function usePullRequestPaneIntentOpener({ if (pendingIntent?.workspaceId !== workspaceId) return; const intent = usePullRequestPaneIntent.getState().consume(workspaceId); if (!intent) return; - openPullRequestPane(intent.prNumber); + openPullRequestPane({ + repoFullName: intent.repoFullName, + number: intent.number, + }); }, [pendingIntent, workspaceId, isLayoutReady, openPullRequestPane]); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts index b99948675b7..cae9bba6f46 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts @@ -1,6 +1,7 @@ import type { WorkspaceStore } from "@superset/panes"; import { useCallback } from "react"; import type { V2UserPreferencesApi } from "renderer/hooks/useV2UserPreferences"; +import type { PullRequestRef } from "renderer/lib/github/pullRequestRef"; import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { V2TerminalPresetRow } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; @@ -63,7 +64,7 @@ export function useWorkspacePaneOpeners({ openCommentPane: (comment: CommentPaneData) => void; openPagePane: (page: PagePaneData) => void; /** Focus or open the pane showing the workspace's linked PR summary. */ - openPullRequestPane: (prNumber: number) => void; + openPullRequestPane: (ref: PullRequestRef) => void; } { const openDiffPane = useCallback( ( @@ -250,8 +251,8 @@ export function useWorkspacePaneOpeners({ ); const openPullRequestPane = useCallback( - (prNumber: number) => { - openPullRequestPaneInStore(store, prNumber); + (ref: PullRequestRef) => { + openPullRequestPaneInStore(store, ref); }, [store], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts index bb434312d24..17e337f70cb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts @@ -66,8 +66,8 @@ export interface CommentPaneData { } export interface PullRequestPaneData { - prNumber: number; - projectId?: string; + repoFullName: string; + number: number; } export interface PagePaneData { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.test.ts index 63f7940284e..c971cc2408c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.test.ts @@ -9,6 +9,8 @@ import type { StoreApi } from "zustand/vanilla"; import type { PaneViewerData, PullRequestPaneData } from "../../types"; import { openPullRequestPaneInStore } from "./openPullRequestPaneInStore"; +const REPO = "superset-sh/superset"; + function paneLayout(paneId: string): LayoutNode { return { type: "pane", paneId }; } @@ -16,7 +18,7 @@ function paneLayout(paneId: string): LayoutNode { function workspaceState(existing?: { tabId: string; paneId: string; - prNumber: number; + data: PullRequestPaneData; }): WorkspaceState { return { version: 1, @@ -46,9 +48,7 @@ function workspaceState(existing?: { [existing.paneId]: { id: existing.paneId, kind: "pull-request", - data: { - prNumber: existing.prNumber, - } as PaneViewerData, + data: existing.data as PaneViewerData, }, }, }, @@ -66,19 +66,25 @@ function findPullRequestPanes(store: StoreApi>) { ); } +function paneData(store: StoreApi>) { + return findPullRequestPanes(store)[0]?.pane.data as + | PullRequestPaneData + | undefined; +} + describe("openPullRequestPaneInStore", () => { it("splits the active pane when no pull-request pane exists", () => { const store = createWorkspaceStore({ initialState: workspaceState(), }); - openPullRequestPaneInStore(store, 42); + openPullRequestPaneInStore(store, { repoFullName: REPO, number: 42 }); const state = store.getState(); expect(state.tabs).toHaveLength(1); const panes = findPullRequestPanes(store); expect(panes).toHaveLength(1); - expect((panes[0]?.pane.data as PullRequestPaneData).prNumber).toBe(42); + expect(paneData(store)).toEqual({ repoFullName: REPO, number: 42 }); expect(state.tabs[0]?.activePaneId).toBe(panes[0]?.pane.id); expect(state.tabs[0]?.layout.type).toBe("split"); }); @@ -88,12 +94,12 @@ describe("openPullRequestPaneInStore", () => { initialState: workspaceState({ tabId: "tab-2", paneId: "pr-pane", - prNumber: 42, + data: { repoFullName: REPO, number: 42 }, }), }); const before = store.getState().tabs[1]?.panes["pr-pane"]?.data; - openPullRequestPaneInStore(store, 42); + openPullRequestPaneInStore(store, { repoFullName: REPO, number: 42 }); const state = store.getState(); expect(state.activeTabId).toBe("tab-2"); @@ -107,15 +113,35 @@ describe("openPullRequestPaneInStore", () => { initialState: workspaceState({ tabId: "tab-2", paneId: "pr-pane", - prNumber: 42, + data: { repoFullName: REPO, number: 42 }, }), }); - openPullRequestPaneInStore(store, 43); + openPullRequestPaneInStore(store, { repoFullName: REPO, number: 43 }); - const panes = findPullRequestPanes(store); - expect(panes).toHaveLength(1); - expect((panes[0]?.pane.data as PullRequestPaneData).prNumber).toBe(43); + expect(findPullRequestPanes(store)).toHaveLength(1); + expect(paneData(store)).toEqual({ repoFullName: REPO, number: 43 }); expect(store.getState().activeTabId).toBe("tab-2"); }); + + it("retargets when the same number belongs to another repository", () => { + const store = createWorkspaceStore({ + initialState: workspaceState({ + tabId: "tab-2", + paneId: "pr-pane", + data: { repoFullName: REPO, number: 42 }, + }), + }); + + openPullRequestPaneInStore(store, { + repoFullName: "superset-sh/docs", + number: 42, + }); + + expect(findPullRequestPanes(store)).toHaveLength(1); + expect(paneData(store)).toEqual({ + repoFullName: "superset-sh/docs", + number: 42, + }); + }); }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.ts index 89100fbbbbe..9ffcd9b1a11 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openPullRequestPaneInStore/openPullRequestPaneInStore.ts @@ -1,25 +1,31 @@ import type { WorkspaceStore } from "@superset/panes"; +import { + isSamePullRequest, + type PullRequestRef, +} from "renderer/lib/github/pullRequestRef"; import type { StoreApi } from "zustand/vanilla"; import type { PaneViewerData, PullRequestPaneData } from "../../types"; /** - * Focus the workspace's pull-request pane, retargeting it to `prNumber` - * when one already exists (a workspace links one PR, so one pane is - * enough), or opening a fresh one beside the active pane — from the - * Changes pane that lands the PR summary next to the diff. + * Focus the workspace's pull-request pane, retargeting it to `ref` when one + * already exists (a workspace links one PR, so one pane is enough), or + * opening a fresh one beside the active pane — from the Changes pane that + * lands the PR summary next to the diff. */ export function openPullRequestPaneInStore( store: StoreApi>, - prNumber: number, + ref: PullRequestRef, ): void { const state = store.getState(); - const data: PullRequestPaneData = { prNumber }; + const data: PullRequestPaneData = { + repoFullName: ref.repoFullName, + number: ref.number, + }; for (const tab of state.tabs) { for (const pane of Object.values(tab.panes)) { if (pane.kind !== "pull-request") continue; - if ((pane.data as PullRequestPaneData).projectId) continue; - if ((pane.data as PullRequestPaneData).prNumber !== prNumber) { + if (!isSamePullRequest(pane.data as PullRequestPaneData, data)) { state.setPaneData({ paneId: pane.id, data: data as PaneViewerData }); } state.setActiveTab(tab.id); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx index 971ea74b39b..167cefa28cf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx @@ -9,6 +9,7 @@ import { CgLaptop } from "react-icons/cg"; import { LuLaptop, LuMonitor } from "react-icons/lu"; import { WorkspaceNameMarquee } from "renderer/components/WorkspaceNameMarquee"; import { useFocusVisible } from "renderer/hooks/useFocusVisible"; +import { pullRequestRefFromUrl } from "renderer/lib/github/pullRequestRef"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { V2WorkspaceContextMenu } from "renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspaceContextMenu"; import { WorkspaceStateGlyph } from "renderer/routes/_authenticated/_dashboard/v2-workspaces/components/WorkspaceStateGlyph"; @@ -186,9 +187,14 @@ export const V2WorkspaceRow = memo(function V2WorkspaceRow({ event.stopPropagation(); if (!workspace.pr) return; // Opens the PR pane inside the workspace instead of GitHub. + const ref = pullRequestRefFromUrl(workspace.pr.url); + if (!ref) { + window.open(workspace.pr.url, "_blank"); + return; + } usePullRequestPaneIntent.getState().request({ workspaceId: workspace.id, - prNumber: workspace.pr.prNumber, + ...ref, }); void navigateToV2Workspace(workspace.id, navigate); }} diff --git a/apps/desktop/src/renderer/stores/pull-request-pane-intent.ts b/apps/desktop/src/renderer/stores/pull-request-pane-intent.ts index b87b170f328..0541b919ca0 100644 --- a/apps/desktop/src/renderer/stores/pull-request-pane-intent.ts +++ b/apps/desktop/src/renderer/stores/pull-request-pane-intent.ts @@ -8,7 +8,8 @@ import { create } from "zustand"; */ export interface PullRequestPaneIntent { workspaceId: string; - prNumber: number; + repoFullName: string; + number: number; } interface PullRequestPaneIntentState { diff --git a/packages/i18n/locales/cs/messages.po b/packages/i18n/locales/cs/messages.po index 24f7697dc76..35eaf059e18 100644 --- a/packages/i18n/locales/cs/messages.po +++ b/packages/i18n/locales/cs/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "„{0}“ bude na GitHubu označen jako uzavřený. Kdykoli ho odsud můžete znovu otevřít." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "„{0}“ bude sloučen do {1} pomocí {2}. Kontroly zatím neprošly — tímto je obejdete. Odsud to nelze vrátit zpět." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "„{0}“ bude sloučen do {1} pomocí {2}. Odsud to nelze vrátit zpět." @@ -371,6 +371,10 @@ msgstr "{0} nainstalováno" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} je už přiřazeno k „{1}“." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} není repozitář, ve kterém je nainstalována aplikace GitHub App" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} je offline" @@ -11570,7 +11574,7 @@ msgstr "Zveřejňujete jako <0>{handle}. Jen počty tokenů a názvy modelů msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/de/messages.po b/packages/i18n/locales/de/messages.po index 353cb2183b2..a0447416b52 100644 --- a/packages/i18n/locales/de/messages.po +++ b/packages/i18n/locales/de/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" wird auf GitHub als geschlossen markiert. Du kannst ihn hier jederzeit wieder öffnen." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" wird per {2} in {1} zusammengeführt. Die Prüfungen sind noch nicht bestanden — das übergeht sie. Das lässt sich von hier aus nicht rückgängig machen." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" wird per {2} in {1} zusammengeführt. Das lässt sich von hier aus nicht rückgängig machen." @@ -371,6 +371,10 @@ msgstr "{0} installiert" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} ist bereits \"{1}\" zugewiesen." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} ist kein Repository, in dem die GitHub App installiert ist" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} ist offline" @@ -11570,7 +11574,7 @@ msgstr "Veröffentlichung als <0>{handle}. Nur Token-Zahlen und Modellnamen, msgid "Pull request" msgstr "Pull Request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull Request #{0}" diff --git a/packages/i18n/locales/en/messages.po b/packages/i18n/locales/en/messages.po index cb48b24653a..71415cb81cc 100644 --- a/packages/i18n/locales/en/messages.po +++ b/packages/i18n/locales/en/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" will be marked closed on GitHub. You can reopen it from here at any time." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." @@ -371,6 +371,10 @@ msgstr "{0} installed" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} is already assigned to \"{1}\"." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} is not a repository the GitHub App is installed on" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} is offline" @@ -11570,7 +11574,7 @@ msgstr "Publishing as <0>{handle}. Token counts and model names only, no rep msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/es/messages.po b/packages/i18n/locales/es/messages.po index be40236a629..7c3fd165ffd 100644 --- a/packages/i18n/locales/es/messages.po +++ b/packages/i18n/locales/es/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" se marcará como cerrada en GitHub. Puedes reabrirla desde aquí en cualquier momento." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" se fusionará en {1} mediante {2}. Las comprobaciones aún no se han superado y esto las ignora. No se puede deshacer desde aquí." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" se fusionará en {1} mediante {2}. No se puede deshacer desde aquí." @@ -371,6 +371,10 @@ msgstr "{0} instalado" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} ya está asignado a \"{1}\"." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} no es un repositorio en el que esté instalada la GitHub App" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} está sin conexión" @@ -11570,7 +11574,7 @@ msgstr "Publicando como <0>{handle}. Solo recuentos de tokens y nombres de m msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/fr/messages.po b/packages/i18n/locales/fr/messages.po index ce5120e287e..313a181507e 100644 --- a/packages/i18n/locales/fr/messages.po +++ b/packages/i18n/locales/fr/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" sera marquée comme fermée sur GitHub. Vous pourrez la rouvrir d'ici à tout moment." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" sera fusionnée dans {1} via {2}. Les vérifications n'ont pas encore réussi — cette action passe outre. Elle est irréversible depuis cet endroit." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" sera fusionnée dans {1} via {2}. Cette action est irréversible depuis cet endroit." @@ -371,6 +371,10 @@ msgstr "{0} installé" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} est déjà attribué à « {1} »." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} n’est pas un dépôt sur lequel la GitHub App est installée" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} est hors ligne" @@ -11570,7 +11574,7 @@ msgstr "Publication sous <0>{handle}. Uniquement le nombre de tokens et les msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/id/messages.po b/packages/i18n/locales/id/messages.po index fbc03fa0d02..c990ac109ef 100644 --- a/packages/i18n/locales/id/messages.po +++ b/packages/i18n/locales/id/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" akan ditandai ditutup di GitHub. Anda bisa membukanya lagi dari sini kapan saja." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" akan di-merge ke {1} lewat {2}. Check-nya belum lulus — ini menimpanya. Ini tidak bisa dibatalkan dari sini." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" akan di-merge ke {1} lewat {2}. Ini tidak bisa dibatalkan dari sini." @@ -371,6 +371,10 @@ msgstr "{0} terpasang" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} sudah ditetapkan untuk \"{1}\"." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} bukan repositori tempat GitHub App terpasang" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} sedang offline" @@ -11570,7 +11574,7 @@ msgstr "Dipublikasikan sebagai <0>{handle}. Hanya jumlah token dan nama mode msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/it/messages.po b/packages/i18n/locales/it/messages.po index 242bb90e23c..d5ccc8d704c 100644 --- a/packages/i18n/locales/it/messages.po +++ b/packages/i18n/locales/it/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" verrà contrassegnata come chiusa su GitHub. Puoi riaprirla da qui in qualsiasi momento." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" verrà unita in {1} tramite {2}. I controlli non sono ancora passati — questa operazione li ignora. Non può essere annullata da qui." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" verrà unita in {1} tramite {2}. L'operazione non può essere annullata da qui." @@ -371,6 +371,10 @@ msgstr "{0} installato" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} è già assegnata a \"{1}\"." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} non è un repository in cui è installata la GitHub App" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} è offline" @@ -11570,7 +11574,7 @@ msgstr "Pubblichi come <0>{handle}. Solo conteggi di token e nomi dei modell msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/ja/messages.po b/packages/i18n/locales/ja/messages.po index 1dcf71de9e8..a0bc7b3c3ad 100644 --- a/packages/i18n/locales/ja/messages.po +++ b/packages/i18n/locales/ja/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "「{0}」はGitHub上でクローズ済みになります。ここからいつでも再オープンできます。" #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "「{0}」は{2}で{1}にマージされます。チェックはまだ通っていません — この操作はそれを無視します。ここからは元に戻せません。" #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "「{0}」は{2}で{1}にマージされます。ここからは元に戻せません。" @@ -371,6 +371,10 @@ msgstr "{0}をインストールしました" msgid "{0} is already assigned to \"{1}\"." msgstr "{0}はすでに「{1}」に割り当てられています。" +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} は GitHub App がインストールされているリポジトリではありません" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0}はオフラインです" @@ -11570,7 +11574,7 @@ msgstr "<0>{handle}として公開中。トークン数とモデル名のみ msgid "Pull request" msgstr "プルリクエスト" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "プルリクエスト #{0}" diff --git a/packages/i18n/locales/ko/messages.po b/packages/i18n/locales/ko/messages.po index 0b238f748bf..40f109363ac 100644 --- a/packages/i18n/locales/ko/messages.po +++ b/packages/i18n/locales/ko/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\"이(가) GitHub에서 닫힘으로 표시됩니다. 언제든 여기에서 다시 열 수 있습니다." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\"이(가) {2} 방식으로 {1}에 머지됩니다. 검사가 아직 통과하지 않았지만 이를 무시하고 진행합니다. 여기에서는 되돌릴 수 없습니다." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\"이(가) {2} 방식으로 {1}에 머지됩니다. 여기에서는 되돌릴 수 없습니다." @@ -371,6 +371,10 @@ msgstr "{0} 설치됨" msgid "{0} is already assigned to \"{1}\"." msgstr "{0}은(는) 이미 \"{1}\"에 지정되어 있습니다." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0}은(는) GitHub App이 설치된 리포지토리가 아닙니다" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0}이(가) 오프라인입니다" @@ -11570,7 +11574,7 @@ msgstr "<0>{handle}(으)로 게시 중입니다. 토큰 수와 모델 이름 msgid "Pull request" msgstr "풀 리퀘스트" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "풀 리퀘스트 #{0}" diff --git a/packages/i18n/locales/nl/messages.po b/packages/i18n/locales/nl/messages.po index 426cc542034..6491cb413e0 100644 --- a/packages/i18n/locales/nl/messages.po +++ b/packages/i18n/locales/nl/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" wordt op GitHub als gesloten gemarkeerd. Je kunt hem hier op elk moment heropenen." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" wordt via {2} in {1} gemerged. De checks zijn nog niet geslaagd — dit overschrijft ze. Dit kan hier niet ongedaan worden gemaakt." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" wordt via {2} in {1} gemerged. Dit kan hier niet ongedaan worden gemaakt." @@ -371,6 +371,10 @@ msgstr "{0} geïnstalleerd" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} is al toegewezen aan \"{1}\"." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} is geen repository waarop de GitHub App is geïnstalleerd" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} is offline" @@ -11570,7 +11574,7 @@ msgstr "Gepubliceerd als <0>{handle}. Alleen tokenaantallen en modelnamen, g msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/pl/messages.po b/packages/i18n/locales/pl/messages.po index c96fb725e5f..fd8ff28b7c9 100644 --- a/packages/i18n/locales/pl/messages.po +++ b/packages/i18n/locales/pl/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "„{0}” zostanie oznaczony jako zamknięty na GitHub. Można go stąd w każdej chwili otworzyć ponownie." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "„{0}” zostanie scalony do {1} przez {2}. Kontrole jeszcze nie przeszły — to je pomija. Nie można tego stąd cofnąć." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "„{0}” zostanie scalony do {1} przez {2}. Nie można tego stąd cofnąć." @@ -371,6 +371,10 @@ msgstr "{0} zainstalowana" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} jest już przypisane do „{1}”." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} nie jest repozytorium, w którym zainstalowano aplikację GitHub App" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} jest offline" @@ -11570,7 +11574,7 @@ msgstr "Publikujesz jako <0>{handle}. Tylko liczby tokenów i nazwy modeli, msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/pt-BR/messages.po b/packages/i18n/locales/pt-BR/messages.po index c50346d63d4..3733e211c01 100644 --- a/packages/i18n/locales/pt-BR/messages.po +++ b/packages/i18n/locales/pt-BR/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" será marcado como fechado no GitHub. Você pode reabri-lo daqui quando quiser." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" será mesclado em {1} via {2}. As verificações ainda não passaram — isso as ignora. Não dá para desfazer daqui." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" será mesclado em {1} via {2}. Não dá para desfazer daqui." @@ -371,6 +371,10 @@ msgstr "{0} instalado" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} já está atribuído a \"{1}\"." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} não é um repositório em que o GitHub App está instalado" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} está offline" @@ -11570,7 +11574,7 @@ msgstr "Publicando como <0>{handle}. Só contagens de tokens e nomes de mode msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/ru/messages.po b/packages/i18n/locales/ru/messages.po index 570a174bd13..b8e39d6046e 100644 --- a/packages/i18n/locales/ru/messages.po +++ b/packages/i18n/locales/ru/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "«{0}» будет отмечен как закрытый на GitHub. Открыть заново можно отсюда в любой момент." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "«{0}» будет объединён в {1} через {2}. Проверки ещё не пройдены — это их переопределит. Отменить отсюда нельзя." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "«{0}» будет объединён в {1} через {2}. Отменить отсюда нельзя." @@ -371,6 +371,10 @@ msgstr "{0} установлен" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} уже назначено на «{1}»." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} — не репозиторий, в котором установлено GitHub App" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} офлайн" @@ -11570,7 +11574,7 @@ msgstr "Публикуется как <0>{handle}. Только количе msgid "Pull request" msgstr "Пул-реквест" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Пул-реквест #{0}" diff --git a/packages/i18n/locales/tr/messages.po b/packages/i18n/locales/tr/messages.po index 1b67743d081..70d737e1238 100644 --- a/packages/i18n/locales/tr/messages.po +++ b/packages/i18n/locales/tr/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" GitHub'da kapalı olarak işaretlenecek. Buradan istediğiniz zaman yeniden açabilirsiniz." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\", {2} ile {1} dalına birleştirilecek. Kontroller henüz geçmedi — bu işlem onları geçersiz kılar. Buradan geri alınamaz." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\", {2} ile {1} dalına birleştirilecek. Buradan geri alınamaz." @@ -371,6 +371,10 @@ msgstr "{0} kuruldu" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} zaten \"{1}\" için atanmış." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0}, GitHub App’in yüklü olduğu bir depo değil" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} çevrimdışı" @@ -11570,7 +11574,7 @@ msgstr "<0>{handle} adıyla yayımlanıyor. Yalnızca token sayıları ve mo msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/vi/messages.po b/packages/i18n/locales/vi/messages.po index 973b5c2edee..9ea0b09faa2 100644 --- a/packages/i18n/locales/vi/messages.po +++ b/packages/i18n/locales/vi/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "\"{0}\" sẽ được đánh dấu là đã đóng trên GitHub. Bạn có thể mở lại từ đây bất cứ lúc nào." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "\"{0}\" sẽ được merge vào {1} bằng {2}. Các check chưa qua — thao tác này bỏ qua chúng. Không thể hoàn tác từ đây." #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "\"{0}\" sẽ được merge vào {1} bằng {2}. Không thể hoàn tác từ đây." @@ -371,6 +371,10 @@ msgstr "Đã cài {0}" msgid "{0} is already assigned to \"{1}\"." msgstr "{0} đã được gán cho \"{1}\"." +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} không phải là kho lưu trữ đã cài đặt GitHub App" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0} đang ngoại tuyến" @@ -11570,7 +11574,7 @@ msgstr "Đang đăng với tên <0>{handle}. Chỉ số token và tên mô h msgid "Pull request" msgstr "Pull request" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "Pull request #{0}" diff --git a/packages/i18n/locales/zh-CN/messages.po b/packages/i18n/locales/zh-CN/messages.po index fd70363b73e..dd194102cc5 100644 --- a/packages/i18n/locales/zh-CN/messages.po +++ b/packages/i18n/locales/zh-CN/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "“{0}”会在GitHub上被标记为关闭。你随时可以在这里重新打开它。" #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "“{0}”将通过{2}合并到{1}。检查还没通过,此操作会绕过它们。在这里无法撤销。" #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "“{0}”将通过{2}合并到{1}。在这里无法撤销。" @@ -371,6 +371,10 @@ msgstr "{0}已安装" msgid "{0} is already assigned to \"{1}\"." msgstr "{0}已分配给“{1}”。" +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} 不是已安装 GitHub App 的仓库" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0}已离线" @@ -11570,7 +11574,7 @@ msgstr "以<0>{handle}的身份发布。仅包含token数量和模型名称 msgid "Pull request" msgstr "拉取请求" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "拉取请求#{0}" diff --git a/packages/i18n/locales/zh-TW/messages.po b/packages/i18n/locales/zh-TW/messages.po index 7f50038d99b..ec4af9c1bf7 100644 --- a/packages/i18n/locales/zh-TW/messages.po +++ b/packages/i18n/locales/zh-TW/messages.po @@ -47,13 +47,13 @@ msgid "\"{0}\" will be marked closed on GitHub. You can reopen it from here at a msgstr "「{0}」會在GitHub上被標記為關閉。你隨時可以在這裡重新開啟它。" #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. Checks haven't passed yet — this overrides them. This can't be undone from here." msgstr "「{0}」將通過{2}合併到{1}。檢查還沒通過,此操作會繞過它們。在這裡無法復原。" #. placeholder {0}: data.title -#. placeholder {1}: data.baseBranch +#. placeholder {1}: data.base.ref #. placeholder {2}: mergeMethodLabels[pendingAction.method].toLowerCase() msgid "\"{0}\" will be merged into {1} via {2}. This can't be undone from here." msgstr "「{0}」將通過{2}合併到{1}。在這裡無法復原。" @@ -371,6 +371,10 @@ msgstr "{0}已安裝" msgid "{0} is already assigned to \"{1}\"." msgstr "{0}已分配給「{1}」。" +#. placeholder {0}: params?.repoFullName +msgid "{0} is not a repository the GitHub App is installed on" +msgstr "{0} 不是已安裝 GitHub App 的存放庫" + #. placeholder {0}: host.name msgid "{0} is offline" msgstr "{0}已離線" @@ -11570,7 +11574,7 @@ msgstr "以<0>{handle}的身份釋出。僅包含token數量和模型名稱 msgid "Pull request" msgstr "拉取請求" -#. placeholder {0}: data.prNumber +#. placeholder {0}: data.number #. placeholder {0}: pullRequest.prNumber msgid "Pull request #{0}" msgstr "拉取請求#{0}" diff --git a/packages/i18n/src/server-errors.ts b/packages/i18n/src/server-errors.ts index 0fd05a79ec4..04744244e6d 100644 --- a/packages/i18n/src/server-errors.ts +++ b/packages/i18n/src/server-errors.ts @@ -381,6 +381,12 @@ export const serverErrorMessages: Record< message: "Only owners can delete projects", }), ), + "serverError.integration.repositoryNotInstalled": (params) => + i18n._( + msg({ + message: `${params?.repoFullName} is not a repository the GitHub App is installed on`, + }), + ), "serverError.integration.sentryRejectedTheToken": () => i18n._( msg({ diff --git a/packages/shared/package.json b/packages/shared/package.json index a9ebddb80fa..65aa601714f 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -84,6 +84,10 @@ "types": "./src/sandbox-contract.ts", "default": "./src/sandbox-contract.ts" }, + "./pull-request": { + "types": "./src/pull-request.ts", + "default": "./src/pull-request.ts" + }, "./sandbox-gate": { "types": "./src/sandbox-gate.ts", "default": "./src/sandbox-gate.ts" diff --git a/packages/shared/src/pull-request.ts b/packages/shared/src/pull-request.ts new file mode 100644 index 00000000000..52910945574 --- /dev/null +++ b/packages/shared/src/pull-request.ts @@ -0,0 +1,48 @@ +/** The one vocabulary every surface renders a pull request from. */ + +export type PullRequestState = "open" | "closed" | "merged"; +export type PullRequestCheckStatus = + | "success" + | "failure" + | "pending" + | "skipped" + | "cancelled"; +export type PullRequestChecksStatus = + | "success" + | "failure" + | "pending" + | "none"; +export type PullRequestReviewDecision = + | "approved" + | "changes_requested" + | "pending" + | null; + +export interface PullRequestCheck { + name: string; + status: PullRequestCheckStatus; + url: string | null; +} + +/** A pull request by its own identity, with what a detail view renders. */ +export interface PullRequestDetail { + repoFullName: string; + number: number; + url: string; + title: string; + body: string; + state: PullRequestState; + isDraft: boolean; + author: { login: string; avatarUrl: string | null } | null; + head: { + ref: string; + /** Null when the head repository is unknown or gone (a deleted fork). */ + repoFullName: string | null; + }; + base: { ref: string }; + reviewDecision: PullRequestReviewDecision; + checksStatus: PullRequestChecksStatus; + checks: PullRequestCheck[]; + createdAt: string; + updatedAt: string; +} diff --git a/packages/trpc/src/router/integration/github/github.ts b/packages/trpc/src/router/integration/github/github.ts index dccebb0be11..01eb9840c1d 100644 --- a/packages/trpc/src/router/integration/github/github.ts +++ b/packages/trpc/src/router/integration/github/github.ts @@ -9,8 +9,16 @@ import { Client } from "@upstash/qstash"; import { and, desc, eq, inArray, sql } from "drizzle-orm"; import { z } from "zod"; import { env } from "../../../env"; +import { installationOctokit } from "../../../lib/sandbox/clone-token"; import { protectedProcedure, userError } from "../../../trpc"; import { verifyOrgAdmin, verifyOrgMembership } from "../utils"; +import { + type PullRequestDetail, + toChecks, + toChecksStatus, + toPullRequestState, + toReviewDecision, +} from "./pull-request-shape"; import { listGithubRepositories } from "./trigger-options"; const qstash = new Client({ token: env.QSTASH_TOKEN }); @@ -292,17 +300,95 @@ export const githubRouter = { number: row.number, url: row.url, title: row.title, - state: row.state, + state: toPullRequestState(row.state, row.mergedAt), isDraft: row.isDraft, - reviewDecision: row.reviewDecision, - checksStatus: row.checksStatus, - checks: row.checks ?? [], - mergedAt: row.mergedAt, + reviewDecision: toReviewDecision(row.reviewDecision), + checksStatus: toChecksStatus(row.checksStatus), + checks: toChecks(row.checks), updatedAt: row.updatedAt, })), }; }), + /** + * One pull request by its own identity, repository and number, for a + * detail pane that has no host in the loop: the webhook row supplies + * everything but the description, which comes from GitHub with the + * installation's token. + */ + getPullRequest: protectedProcedure + .input( + z.object({ + organizationId: z.string().uuid(), + repoFullName: z.string().min(1), + number: z.number().int().positive(), + }), + ) + .query(async ({ ctx, input }): Promise => { + await verifyOrgMembership(ctx.session.user.id, input.organizationId); + const installation = await db.query.githubInstallations.findFirst({ + where: eq(githubInstallations.organizationId, input.organizationId), + }); + if (!installation) { + throw userError({ + code: "PRECONDITION_FAILED", + message: "GitHub installation not found", + i18nKey: "serverError.integration.githubInstallationNotFound", + }); + } + const repo = await db.query.githubRepositories.findFirst({ + where: and( + eq(githubRepositories.installationId, installation.id), + sql`lower(${githubRepositories.fullName}) = ${input.repoFullName.toLowerCase()}`, + ), + columns: { id: true, fullName: true }, + }); + if (!repo) { + throw userError({ + code: "NOT_FOUND", + message: `${input.repoFullName} is not a repository the GitHub App is installed on`, + i18nKey: "serverError.integration.repositoryNotInstalled", + params: { repoFullName: input.repoFullName }, + }); + } + const [owner, name] = repo.fullName.split("/"); + const [row, octokit] = await Promise.all([ + db.query.githubPullRequests.findFirst({ + where: and( + eq(githubPullRequests.repositoryId, repo.id), + eq(githubPullRequests.prNumber, input.number), + ), + }), + installationOctokit(installation.installationId), + ]); + const { data: pr } = await octokit.request( + "GET /repos/{owner}/{repo}/pulls/{pull_number}", + { owner: owner ?? "", repo: name ?? "", pull_number: input.number }, + ); + return { + repoFullName: repo.fullName, + number: pr.number, + url: pr.html_url, + title: pr.title, + body: pr.body ?? "", + state: toPullRequestState(pr.state, pr.merged_at), + isDraft: pr.draft ?? false, + author: pr.user + ? { login: pr.user.login, avatarUrl: pr.user.avatar_url ?? null } + : null, + head: { + ref: pr.head.ref, + repoFullName: pr.head.repo?.full_name ?? null, + }, + base: { ref: pr.base.ref }, + reviewDecision: toReviewDecision(row?.reviewDecision ?? null), + checksStatus: toChecksStatus(row?.checksStatus ?? "none"), + checks: toChecks(row?.checks ?? null), + createdAt: pr.created_at, + updatedAt: pr.updated_at, + }; + }), + getStats: protectedProcedure .input(z.object({ organizationId: z.string().uuid() })) .query(async ({ ctx, input }) => { diff --git a/packages/trpc/src/router/integration/github/pull-request-shape.ts b/packages/trpc/src/router/integration/github/pull-request-shape.ts new file mode 100644 index 00000000000..376f4582cf2 --- /dev/null +++ b/packages/trpc/src/router/integration/github/pull-request-shape.ts @@ -0,0 +1,72 @@ +/** Builds the shared pull-request vocabulary once from the webhook row. */ +import type { + PullRequestCheck, + PullRequestCheckStatus, + PullRequestChecksStatus, + PullRequestReviewDecision, + PullRequestState, +} from "@superset/shared/pull-request"; + +export type { PullRequestDetail } from "@superset/shared/pull-request"; + +type StoredCheck = { + name: string; + status: string; + conclusion: string | null; + detailsUrl?: string; +}; + +export function toPullRequestState( + state: string, + mergedAt: Date | string | null, +): PullRequestState { + if (state === "merged" || mergedAt) return "merged"; + return state === "closed" ? "closed" : "open"; +} + +export function toReviewDecision( + value: string | null, +): PullRequestReviewDecision { + switch (value) { + case "APPROVED": + return "approved"; + case "CHANGES_REQUESTED": + return "changes_requested"; + case "REVIEW_REQUIRED": + return "pending"; + default: + return null; + } +} + +export function toChecksStatus(value: string): PullRequestChecksStatus { + return value === "success" || value === "failure" || value === "pending" + ? value + : "none"; +} + +function toCheckStatus(check: StoredCheck): PullRequestCheckStatus { + if (check.status.toLowerCase() !== "completed") return "pending"; + switch (check.conclusion?.toLowerCase()) { + case "success": + return "success"; + case "skipped": + case "neutral": + return "skipped"; + case "cancelled": + return "cancelled"; + case null: + case undefined: + return "pending"; + default: + return "failure"; + } +} + +export function toChecks(checks: StoredCheck[] | null): PullRequestCheck[] { + return (checks ?? []).map((check) => ({ + name: check.name, + status: toCheckStatus(check), + url: check.detailsUrl ?? null, + })); +}