Repository navigation
feat(web): add fast actions to linked pull requests #16627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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, | ||||||||||
|
|
@@ -14,9 +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 { useServerConfigs, useThreadShell } from "~/state/entities"; | ||||||||||
| import { useShortcutModifierState } from "~/shortcutModifierState"; | ||||||||||
| import { useProjects, useServerConfigs, useThreadShell } from "~/state/entities"; | ||||||||||
| import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState"; | ||||||||||
| import { threadEnvironment } from "~/state/threads"; | ||||||||||
| import { useAtomCommand } from "~/state/use-atom-command"; | ||||||||||
|
|
@@ -42,6 +44,7 @@ import { | |||||||||
| pullRequestChecksStatePresentation, | ||||||||||
| } from "./pullRequestPresentation"; | ||||||||||
| import { PullRequestGlyph } from "./pullRequestIcons"; | ||||||||||
| import { PullRequestSpeedActions } from "./PullRequestSpeedActions"; | ||||||||||
|
|
||||||||||
| const SOURCE_LABELS: Record<ThreadPullRequestLink["source"], string> = { | ||||||||||
| 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 ( | ||||||||||
| <div | ||||||||||
| className={cn(PULL_REQUEST_ROW_CLASS, "relative hover:bg-accent/60")} | ||||||||||
|
|
@@ -206,6 +229,9 @@ function LinkRow({ | |||||||||
| updatedAt={snapshot?.updatedAt} | ||||||||||
| /> | ||||||||||
| </a> | ||||||||||
| {actionEntry !== null ? ( | ||||||||||
| <PullRequestSpeedActions entry={actionEntry} visible={speedMode} /> | ||||||||||
| ) : 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", | ||||||||||
| )} | ||||||||||
| > | ||||||||||
| <span aria-hidden className="absolute inset-0 bg-accent/60" /> | ||||||||||
|
|
@@ -275,12 +303,24 @@ 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; | ||||||||||
| 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 +391,14 @@ function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThread | |||||||||
| key={`${line.link.host}/${line.link.repository}#${line.link.number}`} | ||||||||||
| line={line} | ||||||||||
| threadRef={threadRef} | ||||||||||
| projectId={ | ||||||||||
| capabilities?.pullRequests === true | ||||||||||
| ? (findProjectForChangeRequest(environmentProjects, line.link)?.id ?? | ||||||||||
| thread?.projectId ?? | ||||||||||
| null) | ||||||||||
|
Comment on lines
+396
to
+398
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: git diff --unified=35 8ddf200e8f637eaf9a4f0d3036ece8f1d3869d10 c02fc91ce4a44b6d2ca655f258e64fc1b383dc2d -- apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx
printf '\\n--- action references ---\\n'
rg -n -F -- 'runAction' apps/web/src
printf '\\n--- relevant action type and speed actions references ---\\n'
rg -n -F -- 'PullRequestSpeedActions' apps/web/src/components/pullRequest
printf '\\n--- project lookup ---\\n'
rg -n -F -- 'findProjectForChangeRequest' apps/web/srcRepository: pingdotgg/t3code Length of output: 20848 🏁 Script executed: printf '%s\\n' '--- speed action component ---'
sed -n '1,240p' apps/web/src/components/pullRequest/PullRequestSpeedActions.tsx
printf '%s\\n' '--- action hook ---'
sed -n '120,245p' apps/web/src/components/pullRequest/usePullRequestActions.ts
printf '%s\\n' '--- runAction declaration and implementation references ---'
rg -n -F -- 'runAction:' .
rg -n -F -- 'runAction = ' apps
rg -n -F -- 'pullRequestEnvironment' apps
printf '%s\\n' '--- project matching implementation ---'
sed -n '55,105p' apps/web/src/lib/openPullRequestLink.tsRepository: pingdotgg/t3code Length of output: 25259 🏁 Script executed: nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '1920,2025p'
nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '3360,3425p'
nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '235,275p'
rg -n -F -- 'resolveMergeMethod' apps/web/src/components/pullRequest apps/server/src/pullRequest/PullRequestService.tsRepository: pingdotgg/t3code Length of output: 10629 🏁 Script executed: printf '%s\\n' '--- relevant declarations ---'
rg -n -F -- 'requireProject' apps/server/src/pullRequest/PullRequestService.ts
rg -n -F -- 'canonicalRef' apps/server/src/pullRequest/PullRequestService.ts
rg -n -F -- 'runActionAndInvalidate' apps/server/src/pullRequest/PullRequestService.ts
rg -n -F -- 'usePullRequestDefaultMergeMethodResolver' apps/web/src/components/pullRequest/usePullRequestActions.ts
printf '%s\\n' '--- action flow remainder ---'
nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '1990,2075p'
nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '3260,3310p'
printf '%s\\n' '--- default resolver ---'
nl -ba apps/web/src/components/pullRequest/usePullRequestActions.ts | sed -n '1,115p'Repository: pingdotgg/t3code Length of output: 14938 🏁 Script executed: nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '850,970p'
nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '1941,2085p'
nl -ba apps/server/src/pullRequest/PullRequestService.ts | sed -n '3315,3355p'Repository: pingdotgg/t3code Length of output: 15589 🏁 Script executed: rg -n -F -- 'resolvePullRequestMergeMethod' apps/web/src/components/pullRequest
rg -n -F -- 'pullRequestMergeMethod' apps/web/src docs packages
rg -n -F -- 'merge method' docs/internals docs/user apps/web/src/components/settings apps/web/srcRepository: pingdotgg/t3code Length of output: 7015 🏁 Script executed: nl -ba docs/user/project-settings.md | sed -n '95,118p'
nl -ba apps/web/src/components/pullRequest/pullRequestDetail.logic.ts | sed -n '48,92p'
nl -ba apps/web/src/components/settings/ProjectDefaultsSettings.tsx | sed -n '432,478p'Repository: pingdotgg/t3code Length of output: 5915 Do not use the thread project’s merge default for an unmatched repository. When no environment project matches a linked PR from another repository, this fallback supplies the thread project ID. The speed action uses that ID to resolve the project merge default, which takes precedence over the last-selected method. An open, non-draft PR can therefore be merged with a method configured for a different project. Use only the matched project ID; without a match, do not offer the quick action. 🐛 Suggested fix- ? (findProjectForChangeRequest(environmentProjects, line.link)?.id ??
- thread?.projectId ??
- null)
+ ? (findProjectForChangeRequest(environmentProjects, line.link)?.id ?? null)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| : null | ||||||||||
| } | ||||||||||
| speedMode={speedMode} | ||||||||||
| onUnlink={handleUnlink} | ||||||||||
| onSetWatching={supportsWatch ? handleSetWatching : null} | ||||||||||
| /> | ||||||||||
|
|
||||||||||
Uh oh!
There was an error while loading. Please reload this page.