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