Skip to content

Commit ab28039

Browse files
authored
feat(web): add fast actions to linked pull requests (#16627)
1 parent c5c6f9c commit ab28039

2 files changed

Lines changed: 67 additions & 14 deletions

File tree

‎apps/web/src/components/pullRequest/PullRequestSpeedActions.tsx‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,31 @@ import {
1111
usePullRequestDefaultMergeMethodResolver,
1212
} from "./usePullRequestActions";
1313

14-
export interface PullRequestSpeedActionResult {
15-
readonly entry: EnvironmentPullRequestEntry;
14+
type PullRequestSpeedActionEntry = Pick<
15+
EnvironmentPullRequestEntry,
16+
"environmentId" | "projectId" | "host" | "repository" | "number" | "state" | "isDraft"
17+
> & { readonly stack?: object | undefined };
18+
19+
export interface PullRequestSpeedActionResult<Entry = EnvironmentPullRequestEntry> {
20+
readonly entry: Entry;
1621
readonly action: PullRequestAction;
1722
}
1823

1924
/** No detail or stack reads until a merge is clicked, even on a long list. */
20-
export function PullRequestSpeedActions({
25+
export function PullRequestSpeedActions<Entry extends PullRequestSpeedActionEntry>({
2126
entry,
2227
visible,
2328
onActed,
2429
closing = false,
2530
sweeping = false,
2631
onCloseSweepStart,
2732
}: {
28-
entry: EnvironmentPullRequestEntry;
33+
entry: Entry;
2934
visible: boolean;
30-
onActed: (result: PullRequestSpeedActionResult) => void;
35+
onActed?: (result: PullRequestSpeedActionResult<Entry>) => void;
3136
closing?: boolean;
3237
sweeping?: boolean;
33-
onCloseSweepStart?: (entry: EnvironmentPullRequestEntry, event: PointerEvent) => void;
38+
onCloseSweepStart?: (entry: Entry, event: PointerEvent) => void;
3439
}) {
3540
const resolveProjectDefault = usePullRequestDefaultMergeMethodResolver(
3641
entry.environmentId,
@@ -45,7 +50,7 @@ export function PullRequestSpeedActions({
4550
const { actionPending, perform } = usePullRequestActionRunner({
4651
environmentId: entry.environmentId,
4752
reference,
48-
onSuccess: (action) => onActed({ entry, action }),
53+
onSuccess: (action) => onActed?.({ entry, action }),
4954
resolveMergeMethod: (detail) => {
5055
const allowed = detail.capabilities.mergeMethods.filter(
5156
(method) => detail.mergeCapabilities[method],
@@ -102,7 +107,7 @@ export function PullRequestSpeedActions({
102107
<TooltipPopup>
103108
{action === "merge" && entry.stack
104109
? "Open this pull request to merge its stack"
105-
: action === "close"
110+
: action === "close" && onCloseSweepStart
106111
? "Close immediately, or drag across rows to close several"
107112
: `${label} immediately`}
108113
</TooltipPopup>

‎apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx‎

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts";
1+
import type { ProjectId, ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts";
2+
import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl";
23
import {
34
resolveThreadPullRequestChains,
45
visibleThreadPullRequests,
@@ -14,9 +15,10 @@ import {
1415
import { useCallback, useMemo, useState } from "react";
1516

1617
import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
17-
import { useOpenPrLink } from "~/lib/openPullRequestLink";
18+
import { findProjectForChangeRequest, useOpenPrLink } from "~/lib/openPullRequestLink";
1819
import { cn } from "~/lib/utils";
19-
import { useServerConfigs, useThreadShell } from "~/state/entities";
20+
import { useShortcutModifierState } from "~/shortcutModifierState";
21+
import { useProjects, useServerConfigs, useThreadShell } from "~/state/entities";
2022
import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState";
2123
import { threadEnvironment } from "~/state/threads";
2224
import { useAtomCommand } from "~/state/use-atom-command";
@@ -42,6 +44,7 @@ import {
4244
pullRequestChecksStatePresentation,
4345
} from "./pullRequestPresentation";
4446
import { PullRequestGlyph } from "./pullRequestIcons";
47+
import { PullRequestSpeedActions } from "./PullRequestSpeedActions";
4548

4649
const SOURCE_LABELS: Record<ThreadPullRequestLink["source"], string> = {
4750
manual: "Linked by you",
@@ -74,11 +77,15 @@ function ChecksGlyph({
7477
function LinkRow({
7578
line,
7679
threadRef,
80+
projectId,
81+
speedMode,
7782
onUnlink,
7883
onSetWatching,
7984
}: {
8085
line: PullRequestListLine;
8186
threadRef: ScopedThreadRef;
87+
projectId: ProjectId | null;
88+
speedMode: boolean;
8289
onUnlink: (link: ThreadPullRequestLink) => void;
8390
/** Null when the environment cannot watch pull requests. */
8491
onSetWatching: ((link: ThreadPullRequestLink, watching: boolean) => void) | null;
@@ -97,6 +104,22 @@ function LinkRow({
97104
const snapshot = link.snapshot;
98105
const open = snapshot === null || snapshot.state === "open";
99106
const watching = link.watch !== undefined;
107+
const actionEntry =
108+
projectId !== null &&
109+
snapshot !== null &&
110+
snapshot.state !== "merged" &&
111+
detectSourceControlProviderFromRemoteUrl(link.url)?.kind === "github"
112+
? {
113+
environmentId: threadRef.environmentId,
114+
projectId,
115+
host: link.host,
116+
repository: link.repository,
117+
number: link.number,
118+
state: snapshot.state,
119+
isDraft: snapshot.isDraft,
120+
...(link.stack === null ? {} : { stack: link.stack }),
121+
}
122+
: null;
100123
return (
101124
<div
102125
className={cn(PULL_REQUEST_ROW_CLASS, "relative hover:bg-accent/60")}
@@ -221,6 +244,9 @@ function LinkRow({
221244
updatedAt={snapshot?.updatedAt}
222245
/>
223246
</a>
247+
{actionEntry !== null ? (
248+
<PullRequestSpeedActions entry={actionEntry} visible={speedMode} />
249+
) : null}
224250
{/* Out of the row's flow, so no row reserves a column for a button only the hovered one
225251
shows. It sits over the right end of the second line on the row's own hover color,
226252
fading in from the left, so it covers the time and leaves the diff counts alone. */}
@@ -233,6 +259,8 @@ function LinkRow({
233259
"pointer-events-none opacity-0 group-hover/pr-row:pointer-events-auto group-hover/pr-row:opacity-100",
234260
"has-[[data-popup-open]]:pointer-events-auto has-[[data-popup-open]]:opacity-100",
235261
"has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:opacity-100",
262+
"group-has-[[data-pull-request-action-pending=true]]/pr-row:hidden",
263+
speedMode && actionEntry !== null && "hidden",
236264
)}
237265
>
238266
<span aria-hidden className="absolute inset-0 bg-accent/60" />
@@ -301,12 +329,24 @@ export function ThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread
301329

302330
function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThreadRef }) {
303331
const thread = useThreadShell(threadRef);
332+
const projects = useProjects();
333+
const environmentProjects = useMemo(
334+
() =>
335+
projects
336+
.filter((project) => project.environmentId === threadRef.environmentId)
337+
.toSorted((left, right) =>
338+
left.id === thread?.projectId ? -1 : right.id === thread?.projectId ? 1 : 0,
339+
),
340+
[projects, threadRef.environmentId, thread?.projectId],
341+
);
342+
const modifiers = useShortcutModifierState(true);
343+
const speedMode =
344+
modifiers.shiftKey && !modifiers.metaKey && !modifiers.ctrlKey && !modifiers.altKey;
304345
const openLinkDialog = useCallback(() => openLinkPullRequestDialog(threadRef), [threadRef]);
305346
const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: true });
306347
const watch = useAtomCommand(threadEnvironment.watchPullRequest, { reportFailure: true });
307-
const supportsWatch =
308-
useServerConfigs().get(threadRef.environmentId)?.environment.capabilities
309-
.threadPullRequestWatch === true;
348+
const capabilities = useServerConfigs().get(threadRef.environmentId)?.environment.capabilities;
349+
const supportsWatch = capabilities?.threadPullRequestWatch === true;
310350
const links = useMemo(() => visibleThreadPullRequests(thread?.pullRequests ?? []), [thread]);
311351
const lines = useMemo(() => pullRequestListLines(resolveThreadPullRequestChains(links)), [links]);
312352
const handleUnlink = useCallback(
@@ -377,6 +417,14 @@ function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread
377417
key={`${line.link.host}/${line.link.repository}#${line.link.number}`}
378418
line={line}
379419
threadRef={threadRef}
420+
projectId={
421+
capabilities?.pullRequests === true
422+
? (findProjectForChangeRequest(environmentProjects, line.link)?.id ??
423+
thread?.projectId ??
424+
null)
425+
: null
426+
}
427+
speedMode={speedMode}
380428
onUnlink={handleUnlink}
381429
onSetWatching={supportsWatch ? handleSetWatching : null}
382430
/>

0 commit comments

Comments
 (0)