mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
fix(github): avoid effect-driven stack mode state
This commit is contained in:
@@ -1319,8 +1319,6 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
setBody: setPrBody,
|
||||
draft: prDraft,
|
||||
setDraft: setPrDraft,
|
||||
stacked: prStacked,
|
||||
setStacked: setPrStacked,
|
||||
stackedCreationSupported: prStackedCreationSupported,
|
||||
baseQuery: prBaseQuery,
|
||||
setBaseQuery: setPrBaseQuery,
|
||||
@@ -3980,124 +3978,86 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
]
|
||||
)
|
||||
|
||||
const handleCreatePullRequest = useCallback(async (): Promise<void> => {
|
||||
if (!repo || !branch || !createComposerOpen || prGenerating || createPrInFlightRef.current) {
|
||||
return
|
||||
}
|
||||
const handleCreatePullRequest = useCallback(
|
||||
async (stacked = false): Promise<void> => {
|
||||
if (!repo || !branch || !createComposerOpen || prGenerating || createPrInFlightRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestContextKey = panelContextKey
|
||||
const isCurrentCreateRequest = (): boolean =>
|
||||
panelContextKeyRef.current === requestContextKey &&
|
||||
createPrInFlightRef.current === requestContextKey
|
||||
const base = stripBaseRef(prBase).trim()
|
||||
const title = prTitle.trim()
|
||||
const worktreePath = activeWorktreePath ?? repo.path
|
||||
if (!title) {
|
||||
setCreatePrError(
|
||||
translate(
|
||||
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
|
||||
'Enter a {{value0}} title.',
|
||||
{
|
||||
value0: hostedReviewCreateCopy.reviewLabel
|
||||
const requestContextKey = panelContextKey
|
||||
const isCurrentCreateRequest = (): boolean =>
|
||||
panelContextKeyRef.current === requestContextKey &&
|
||||
createPrInFlightRef.current === requestContextKey
|
||||
const base = stripBaseRef(prBase).trim()
|
||||
const title = prTitle.trim()
|
||||
const worktreePath = activeWorktreePath ?? repo.path
|
||||
if (!title) {
|
||||
setCreatePrError(
|
||||
translate(
|
||||
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
|
||||
'Enter a {{value0}} title.',
|
||||
{
|
||||
value0: hostedReviewCreateCopy.reviewLabel
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branch).toLowerCase()) {
|
||||
setCreatePrError(
|
||||
translate(
|
||||
'auto.components.right.sidebar.SourceControl.ae743199cd',
|
||||
'Choose a different base branch before creating a {{value0}}.',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
createPrInFlightRef.current = requestContextKey
|
||||
setIsCreatingPr(true)
|
||||
setCreatePrError(null)
|
||||
let pushed = false
|
||||
try {
|
||||
const shouldPushBeforeCreate =
|
||||
createPrPushFirst || hostedReviewCreation?.blockedReason === 'needs_push'
|
||||
if (shouldPushBeforeCreate) {
|
||||
const ok = await pushBeforeCreatePullRequest()
|
||||
if (!isCurrentCreateRequest()) {
|
||||
return
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branch).toLowerCase()) {
|
||||
setCreatePrError(
|
||||
translate(
|
||||
'auto.components.right.sidebar.SourceControl.ae743199cd',
|
||||
'Choose a different base branch before creating a {{value0}}.',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
createPrInFlightRef.current = requestContextKey
|
||||
setIsCreatingPr(true)
|
||||
setCreatePrError(null)
|
||||
let pushed = false
|
||||
try {
|
||||
const shouldPushBeforeCreate =
|
||||
createPrPushFirst || hostedReviewCreation?.blockedReason === 'needs_push'
|
||||
if (shouldPushBeforeCreate) {
|
||||
const ok = await pushBeforeCreatePullRequest()
|
||||
if (!ok) {
|
||||
setCreatePrError('Push failed. Resolve the push error, then try again.')
|
||||
return
|
||||
}
|
||||
pushed = true
|
||||
}
|
||||
const createInput = {
|
||||
repoId: repo.id,
|
||||
provider: hostedReviewCreateProvider,
|
||||
base,
|
||||
head: normalizeHostedReviewHeadRef(branch),
|
||||
title,
|
||||
body: prBody,
|
||||
draft: prDraft,
|
||||
worktreePath,
|
||||
useTemplate: prCreationDefaults.useTemplate
|
||||
}
|
||||
const result = stacked
|
||||
? await createStackedHostedReview(repo.path, createInput)
|
||||
: await createHostedReview(repo.path, createInput)
|
||||
if (!isCurrentCreateRequest()) {
|
||||
return
|
||||
}
|
||||
if (!ok) {
|
||||
setCreatePrError('Push failed. Resolve the push error, then try again.')
|
||||
return
|
||||
}
|
||||
pushed = true
|
||||
}
|
||||
const createInput = {
|
||||
repoId: repo.id,
|
||||
provider: hostedReviewCreateProvider,
|
||||
base,
|
||||
head: normalizeHostedReviewHeadRef(branch),
|
||||
title,
|
||||
body: prBody,
|
||||
draft: prDraft,
|
||||
worktreePath,
|
||||
useTemplate: prCreationDefaults.useTemplate
|
||||
}
|
||||
const result = prStacked
|
||||
? await createStackedHostedReview(repo.path, createInput)
|
||||
: await createHostedReview(repo.path, createInput)
|
||||
if (!isCurrentCreateRequest()) {
|
||||
return
|
||||
}
|
||||
if (result.ok) {
|
||||
await handlePullRequestCreated({
|
||||
provider: hostedReviewCreateProvider,
|
||||
number: result.number,
|
||||
url: result.url
|
||||
})
|
||||
if (prCreationDefaults.openAfterCreate) {
|
||||
openHttpLink(result.url, { worktreeId: activeWorktreeId })
|
||||
}
|
||||
if (activePullRequestGenerationKey) {
|
||||
updatePullRequestGenerationRecord(
|
||||
activePullRequestGenerationKey,
|
||||
clearPullRequestGenerationRequiresPushBeforeCreate
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ('existingReview' in result && result.existingReview?.url) {
|
||||
const number = result.existingReview.number
|
||||
toast.success(
|
||||
number
|
||||
? translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.b6ce28da5b',
|
||||
'{{value0}} #{{value1}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.cf9e69f3be',
|
||||
'{{value0}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel }
|
||||
),
|
||||
{
|
||||
action: {
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.192e686e57',
|
||||
'Open on {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.providerName }
|
||||
),
|
||||
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (number) {
|
||||
if (result.ok) {
|
||||
await handlePullRequestCreated({
|
||||
provider: hostedReviewCreateProvider,
|
||||
number,
|
||||
url: result.existingReview.url
|
||||
number: result.number,
|
||||
url: result.url
|
||||
})
|
||||
if (prCreationDefaults.openAfterCreate) {
|
||||
openHttpLink(result.url, { worktreeId: activeWorktreeId })
|
||||
}
|
||||
if (activePullRequestGenerationKey) {
|
||||
updatePullRequestGenerationRecord(
|
||||
activePullRequestGenerationKey,
|
||||
@@ -4106,57 +4066,97 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
}
|
||||
return
|
||||
}
|
||||
if ('existingReview' in result && result.existingReview?.url) {
|
||||
const number = result.existingReview.number
|
||||
toast.success(
|
||||
number
|
||||
? translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.b6ce28da5b',
|
||||
'{{value0}} #{{value1}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.cf9e69f3be',
|
||||
'{{value0}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel }
|
||||
),
|
||||
{
|
||||
action: {
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.ChecksPanel.192e686e57',
|
||||
'Open on {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.providerName }
|
||||
),
|
||||
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (number) {
|
||||
await handlePullRequestCreated({
|
||||
provider: hostedReviewCreateProvider,
|
||||
number,
|
||||
url: result.existingReview.url
|
||||
})
|
||||
if (activePullRequestGenerationKey) {
|
||||
updatePullRequestGenerationRecord(
|
||||
activePullRequestGenerationKey,
|
||||
clearPullRequestGenerationRequiresPushBeforeCreate
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
setCreatePrError(formatCreateError(result, pushed, hostedReviewCreateCopy.shortLabel))
|
||||
} catch (error) {
|
||||
if (!isCurrentCreateRequest()) {
|
||||
return
|
||||
}
|
||||
setCreatePrError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
|
||||
'Failed to create {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
if (createPrInFlightRef.current === requestContextKey) {
|
||||
createPrInFlightRef.current = null
|
||||
setIsCreatingPr(false)
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
}
|
||||
}
|
||||
setCreatePrError(formatCreateError(result, pushed, hostedReviewCreateCopy.shortLabel))
|
||||
} catch (error) {
|
||||
if (!isCurrentCreateRequest()) {
|
||||
return
|
||||
}
|
||||
setCreatePrError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
|
||||
'Failed to create {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
if (createPrInFlightRef.current === requestContextKey) {
|
||||
createPrInFlightRef.current = null
|
||||
setIsCreatingPr(false)
|
||||
setGitStatusRefreshNonce((value) => value + 1)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
activeWorktreePath,
|
||||
activeWorktreeId,
|
||||
activePullRequestGenerationKey,
|
||||
branch,
|
||||
createComposerOpen,
|
||||
createHostedReview,
|
||||
createStackedHostedReview,
|
||||
createPrPushFirst,
|
||||
handlePullRequestCreated,
|
||||
hostedReviewCreateCopy.providerName,
|
||||
hostedReviewCreateCopy.reviewLabel,
|
||||
hostedReviewCreateCopy.shortLabel,
|
||||
hostedReviewCreateCopy.titleLabel,
|
||||
hostedReviewCreateProvider,
|
||||
hostedReviewCreation?.blockedReason,
|
||||
panelContextKey,
|
||||
prBase,
|
||||
prBody,
|
||||
prCreationDefaults.openAfterCreate,
|
||||
prCreationDefaults.useTemplate,
|
||||
prDraft,
|
||||
prGenerating,
|
||||
prStacked,
|
||||
prTitle,
|
||||
pushBeforeCreatePullRequest,
|
||||
repo,
|
||||
updatePullRequestGenerationRecord
|
||||
])
|
||||
},
|
||||
[
|
||||
activeWorktreePath,
|
||||
activeWorktreeId,
|
||||
activePullRequestGenerationKey,
|
||||
branch,
|
||||
createComposerOpen,
|
||||
createHostedReview,
|
||||
createStackedHostedReview,
|
||||
createPrPushFirst,
|
||||
handlePullRequestCreated,
|
||||
hostedReviewCreateCopy.providerName,
|
||||
hostedReviewCreateCopy.reviewLabel,
|
||||
hostedReviewCreateCopy.shortLabel,
|
||||
hostedReviewCreateCopy.titleLabel,
|
||||
hostedReviewCreateProvider,
|
||||
hostedReviewCreation?.blockedReason,
|
||||
panelContextKey,
|
||||
prBase,
|
||||
prBody,
|
||||
prCreationDefaults.openAfterCreate,
|
||||
prCreationDefaults.useTemplate,
|
||||
prDraft,
|
||||
prGenerating,
|
||||
prTitle,
|
||||
pushBeforeCreatePullRequest,
|
||||
repo,
|
||||
updatePullRequestGenerationRecord
|
||||
]
|
||||
)
|
||||
|
||||
// ── Empty state ──
|
||||
if (!activeWorktree) {
|
||||
@@ -4298,6 +4298,7 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
{!operationInProgress && createComposerOpen ? (
|
||||
<div className="mt-4 border-t border-border pt-3">
|
||||
<CreateHostedReviewComposer
|
||||
key={panelContextKey}
|
||||
className="p-0"
|
||||
provider={hostedReviewCreateProvider}
|
||||
branch={branch}
|
||||
@@ -4309,8 +4310,6 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
setBody={setPrBody}
|
||||
draft={prDraft}
|
||||
setDraft={setPrDraft}
|
||||
stacked={prStacked}
|
||||
setStacked={setPrStacked}
|
||||
stackedCreationSupported={prStackedCreationSupported}
|
||||
baseQuery={prBaseQuery}
|
||||
setBaseQuery={setPrBaseQuery}
|
||||
@@ -4341,7 +4340,7 @@ export default function ChecksPanel(): React.JSX.Element {
|
||||
}}
|
||||
onGenerate={() => void handleGeneratePullRequestFields()}
|
||||
onCancelGenerate={handleCancelGeneratePullRequestFields}
|
||||
onPrimaryAction={() => void handleCreatePullRequest()}
|
||||
onPrimaryAction={(stacked) => void handleCreatePullRequest(stacked)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
ChevronDown,
|
||||
GitMerge,
|
||||
@@ -50,8 +51,6 @@ export type CreateHostedReviewComposerProps = {
|
||||
setBody: (value: string) => void
|
||||
draft: boolean
|
||||
setDraft: (value: boolean) => void
|
||||
stacked: boolean
|
||||
setStacked: (value: boolean) => void
|
||||
stackedCreationSupported: boolean
|
||||
baseQuery: string
|
||||
setBaseQuery: (value: string) => void
|
||||
@@ -70,7 +69,7 @@ export type CreateHostedReviewComposerProps = {
|
||||
dropdownItems?: DropdownEntry[]
|
||||
onGenerate: () => void
|
||||
onCancelGenerate: () => void
|
||||
onPrimaryAction: () => void
|
||||
onPrimaryAction: (stacked: boolean) => void
|
||||
onDropdownAction?: (kind: DropdownActionKind) => void
|
||||
}
|
||||
|
||||
@@ -86,8 +85,6 @@ export function CreateHostedReviewComposer({
|
||||
setBody,
|
||||
draft,
|
||||
setDraft,
|
||||
stacked,
|
||||
setStacked,
|
||||
stackedCreationSupported,
|
||||
baseQuery,
|
||||
setBaseQuery,
|
||||
@@ -109,8 +106,11 @@ export function CreateHostedReviewComposer({
|
||||
onPrimaryAction,
|
||||
onDropdownAction
|
||||
}: CreateHostedReviewComposerProps): React.JSX.Element {
|
||||
const [stacked, setStacked] = useState(false)
|
||||
const copy = localizedHostedReviewCopy(resolveSupportedHostedReviewCopyProvider(provider))
|
||||
const ReviewIcon = provider === 'gitlab' ? GitMerge : GitPullRequestArrow
|
||||
const stackedModeAvailable = provider === 'github' && stackedCreationSupported
|
||||
const effectiveStacked = stackedModeAvailable && stacked
|
||||
const normalizedBase = stripBaseRef(base)
|
||||
const strippedBranch = stripBaseRef(branch)
|
||||
const baseSameAsBranch = normalizedBase.toLowerCase() === strippedBranch.toLowerCase()
|
||||
@@ -235,9 +235,9 @@ export function CreateHostedReviewComposer({
|
||||
setBody={setBody}
|
||||
draft={draft}
|
||||
setDraft={setDraft}
|
||||
stacked={stacked}
|
||||
stacked={effectiveStacked}
|
||||
setStacked={setStacked}
|
||||
showStackedMode={provider === 'github' && stackedCreationSupported}
|
||||
showStackedMode={stackedModeAvailable}
|
||||
baseQuery={baseQuery}
|
||||
setBaseQuery={setBaseQuery}
|
||||
baseResults={baseResults}
|
||||
@@ -257,7 +257,7 @@ export function CreateHostedReviewComposer({
|
||||
type="button"
|
||||
size="xs"
|
||||
disabled={createDisabled}
|
||||
onClick={() => onPrimaryAction()}
|
||||
onClick={() => onPrimaryAction(effectiveStacked)}
|
||||
className={cn(
|
||||
'h-7 px-3 text-xs',
|
||||
showDropdown && 'rounded-r-none',
|
||||
@@ -275,7 +275,7 @@ export function CreateHostedReviewComposer({
|
||||
isCreating,
|
||||
pushBeforeCreate,
|
||||
draft,
|
||||
stacked,
|
||||
stacked: effectiveStacked,
|
||||
shortLabel: copy.shortLabel
|
||||
})}
|
||||
</span>
|
||||
|
||||
+27
-12
@@ -1,5 +1,8 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render as renderDom, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { CreateHostedReviewComposer } from './CreateHostedReviewComposer'
|
||||
import { resolveDropdownItems } from './source-control-dropdown-items'
|
||||
@@ -10,18 +13,18 @@ type RenderPullRequestComposerOptions = {
|
||||
generating?: boolean
|
||||
generateDisabled?: boolean
|
||||
generateDisabledReason?: string
|
||||
stacked?: boolean
|
||||
stackedCreationSupported?: boolean
|
||||
onPrimaryAction?: (stacked: boolean) => void
|
||||
}
|
||||
|
||||
function renderPullRequestComposer({
|
||||
function pullRequestComposerElement({
|
||||
aiGenerationEnabled = true,
|
||||
generating = false,
|
||||
generateDisabled = false,
|
||||
generateDisabledReason,
|
||||
stacked = false,
|
||||
stackedCreationSupported = true
|
||||
}: RenderPullRequestComposerOptions = {}): string {
|
||||
stackedCreationSupported = true,
|
||||
onPrimaryAction = vi.fn()
|
||||
}: RenderPullRequestComposerOptions = {}): React.JSX.Element {
|
||||
const sourceControlInputs = {
|
||||
stagedCount: 1,
|
||||
hasUnstagedChanges: false,
|
||||
@@ -35,21 +38,19 @@ function renderPullRequestComposer({
|
||||
}
|
||||
const primaryAction = resolvePrimaryAction(sourceControlInputs)
|
||||
|
||||
return renderToStaticMarkup(
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<CreateHostedReviewComposer
|
||||
provider="github"
|
||||
branch="branch-login-issue"
|
||||
base="master"
|
||||
setBase={vi.fn()}
|
||||
title=""
|
||||
title="Ready to create"
|
||||
setTitle={vi.fn()}
|
||||
body=""
|
||||
setBody={vi.fn()}
|
||||
draft={false}
|
||||
setDraft={vi.fn()}
|
||||
stacked={stacked}
|
||||
setStacked={vi.fn()}
|
||||
stackedCreationSupported={stackedCreationSupported}
|
||||
baseQuery=""
|
||||
setBaseQuery={vi.fn()}
|
||||
@@ -67,13 +68,17 @@ function renderPullRequestComposer({
|
||||
dropdownItems={resolveDropdownItems(sourceControlInputs)}
|
||||
onGenerate={vi.fn()}
|
||||
onCancelGenerate={vi.fn()}
|
||||
onPrimaryAction={vi.fn()}
|
||||
onPrimaryAction={onPrimaryAction}
|
||||
onDropdownAction={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function renderPullRequestComposer(options: RenderPullRequestComposerOptions = {}): string {
|
||||
return renderToStaticMarkup(pullRequestComposerElement(options))
|
||||
}
|
||||
|
||||
function elementByLabel(markup: string, tagName: string, label: string): string {
|
||||
const element = [...markup.matchAll(new RegExp(`<${tagName}\\b[\\s\\S]*?</${tagName}>`, 'g'))]
|
||||
.map((match) => match[0])
|
||||
@@ -87,6 +92,8 @@ function elementByLabel(markup: string, tagName: string, label: string): string
|
||||
}
|
||||
|
||||
describe('CreateHostedReviewComposer generate tooltip', () => {
|
||||
afterEach(cleanup)
|
||||
|
||||
it('renders hosted review labels without leaking interpolation placeholders', () => {
|
||||
const markup = renderPullRequestComposer()
|
||||
|
||||
@@ -131,13 +138,21 @@ describe('CreateHostedReviewComposer generate tooltip', () => {
|
||||
})
|
||||
|
||||
it('shows the parent-child preview and stacked create action', () => {
|
||||
const markup = renderPullRequestComposer({ stacked: true })
|
||||
const onPrimaryAction = vi.fn()
|
||||
const { container } = renderDom(pullRequestComposerElement({ onPrimaryAction }))
|
||||
|
||||
fireEvent.click(screen.getByText('Stacked PR'))
|
||||
|
||||
const markup = container.innerHTML
|
||||
|
||||
expect(markup).toContain('Stacked PR')
|
||||
expect(markup).toContain('master')
|
||||
expect(markup).toContain('branch-login-issue')
|
||||
expect(markup).toContain('The base branch must have an open PR and be the top of its stack.')
|
||||
expect(markup).toContain('Create stacked PR')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Create stacked PR/ }))
|
||||
expect(onPrimaryAction).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('hides stacked creation when the executing host lacks the capability', () => {
|
||||
|
||||
@@ -3021,8 +3021,6 @@ function SourceControlInner(): React.JSX.Element {
|
||||
setBody: setPrBody,
|
||||
draft: prDraft,
|
||||
setDraft: setPrDraft,
|
||||
stacked: prStacked,
|
||||
setStacked: setPrStacked,
|
||||
stackedCreationSupported: prStackedCreationSupported,
|
||||
baseQuery: prBaseQuery,
|
||||
setBaseQuery: setPrBaseQuery,
|
||||
@@ -3284,168 +3282,170 @@ function SourceControlInner(): React.JSX.Element {
|
||||
worktreePath
|
||||
])
|
||||
|
||||
const handleCreatePullRequest = useCallback(async (): Promise<void> => {
|
||||
if (
|
||||
!activeRepo ||
|
||||
!activeWorktreeId ||
|
||||
!worktreePath ||
|
||||
!hostedReviewCreation ||
|
||||
prGenerating ||
|
||||
createPrInFlightRef.current[activeWorktreeId]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hostedReviewCreation.canCreate) {
|
||||
// Why: blocked Create Review clicks are intentional; the inline notice tells the user which prerequisite to clear.
|
||||
const message = resolveBlockedCreateReviewNoticeMessage(hostedReviewCreation)
|
||||
if (message) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message
|
||||
})
|
||||
const handleCreatePullRequest = useCallback(
|
||||
async (stacked = false): Promise<void> => {
|
||||
if (
|
||||
!activeRepo ||
|
||||
!activeWorktreeId ||
|
||||
!worktreePath ||
|
||||
!hostedReviewCreation ||
|
||||
prGenerating ||
|
||||
createPrInFlightRef.current[activeWorktreeId]
|
||||
) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const base = stripBaseRef(prBase).trim()
|
||||
const title = prTitle.trim()
|
||||
|
||||
if (!title) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message: translate(
|
||||
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
|
||||
'Enter a {{value0}} title.',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branchName).toLowerCase()) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message: translate(
|
||||
'auto.components.right.sidebar.SourceControl.ae743199cd',
|
||||
'Choose a different base branch before creating a {{value0}}.',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
createPrInFlightRef.current[activeWorktreeId] = true
|
||||
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: true }))
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
|
||||
try {
|
||||
const createInput = {
|
||||
repoId: activeRepo.id,
|
||||
provider: hostedReviewCreateProvider,
|
||||
base,
|
||||
head: normalizeHostedReviewHeadRef(branchName),
|
||||
title,
|
||||
body: prBody,
|
||||
draft: prDraft,
|
||||
worktreePath,
|
||||
useTemplate: resolvedPrCreationDefaults.useTemplate
|
||||
}
|
||||
const result = prStacked
|
||||
? await createStackedHostedReview(activeRepo.path, createInput)
|
||||
: await createHostedReview(activeRepo.path, createInput)
|
||||
|
||||
if (result.ok) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
|
||||
await handlePullRequestCreated({
|
||||
provider: hostedReviewCreateProvider,
|
||||
number: result.number,
|
||||
url: result.url
|
||||
})
|
||||
if (resolvedPrCreationDefaults.openAfterCreate) {
|
||||
window.api.shell.openUrl(result.url)
|
||||
if (!hostedReviewCreation.canCreate) {
|
||||
// Why: blocked Create Review clicks are intentional; the inline notice tells the user which prerequisite to clear.
|
||||
const message = resolveBlockedCreateReviewNoticeMessage(hostedReviewCreation)
|
||||
if (message) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ('existingReview' in result && result.existingReview?.url) {
|
||||
const number = result.existingReview.number
|
||||
toast.success(
|
||||
number
|
||||
? translate(
|
||||
'auto.components.right.sidebar.SourceControl.eef5446523',
|
||||
'{{value0}} #{{value1}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControl.d6fb1df5fe',
|
||||
'{{value0}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel }
|
||||
),
|
||||
{
|
||||
action: {
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.SourceControl.812cb992ee',
|
||||
'Open on {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.providerName }
|
||||
),
|
||||
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (number) {
|
||||
const base = stripBaseRef(prBase).trim()
|
||||
const title = prTitle.trim()
|
||||
|
||||
if (!title) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message: translate(
|
||||
'auto.components.right.sidebar.SourceControl.f3a8b2c1d0e5',
|
||||
'Enter a {{value0}} title.',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!base || stripBaseRef(base).toLowerCase() === stripBaseRef(branchName).toLowerCase()) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message: translate(
|
||||
'auto.components.right.sidebar.SourceControl.ae743199cd',
|
||||
'Choose a different base branch before creating a {{value0}}.',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
createPrInFlightRef.current[activeWorktreeId] = true
|
||||
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: true }))
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
|
||||
try {
|
||||
const createInput = {
|
||||
repoId: activeRepo.id,
|
||||
provider: hostedReviewCreateProvider,
|
||||
base,
|
||||
head: normalizeHostedReviewHeadRef(branchName),
|
||||
title,
|
||||
body: prBody,
|
||||
draft: prDraft,
|
||||
worktreePath,
|
||||
useTemplate: resolvedPrCreationDefaults.useTemplate
|
||||
}
|
||||
const result = stacked
|
||||
? await createStackedHostedReview(activeRepo.path, createInput)
|
||||
: await createHostedReview(activeRepo.path, createInput)
|
||||
|
||||
if (result.ok) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
|
||||
await handlePullRequestCreated({
|
||||
provider: hostedReviewCreateProvider,
|
||||
number,
|
||||
url: result.existingReview.url
|
||||
number: result.number,
|
||||
url: result.url
|
||||
})
|
||||
if (resolvedPrCreationDefaults.openAfterCreate) {
|
||||
window.api.shell.openUrl(result.url)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message: result.error
|
||||
})
|
||||
} catch (error) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
|
||||
'Failed to create {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
createPrInFlightRef.current[activeWorktreeId] = false
|
||||
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: false }))
|
||||
}
|
||||
}, [
|
||||
activeRepo,
|
||||
activeWorktreeId,
|
||||
branchName,
|
||||
createHostedReview,
|
||||
createStackedHostedReview,
|
||||
handlePullRequestCreated,
|
||||
hostedReviewCreation,
|
||||
hostedReviewCreateCopy.providerName,
|
||||
hostedReviewCreateCopy.reviewLabel,
|
||||
hostedReviewCreateCopy.titleLabel,
|
||||
hostedReviewCreateProvider,
|
||||
prBase,
|
||||
prBody,
|
||||
prDraft,
|
||||
prGenerating,
|
||||
prStacked,
|
||||
prTitle,
|
||||
resolvedPrCreationDefaults.openAfterCreate,
|
||||
resolvedPrCreationDefaults.useTemplate,
|
||||
setCreatePrIntentNoticeForWorktree,
|
||||
worktreePath
|
||||
])
|
||||
if ('existingReview' in result && result.existingReview?.url) {
|
||||
const number = result.existingReview.number
|
||||
toast.success(
|
||||
number
|
||||
? translate(
|
||||
'auto.components.right.sidebar.SourceControl.eef5446523',
|
||||
'{{value0}} #{{value1}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel, value1: number }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControl.d6fb1df5fe',
|
||||
'{{value0}} is already open',
|
||||
{ value0: hostedReviewCreateCopy.titleLabel }
|
||||
),
|
||||
{
|
||||
action: {
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.SourceControl.812cb992ee',
|
||||
'Open on {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.providerName }
|
||||
),
|
||||
onClick: () => window.api.shell.openUrl(result.existingReview!.url)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (number) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, null)
|
||||
await handlePullRequestCreated({
|
||||
provider: hostedReviewCreateProvider,
|
||||
number,
|
||||
url: result.existingReview.url
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message: result.error
|
||||
})
|
||||
} catch (error) {
|
||||
setCreatePrIntentNoticeForWorktree(activeWorktreeId, {
|
||||
tone: 'destructive',
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControl.e2b7a1c0d9f4',
|
||||
'Failed to create {{value0}}',
|
||||
{ value0: hostedReviewCreateCopy.reviewLabel }
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
createPrInFlightRef.current[activeWorktreeId] = false
|
||||
setCreatePrInFlightByWorktree((prev) => ({ ...prev, [activeWorktreeId]: false }))
|
||||
}
|
||||
},
|
||||
[
|
||||
activeRepo,
|
||||
activeWorktreeId,
|
||||
branchName,
|
||||
createHostedReview,
|
||||
createStackedHostedReview,
|
||||
handlePullRequestCreated,
|
||||
hostedReviewCreation,
|
||||
hostedReviewCreateCopy.providerName,
|
||||
hostedReviewCreateCopy.reviewLabel,
|
||||
hostedReviewCreateCopy.titleLabel,
|
||||
hostedReviewCreateProvider,
|
||||
prBase,
|
||||
prBody,
|
||||
prDraft,
|
||||
prGenerating,
|
||||
prTitle,
|
||||
resolvedPrCreationDefaults.openAfterCreate,
|
||||
resolvedPrCreationDefaults.useTemplate,
|
||||
setCreatePrIntentNoticeForWorktree,
|
||||
worktreePath
|
||||
]
|
||||
)
|
||||
|
||||
const createHostedReviewForCreatePrIntent = useCallback(
|
||||
async (
|
||||
@@ -5826,6 +5826,7 @@ function SourceControlInner(): React.JSX.Element {
|
||||
{shouldRenderCommitArea(unresolvedConflicts.length, conflictOperation) &&
|
||||
(directCreatePrAction ? (
|
||||
<CreateHostedReviewComposer
|
||||
key={`${activeRepo?.id ?? ''}:${activeWorktreeId ?? worktreePath ?? ''}:${branchName}`}
|
||||
provider={hostedReviewCreateProvider}
|
||||
branch={branchName}
|
||||
base={prBase}
|
||||
@@ -5836,8 +5837,6 @@ function SourceControlInner(): React.JSX.Element {
|
||||
setBody={setPrBody}
|
||||
draft={prDraft}
|
||||
setDraft={setPrDraft}
|
||||
stacked={prStacked}
|
||||
setStacked={setPrStacked}
|
||||
stackedCreationSupported={prStackedCreationSupported}
|
||||
baseQuery={prBaseQuery}
|
||||
setBaseQuery={setPrBaseQuery}
|
||||
@@ -5857,8 +5856,8 @@ function SourceControlInner(): React.JSX.Element {
|
||||
dropdownItems={dropdownItems}
|
||||
onGenerate={handleGeneratePullRequestFieldsClick}
|
||||
onCancelGenerate={handleCancelGeneratePullRequestFields}
|
||||
onPrimaryAction={() => {
|
||||
void handleCreatePullRequest()
|
||||
onPrimaryAction={(stacked) => {
|
||||
void handleCreatePullRequest(stacked)
|
||||
}}
|
||||
onDropdownAction={handleActionInvoke}
|
||||
/>
|
||||
|
||||
@@ -166,7 +166,6 @@ export function useCreatePullRequestDialogFields({
|
||||
const [title, setTitle] = useState('')
|
||||
const [body, setBody] = useState('')
|
||||
const [draft, setDraft] = useState(false)
|
||||
const [stacked, setStacked] = useState(false)
|
||||
const [baseQuery, setBaseQuery] = useState('')
|
||||
const [baseResults, setBaseResults] = useState<string[]>([])
|
||||
const [baseSearchError, setBaseSearchError] = useState<string | null>(null)
|
||||
@@ -315,7 +314,6 @@ export function useCreatePullRequestDialogFields({
|
||||
setTitle(resolveCreateReviewDraftTitle({ branch, eligibilityTitle: eligibility.title }))
|
||||
setBody(eligibility.body ?? '')
|
||||
setDraft(resolvedPrDefaults.draft)
|
||||
setStacked(false)
|
||||
setBaseQuery('')
|
||||
setBaseResults([])
|
||||
setBaseSearchError(null)
|
||||
@@ -611,8 +609,6 @@ export function useCreatePullRequestDialogFields({
|
||||
draft,
|
||||
setDraft: setUserDraft,
|
||||
stackedCreationSupported: eligibility?.stackedCreationSupported === true,
|
||||
stacked: eligibility?.stackedCreationSupported === true && stacked,
|
||||
setStacked,
|
||||
fieldRevisions: fieldRevisionsRef.current,
|
||||
applyGeneratedFields,
|
||||
baseQuery,
|
||||
|
||||
Reference in New Issue
Block a user