From e7aca945bd4329c641336b09642c40625e0c2cd6 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 6 Oct 2026 23:30:03 +0000 Subject: [PATCH 1/2] feat(web): add fast actions to linked pull requests --- .../pullRequest/PullRequestSpeedActions.tsx | 21 ++++++---- .../pullRequest/ThreadPullRequestsPanel.tsx | 40 +++++++++++++++++-- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestSpeedActions.tsx b/apps/web/src/components/pullRequest/PullRequestSpeedActions.tsx index ee1bc5dcc02c..319e73d20a31 100644 --- a/apps/web/src/components/pullRequest/PullRequestSpeedActions.tsx +++ b/apps/web/src/components/pullRequest/PullRequestSpeedActions.tsx @@ -11,13 +11,18 @@ import { usePullRequestDefaultMergeMethodResolver, } from "./usePullRequestActions"; -export interface PullRequestSpeedActionResult { - readonly entry: EnvironmentPullRequestEntry; +type PullRequestSpeedActionEntry = Pick< + EnvironmentPullRequestEntry, + "environmentId" | "projectId" | "host" | "repository" | "number" | "state" | "isDraft" +> & { readonly stack?: object | undefined }; + +export interface PullRequestSpeedActionResult { + readonly entry: Entry; readonly action: PullRequestAction; } /** No detail or stack reads until a merge is clicked, even on a long list. */ -export function PullRequestSpeedActions({ +export function PullRequestSpeedActions({ entry, visible, onActed, @@ -25,12 +30,12 @@ export function PullRequestSpeedActions({ sweeping = false, onCloseSweepStart, }: { - entry: EnvironmentPullRequestEntry; + entry: Entry; visible: boolean; - onActed: (result: PullRequestSpeedActionResult) => void; + onActed?: (result: PullRequestSpeedActionResult) => void; closing?: boolean; sweeping?: boolean; - onCloseSweepStart?: (entry: EnvironmentPullRequestEntry, event: PointerEvent) => void; + onCloseSweepStart?: (entry: Entry, event: PointerEvent) => void; }) { const resolveProjectDefault = usePullRequestDefaultMergeMethodResolver( entry.environmentId, @@ -45,7 +50,7 @@ export function PullRequestSpeedActions({ const { actionPending, perform } = usePullRequestActionRunner({ environmentId: entry.environmentId, reference, - onSuccess: (action) => onActed({ entry, action }), + onSuccess: (action) => onActed?.({ entry, action }), resolveMergeMethod: (detail) => { const allowed = detail.capabilities.mergeMethods.filter( (method) => detail.mergeCapabilities[method], @@ -102,7 +107,7 @@ export function PullRequestSpeedActions({ {action === "merge" && entry.stack ? "Open this pull request to merge its stack" - : action === "close" + : action === "close" && onCloseSweepStart ? "Close immediately, or drag across rows to close several" : `${label} immediately`} diff --git a/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx b/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx index 643c17d4b775..c93a6a3d3d5a 100644 --- a/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx +++ b/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx @@ -1,4 +1,5 @@ -import type { ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts"; +import type { ProjectId, ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts"; +import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; import { resolveThreadPullRequestChains, visibleThreadPullRequests, @@ -16,6 +17,7 @@ import { useCallback, useMemo } from "react"; import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; import { useOpenPrLink } from "~/lib/openPullRequestLink"; import { cn } from "~/lib/utils"; +import { useShortcutModifierState } from "~/shortcutModifierState"; import { useServerConfigs, useThreadShell } from "~/state/entities"; import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState"; import { threadEnvironment } from "~/state/threads"; @@ -42,6 +44,7 @@ import { pullRequestChecksStatePresentation, } from "./pullRequestPresentation"; import { PullRequestGlyph } from "./pullRequestIcons"; +import { PullRequestSpeedActions } from "./PullRequestSpeedActions"; const SOURCE_LABELS: Record = { manual: "Linked by you", @@ -74,11 +77,15 @@ function ChecksGlyph({ function LinkRow({ line, threadRef, + projectId, + speedMode, onUnlink, onSetWatching, }: { line: PullRequestListLine; threadRef: ScopedThreadRef; + projectId: ProjectId | null; + speedMode: boolean; onUnlink: (link: ThreadPullRequestLink) => void; /** Null when the environment cannot watch pull requests. */ onSetWatching: ((link: ThreadPullRequestLink, watching: boolean) => void) | null; @@ -88,6 +95,22 @@ function LinkRow({ const snapshot = link.snapshot; const open = snapshot === null || snapshot.state === "open"; const watching = link.watch !== undefined; + const actionEntry = + projectId !== null && + snapshot !== null && + snapshot.state !== "merged" && + detectSourceControlProviderFromRemoteUrl(link.url)?.kind === "github" + ? { + environmentId: threadRef.environmentId, + projectId, + host: link.host, + repository: link.repository, + number: link.number, + state: snapshot.state, + isDraft: snapshot.isDraft, + ...(link.stack === null ? {} : { stack: link.stack }), + } + : null; return (
+ {actionEntry !== null ? ( + + ) : null} {/* Out of the row's flow, so no row reserves a column for a button only the hovered one shows. It sits over the right end of the second line on the row's own hover color, fading in from the left, so it covers the time and leaves the diff counts alone. */} @@ -218,6 +244,8 @@ function LinkRow({ "pointer-events-none opacity-0 group-hover/pr-row:pointer-events-auto group-hover/pr-row:opacity-100", "has-[[data-popup-open]]:pointer-events-auto has-[[data-popup-open]]:opacity-100", "has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:opacity-100", + "group-has-[[data-pull-request-action-pending=true]]/pr-row:hidden", + speedMode && actionEntry !== null && "hidden", )} > @@ -275,12 +303,14 @@ export function ThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThreadRef }) { const thread = useThreadShell(threadRef); + const modifiers = useShortcutModifierState(true); + const speedMode = + modifiers.shiftKey && !modifiers.metaKey && !modifiers.ctrlKey && !modifiers.altKey; const openLinkDialog = useCallback(() => openLinkPullRequestDialog(threadRef), [threadRef]); const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: true }); const watch = useAtomCommand(threadEnvironment.watchPullRequest, { reportFailure: true }); - const supportsWatch = - useServerConfigs().get(threadRef.environmentId)?.environment.capabilities - .threadPullRequestWatch === true; + const capabilities = useServerConfigs().get(threadRef.environmentId)?.environment.capabilities; + const supportsWatch = capabilities?.threadPullRequestWatch === true; const links = useMemo(() => visibleThreadPullRequests(thread?.pullRequests ?? []), [thread]); const lines = useMemo(() => pullRequestListLines(resolveThreadPullRequestChains(links)), [links]); const handleUnlink = useCallback( @@ -351,6 +381,8 @@ function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread key={`${line.link.host}/${line.link.repository}#${line.link.number}`} line={line} threadRef={threadRef} + projectId={capabilities?.pullRequests === true ? (thread?.projectId ?? null) : null} + speedMode={speedMode} onUnlink={handleUnlink} onSetWatching={supportsWatch ? handleSetWatching : null} /> From c02fc91ce4a44b6d2ca655f258e64fc1b383dc2d Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 6 Oct 2026 23:35:45 +0000 Subject: [PATCH 2/2] fix(web): resolve linked pr merge preferences by repository --- .../pullRequest/ThreadPullRequestsPanel.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx b/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx index c93a6a3d3d5a..ffe469f16d68 100644 --- a/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx +++ b/apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx @@ -15,10 +15,10 @@ import { import { useCallback, useMemo } from "react"; import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; -import { useOpenPrLink } from "~/lib/openPullRequestLink"; +import { findProjectForChangeRequest, useOpenPrLink } from "~/lib/openPullRequestLink"; import { cn } from "~/lib/utils"; import { useShortcutModifierState } from "~/shortcutModifierState"; -import { useServerConfigs, useThreadShell } from "~/state/entities"; +import { useProjects, useServerConfigs, useThreadShell } from "~/state/entities"; import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState"; import { threadEnvironment } from "~/state/threads"; import { useAtomCommand } from "~/state/use-atom-command"; @@ -303,6 +303,16 @@ export function ThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThreadRef }) { const thread = useThreadShell(threadRef); + const projects = useProjects(); + const environmentProjects = useMemo( + () => + projects + .filter((project) => project.environmentId === threadRef.environmentId) + .toSorted((left, right) => + left.id === thread?.projectId ? -1 : right.id === thread?.projectId ? 1 : 0, + ), + [projects, threadRef.environmentId, thread?.projectId], + ); const modifiers = useShortcutModifierState(true); const speedMode = modifiers.shiftKey && !modifiers.metaKey && !modifiers.ctrlKey && !modifiers.altKey; @@ -381,7 +391,13 @@ function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread key={`${line.link.host}/${line.link.repository}#${line.link.number}`} line={line} threadRef={threadRef} - projectId={capabilities?.pullRequests === true ? (thread?.projectId ?? null) : null} + projectId={ + capabilities?.pullRequests === true + ? (findProjectForChangeRequest(environmentProjects, line.link)?.id ?? + thread?.projectId ?? + null) + : null + } speedMode={speedMode} onUnlink={handleUnlink} onSetWatching={supportsWatch ? handleSetWatching : null}