| ... | @@ -1,4 +1,5 @@ | ... | @@ -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"; |
| 2 | import { | 3 | import { |
| 3 | resolveThreadPullRequestChains, | 4 | resolveThreadPullRequestChains, |
| 4 | visibleThreadPullRequests, | 5 | visibleThreadPullRequests, |
| ... | @@ -14,9 +15,10 @@ import { | ... | @@ -14,9 +15,10 @@ import { |
| 14 | import { useCallback, useMemo, useState } from "react"; | 15 | import { useCallback, useMemo, useState } from "react"; |
| 15 | | 16 | |
| 16 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; | 17 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; |
| 17 | import { useOpenPrLink } from "~/lib/openPullRequestLink"; | 18 | import { findProjectForChangeRequest, useOpenPrLink } from "~/lib/openPullRequestLink"; |
| 18 | import { cn } from "~/lib/utils"; | 19 | 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"; |
| 20 | import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState"; | 22 | import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState"; |
| 21 | import { threadEnvironment } from "~/state/threads"; | 23 | import { threadEnvironment } from "~/state/threads"; |
| 22 | import { useAtomCommand } from "~/state/use-atom-command"; | 24 | import { useAtomCommand } from "~/state/use-atom-command"; |
| ... | @@ -42,6 +44,7 @@ import { | ... | @@ -42,6 +44,7 @@ import { |
| 42 | pullRequestChecksStatePresentation, | 44 | pullRequestChecksStatePresentation, |
| 43 | } from "./pullRequestPresentation"; | 45 | } from "./pullRequestPresentation"; |
| 44 | import { PullRequestGlyph } from "./pullRequestIcons"; | 46 | import { PullRequestGlyph } from "./pullRequestIcons"; |
| | 47 | import { PullRequestSpeedActions } from "./PullRequestSpeedActions"; |
| 45 | | 48 | |
| 46 | const SOURCE_LABELS: Record<ThreadPullRequestLink["source"], string> = { | 49 | const SOURCE_LABELS: Record<ThreadPullRequestLink["source"], string> = { |
| 47 | manual: "Linked by you", | 50 | manual: "Linked by you", |
| ... | @@ -74,11 +77,15 @@ function ChecksGlyph({ | ... | @@ -74,11 +77,15 @@ function ChecksGlyph({ |
| 74 | function LinkRow({ | 77 | function LinkRow({ |
| 75 | line, | 78 | line, |
| 76 | threadRef, | 79 | threadRef, |
| | 80 | projectId, |
| | 81 | speedMode, |
| 77 | onUnlink, | 82 | onUnlink, |
| 78 | onSetWatching, | 83 | onSetWatching, |
| 79 | }: { | 84 | }: { |
| 80 | line: PullRequestListLine; | 85 | line: PullRequestListLine; |
| 81 | threadRef: ScopedThreadRef; | 86 | threadRef: ScopedThreadRef; |
| | 87 | projectId: ProjectId | null; |
| | 88 | speedMode: boolean; |
| 82 | onUnlink: (link: ThreadPullRequestLink) => void; | 89 | onUnlink: (link: ThreadPullRequestLink) => void; |
| 83 | /** Null when the environment cannot watch pull requests. */ | 90 | /** Null when the environment cannot watch pull requests. */ |
| 84 | onSetWatching: ((link: ThreadPullRequestLink, watching: boolean) => void) | null; | 91 | onSetWatching: ((link: ThreadPullRequestLink, watching: boolean) => void) | null; |
| ... | @@ -97,6 +104,22 @@ function LinkRow({ | ... | @@ -97,6 +104,22 @@ function LinkRow({ |
| 97 | const snapshot = link.snapshot; | 104 | const snapshot = link.snapshot; |
| 98 | const open = snapshot === null || snapshot.state === "open"; | 105 | const open = snapshot === null || snapshot.state === "open"; |
| 99 | const watching = link.watch !== undefined; | 106 | 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; |
| 100 | return ( | 123 | return ( |
| 101 | <div | 124 | <div |
| 102 | className={cn(PULL_REQUEST_ROW_CLASS, "relative hover:bg-accent/60")} | 125 | className={cn(PULL_REQUEST_ROW_CLASS, "relative hover:bg-accent/60")} |
| ... | @@ -221,6 +244,9 @@ function LinkRow({ | ... | @@ -221,6 +244,9 @@ function LinkRow({ |
| 221 | updatedAt={snapshot?.updatedAt} | 244 | updatedAt={snapshot?.updatedAt} |
| 222 | /> | 245 | /> |
| 223 | </a> | 246 | </a> |
| | 247 | {actionEntry !== null ? ( |
| | 248 | <PullRequestSpeedActions entry={actionEntry} visible={speedMode} /> |
| | 249 | ) : null} |
| 224 | {/* Out of the row's flow, so no row reserves a column for a button only the hovered one | 250 | {/* Out of the row's flow, so no row reserves a column for a button only the hovered one |
| 225 | shows. It sits over the right end of the second line on the row's own hover color, | 251 | shows. It sits over the right end of the second line on the row's own hover color, |
| 226 | fading in from the left, so it covers the time and leaves the diff counts alone. */} | 252 | fading in from the left, so it covers the time and leaves the diff counts alone. */} |
| ... | @@ -233,6 +259,8 @@ function LinkRow({ | ... | @@ -233,6 +259,8 @@ function LinkRow({ |
| 233 | "pointer-events-none opacity-0 group-hover/pr-row:pointer-events-auto group-hover/pr-row:opacity-100", | 259 | "pointer-events-none opacity-0 group-hover/pr-row:pointer-events-auto group-hover/pr-row:opacity-100", |
| 234 | "has-[[data-popup-open]]:pointer-events-auto has-[[data-popup-open]]:opacity-100", | 260 | "has-[[data-popup-open]]:pointer-events-auto has-[[data-popup-open]]:opacity-100", |
| 235 | "has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:opacity-100", | 261 | "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", |
| 236 | )} | 264 | )} |
| 237 | > | 265 | > |
| 238 | <span aria-hidden className="absolute inset-0 bg-accent/60" /> | 266 | <span aria-hidden className="absolute inset-0 bg-accent/60" /> |
| ... | @@ -301,12 +329,24 @@ export function ThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread | ... | @@ -301,12 +329,24 @@ export function ThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread |
| 301 | | 329 | |
| 302 | function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThreadRef }) { | 330 | function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThreadRef }) { |
| 303 | const thread = useThreadShell(threadRef); | 331 | 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; |
| 304 | const openLinkDialog = useCallback(() => openLinkPullRequestDialog(threadRef), [threadRef]); | 345 | const openLinkDialog = useCallback(() => openLinkPullRequestDialog(threadRef), [threadRef]); |
| 305 | const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: true }); | 346 | const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: true }); |
| 306 | const watch = useAtomCommand(threadEnvironment.watchPullRequest, { reportFailure: true }); | 347 | const watch = useAtomCommand(threadEnvironment.watchPullRequest, { reportFailure: true }); |
| 307 | const supportsWatch = | 348 | const capabilities = useServerConfigs().get(threadRef.environmentId)?.environment.capabilities; |
| 308 | useServerConfigs().get(threadRef.environmentId)?.environment.capabilities | 349 | const supportsWatch = capabilities?.threadPullRequestWatch === true; |
| 309 | .threadPullRequestWatch === true; | | |
| 310 | const links = useMemo(() => visibleThreadPullRequests(thread?.pullRequests ?? []), [thread]); | 350 | const links = useMemo(() => visibleThreadPullRequests(thread?.pullRequests ?? []), [thread]); |
| 311 | const lines = useMemo(() => pullRequestListLines(resolveThreadPullRequestChains(links)), [links]); | 351 | const lines = useMemo(() => pullRequestListLines(resolveThreadPullRequestChains(links)), [links]); |
| 312 | const handleUnlink = useCallback( | 352 | const handleUnlink = useCallback( |
| ... | @@ -377,6 +417,14 @@ function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread | ... | @@ -377,6 +417,14 @@ function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread |
| 377 | key={`${line.link.host}/${line.link.repository}#${line.link.number}`} | 417 | key={`${line.link.host}/${line.link.repository}#${line.link.number}`} |
| 378 | line={line} | 418 | line={line} |
| 379 | threadRef={threadRef} | 419 | 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} |
| 380 | onUnlink={handleUnlink} | 428 | onUnlink={handleUnlink} |
| 381 | onSetWatching={supportsWatch ? handleSetWatching : null} | 429 | onSetWatching={supportsWatch ? handleSetWatching : null} |
| 382 | /> | 430 | /> |