mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat(composer): support branch defaults and compact picker
This commit is contained in:
@@ -55,12 +55,18 @@ vi.mock('@/components/new-workspace/ProjectCombobox', () => ({
|
||||
vi.mock('@/components/repo/CreateFromPicker', () => ({
|
||||
CreateFromPicker: ({
|
||||
value,
|
||||
onValueChange
|
||||
onValueChange,
|
||||
readOnly
|
||||
}: {
|
||||
value: string
|
||||
onValueChange: (next: string) => void
|
||||
readOnly?: boolean
|
||||
}) => (
|
||||
<div data-testid="base-ref-picker" data-value={value}>
|
||||
<div
|
||||
data-testid="base-ref-picker"
|
||||
data-value={value}
|
||||
data-readonly={readOnly ? 'true' : 'false'}
|
||||
>
|
||||
<button type="button" onClick={() => onValueChange('release/1.2')}>
|
||||
Pick release
|
||||
</button>
|
||||
@@ -174,6 +180,14 @@ describe('NewWorkspaceComposerCard start from', () => {
|
||||
expect(picks).toEqual(['release/1.2'])
|
||||
})
|
||||
|
||||
it('renders the base ref as read-only while the project needs a connection', () => {
|
||||
container = renderCard({ selectedRepoRequiresConnection: true })
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="base-ref-picker"]')?.getAttribute('data-readonly')
|
||||
).toBe('true')
|
||||
})
|
||||
|
||||
it('reports the project default as no base at all', () => {
|
||||
const picks: (string | undefined)[] = []
|
||||
container = renderCard({
|
||||
|
||||
@@ -377,7 +377,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
'[data-contextual-tour-target="workspace-creation-name"]'
|
||||
)
|
||||
expect(projectSection?.textContent).not.toContain('Task Source')
|
||||
expect(nameSection?.textContent).toContain("Name or 'Create From'")
|
||||
expect(nameSection?.textContent).toContain('Create From')
|
||||
expect(
|
||||
current.container
|
||||
.querySelector('[aria-label="workspace name"]')
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from 'react'
|
||||
import { CreateFromPicker } from '@/components/repo/CreateFromPicker'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useRepoMap, useWorktreesForRepo } from '@/store/selectors'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
type ComposerBaseRefPickerProps = {
|
||||
repoId: string
|
||||
baseBranch: string | undefined
|
||||
onBaseBranchChange: (value: string | undefined) => void
|
||||
resetHint: string | null | undefined
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,12 +22,15 @@ export function ComposerBaseRefPicker({
|
||||
repoId,
|
||||
baseBranch,
|
||||
onBaseBranchChange,
|
||||
resetHint
|
||||
resetHint,
|
||||
readOnly = false
|
||||
}: ComposerBaseRefPickerProps): React.JSX.Element {
|
||||
const repoMap = useRepoMap()
|
||||
const repo = repoMap.get(repoId)
|
||||
const repoWorktrees = useWorktreesForRepo(repoId)
|
||||
const updateRepo = useAppStore((state) => state.updateRepo)
|
||||
return (
|
||||
<div className="space-y-1 pt-1">
|
||||
<div className="space-y-1">
|
||||
<CreateFromPicker
|
||||
// Why: branch search state is repo-scoped, so a project switch must drop it before the next paint.
|
||||
key={repoId}
|
||||
@@ -32,7 +38,20 @@ export function ComposerBaseRefPicker({
|
||||
repoMap={repoMap}
|
||||
worktrees={repoWorktrees}
|
||||
value={baseBranch ?? ''}
|
||||
compact
|
||||
readOnly={readOnly}
|
||||
onValueChange={(nextBaseBranch) => onBaseBranchChange(nextBaseBranch || undefined)}
|
||||
onSetDefault={
|
||||
readOnly
|
||||
? undefined
|
||||
: async (nextBaseBranch) => {
|
||||
await updateRepo(
|
||||
repoId,
|
||||
{ worktreeBaseRef: nextBaseBranch },
|
||||
repo ? { hostId: getRepoExecutionHostId(repo) } : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
/>
|
||||
{resetHint ? <p className="text-[11px] text-muted-foreground">{resetHint}</p> : null}
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,7 @@ type NewWorkspaceComposerNameSectionProps = Pick<
|
||||
| 'allowSmartNameAddProject'
|
||||
| 'smartNameRepoSwitchTarget'
|
||||
| 'onSmartNameModeChange'
|
||||
| 'smartNameMode'
|
||||
| 'forkPushWarning'
|
||||
| 'canReuseSelectedBranch'
|
||||
| 'reuseSelectedBranch'
|
||||
@@ -69,6 +70,7 @@ export function NewWorkspaceComposerNameSection({
|
||||
allowSmartNameAddProject = true,
|
||||
smartNameRepoSwitchTarget = 'project',
|
||||
onSmartNameModeChange,
|
||||
smartNameMode,
|
||||
onNamePlainEnter,
|
||||
forkPushWarning,
|
||||
canReuseSelectedBranch,
|
||||
@@ -83,21 +85,33 @@ export function NewWorkspaceComposerNameSection({
|
||||
shouldShowComposerBaseRefPicker({
|
||||
selectedRepoIsGit,
|
||||
branchesEnabled,
|
||||
smartNameMode: smartNameMode ?? 'smart',
|
||||
smartNameSelectionKind: smartNameSelection?.kind ?? null
|
||||
})
|
||||
return (
|
||||
<div className="min-w-0 space-y-1" data-contextual-tour-target="workspace-creation-name">
|
||||
<label className="block min-w-0 truncate text-xs font-medium text-muted-foreground">
|
||||
{selectedRepoIsGit
|
||||
? translate(
|
||||
'auto.components.NewWorkspaceComposerCard.ac3748dcda',
|
||||
"Name or 'Create From'"
|
||||
)
|
||||
: translate('auto.components.NewWorkspaceComposerCard.0ee17638fe', 'Workspace name')}{' '}
|
||||
<span className="text-muted-foreground/70">
|
||||
{translate('auto.components.NewWorkspaceComposerCard.0c5d6a479c', '[Optional]')}
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label className="min-w-0 truncate text-xs font-medium text-muted-foreground">
|
||||
{selectedRepoIsGit
|
||||
? translate('auto.components.NewWorkspaceComposerCard.ac3748dcda', 'Create From')
|
||||
: translate(
|
||||
'auto.components.NewWorkspaceComposerCard.0ee17638fe',
|
||||
'Workspace name'
|
||||
)}{' '}
|
||||
<span className="text-muted-foreground/70">
|
||||
{translate('auto.components.NewWorkspaceComposerCard.0c5d6a479c', '[Optional]')}
|
||||
</span>
|
||||
</label>
|
||||
{showBaseRefPicker && onBaseBranchChange ? (
|
||||
<ComposerBaseRefPicker
|
||||
repoId={repoId}
|
||||
baseBranch={baseBranch}
|
||||
onBaseBranchChange={onBaseBranchChange}
|
||||
resetHint={startFromResetHint}
|
||||
readOnly={selectedRepoRequiresConnection}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<SmartWorkspaceNameField
|
||||
inputRef={nameInputRef}
|
||||
repos={eligibleRepos}
|
||||
@@ -135,14 +149,6 @@ export function NewWorkspaceComposerNameSection({
|
||||
<span>{forkPushWarning}</span>
|
||||
</p>
|
||||
) : null}
|
||||
{showBaseRefPicker && onBaseBranchChange ? (
|
||||
<ComposerBaseRefPicker
|
||||
repoId={repoId}
|
||||
baseBranch={baseBranch}
|
||||
onBaseBranchChange={onBaseBranchChange}
|
||||
resetHint={startFromResetHint}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
|
||||
|
||||
@@ -5,20 +5,38 @@ const gitRepo = { selectedRepoIsGit: true, branchesEnabled: true }
|
||||
|
||||
describe('shouldShowComposerBaseRefPicker', () => {
|
||||
it('offers a base ref when no source is selected yet', () => {
|
||||
expect(shouldShowComposerBaseRefPicker({ ...gitRepo, smartNameSelectionKind: null })).toBe(true)
|
||||
expect(
|
||||
shouldShowComposerBaseRefPicker({
|
||||
...gitRepo,
|
||||
smartNameMode: 'smart',
|
||||
smartNameSelectionKind: null
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['github-issue', 'gitlab-issue', 'linear', 'jira'] as const)(
|
||||
'offers a base ref for a %s source',
|
||||
(smartNameSelectionKind) => {
|
||||
expect(shouldShowComposerBaseRefPicker({ ...gitRepo, smartNameSelectionKind })).toBe(true)
|
||||
expect(
|
||||
shouldShowComposerBaseRefPicker({
|
||||
...gitRepo,
|
||||
smartNameMode: 'smart',
|
||||
smartNameSelectionKind
|
||||
})
|
||||
).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['github-pr', 'gitlab-mr', 'branch'] as const)(
|
||||
'hides the base ref for a %s source that carries its own base',
|
||||
(smartNameSelectionKind) => {
|
||||
expect(shouldShowComposerBaseRefPicker({ ...gitRepo, smartNameSelectionKind })).toBe(false)
|
||||
expect(
|
||||
shouldShowComposerBaseRefPicker({
|
||||
...gitRepo,
|
||||
smartNameMode: 'smart',
|
||||
smartNameSelectionKind
|
||||
})
|
||||
).toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,6 +45,7 @@ describe('shouldShowComposerBaseRefPicker', () => {
|
||||
shouldShowComposerBaseRefPicker({
|
||||
selectedRepoIsGit: false,
|
||||
branchesEnabled: true,
|
||||
smartNameMode: 'smart',
|
||||
smartNameSelectionKind: 'jira'
|
||||
})
|
||||
).toBe(false)
|
||||
@@ -37,8 +56,19 @@ describe('shouldShowComposerBaseRefPicker', () => {
|
||||
shouldShowComposerBaseRefPicker({
|
||||
selectedRepoIsGit: true,
|
||||
branchesEnabled: false,
|
||||
smartNameMode: 'smart',
|
||||
smartNameSelectionKind: 'jira'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('hides the base ref while the Branch tab is active before selection', () => {
|
||||
expect(
|
||||
shouldShowComposerBaseRefPicker({
|
||||
...gitRepo,
|
||||
smartNameMode: 'branches',
|
||||
smartNameSelectionKind: null
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SmartWorkspaceNameSelection } from '@/components/new-workspace/SmartWorkspaceNameField'
|
||||
import type { SmartNameMode } from './smart-workspace-source-results'
|
||||
|
||||
/**
|
||||
* Whether the composer offers a base ref for the worktree it is about to create.
|
||||
@@ -10,11 +11,15 @@ import type { SmartWorkspaceNameSelection } from '@/components/new-workspace/Sma
|
||||
export function shouldShowComposerBaseRefPicker(args: {
|
||||
selectedRepoIsGit: boolean
|
||||
branchesEnabled: boolean
|
||||
smartNameMode: SmartNameMode
|
||||
smartNameSelectionKind: SmartWorkspaceNameSelection['kind'] | null
|
||||
}): boolean {
|
||||
if (!args.selectedRepoIsGit || !args.branchesEnabled) {
|
||||
return false
|
||||
}
|
||||
if (args.smartNameMode === 'branches') {
|
||||
return false
|
||||
}
|
||||
// Why: these already carry a base — a PR/MR pins its own head, and a branch pick IS the base,
|
||||
// where overriding would silently turn a checkout of it into a new branch off something else.
|
||||
return (
|
||||
|
||||
@@ -74,6 +74,7 @@ export type NewWorkspaceComposerCardProps = {
|
||||
onSmartGitLabItemSelect: (item: GitLabWorkItem) => void
|
||||
onSmartBranchSelect: (refName: string, localBranchName: string) => void
|
||||
onSmartNameModeChange?: (mode: SmartNameMode) => void
|
||||
smartNameMode?: SmartNameMode
|
||||
onSmartLinearIssueSelect: (issue: LinearIssue) => void
|
||||
onSmartJiraIssueSelect?: (issue: JiraIssue, sourceContext: TaskSourceContext) => void
|
||||
onOpenJiraSettings?: () => void
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { CreateFromPicker } from './CreateFromPicker'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import {
|
||||
getRuntimeRepoBaseRefDefault,
|
||||
searchRuntimeRepoBaseRefs
|
||||
@@ -74,13 +75,15 @@ function makeRepo(overrides: Partial<Repo>): Repo {
|
||||
async function renderPicker(repo: Repo): Promise<void> {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<CreateFromPicker
|
||||
repoId={repo.id}
|
||||
repoMap={repoMapFor(repo)}
|
||||
worktrees={[]}
|
||||
value=""
|
||||
onValueChange={vi.fn()}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<CreateFromPicker
|
||||
repoId={repo.id}
|
||||
repoMap={repoMapFor(repo)}
|
||||
worktrees={[]}
|
||||
value=""
|
||||
onValueChange={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -109,7 +112,8 @@ describe('CreateFromPicker host routing', () => {
|
||||
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith(
|
||||
{ activeRuntimeEnvironmentId: 'owner-runtime' },
|
||||
repo.id
|
||||
repo.id,
|
||||
'runtime:owner-runtime'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -131,7 +135,8 @@ describe('CreateFromPicker host routing', () => {
|
||||
{ activeRuntimeEnvironmentId: null },
|
||||
repo.id,
|
||||
'',
|
||||
30
|
||||
30,
|
||||
'local'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -143,7 +148,8 @@ describe('CreateFromPicker host routing', () => {
|
||||
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith(
|
||||
{ activeRuntimeEnvironmentId: null },
|
||||
repo.id
|
||||
repo.id,
|
||||
'local'
|
||||
)
|
||||
expect(searchRuntimeRepoBaseRefs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Check, ChevronsUpDown, GitBranch, Star } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
@@ -8,10 +8,17 @@ import {
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from '@/components/ui/command'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger
|
||||
} from '@/components/ui/context-menu'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getRuntimeEnvironmentIdForRepo } from '@/lib/repo-runtime-owner'
|
||||
import {
|
||||
@@ -20,6 +27,7 @@ import {
|
||||
} from '@/runtime/runtime-repo-client'
|
||||
import { isRuntimeRepoRefSearchQueryWithinLimit } from '@/runtime/runtime-repo-search-bounds'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { FilePathCursorTooltip } from '@/components/file-path-cursor-tooltip'
|
||||
|
||||
const DEFAULT_VALUE = '__project_default__'
|
||||
|
||||
@@ -33,14 +41,20 @@ export function CreateFromPicker({
|
||||
worktrees,
|
||||
value,
|
||||
triggerClassName,
|
||||
onValueChange
|
||||
compact = false,
|
||||
readOnly = false,
|
||||
onValueChange,
|
||||
onSetDefault
|
||||
}: {
|
||||
repoId: string
|
||||
repoMap: Map<string, Repo>
|
||||
worktrees: Worktree[]
|
||||
value: string
|
||||
triggerClassName?: string
|
||||
compact?: boolean
|
||||
readOnly?: boolean
|
||||
onValueChange: (baseBranch: string) => void
|
||||
onSetDefault?: (baseBranch: string) => void | Promise<void>
|
||||
}): React.JSX.Element {
|
||||
// Per-repo evidence, not the ambient active-runtime setting; the base-ref helpers
|
||||
// just take a settings-shaped object, so it is synthesized at each call below.
|
||||
@@ -48,6 +62,7 @@ export function CreateFromPicker({
|
||||
getRuntimeEnvironmentIdForRepo(state, repoId)
|
||||
)
|
||||
const repo = repoMap.get(repoId)
|
||||
const repoHostId = repo ? getRepoExecutionHostId(repo) : undefined
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null)
|
||||
const focusFrameRef = React.useRef<number | null>(null)
|
||||
@@ -57,8 +72,13 @@ export function CreateFromPicker({
|
||||
const [isSearching, setIsSearching] = React.useState(false)
|
||||
const effectiveDefault = repo?.worktreeBaseRef ?? defaultBaseRef
|
||||
const selectedValue = value || DEFAULT_VALUE
|
||||
const projectDefaultLabel = translate(
|
||||
'auto.components.automations.CreateFromPicker.ef6d762538',
|
||||
'Project default'
|
||||
)
|
||||
const selectedLabel =
|
||||
value || (effectiveDefault ? `${effectiveDefault} (default)` : 'Project default')
|
||||
value || (effectiveDefault ? `${effectiveDefault} (default)` : projectDefaultLabel)
|
||||
const compactLabel = value || effectiveDefault || projectDefaultLabel
|
||||
const branchOptions = React.useMemo(() => {
|
||||
const options = new Set<string>()
|
||||
if (effectiveDefault) {
|
||||
@@ -76,6 +96,34 @@ export function CreateFromPicker({
|
||||
return Array.from(options).sort((left, right) => left.localeCompare(right))
|
||||
}, [effectiveDefault, searchResults, worktrees])
|
||||
|
||||
const renderBranchContextMenu = React.useCallback(
|
||||
(branch: string, row: React.ReactNode): React.ReactNode => {
|
||||
if (!onSetDefault || !branch) {
|
||||
return row
|
||||
}
|
||||
const isDefault = branch === effectiveDefault
|
||||
return (
|
||||
<ContextMenu key={branch}>
|
||||
<ContextMenuTrigger asChild>{row}</ContextMenuTrigger>
|
||||
<ContextMenuContent className="z-[70]">
|
||||
<ContextMenuItem
|
||||
disabled={isDefault}
|
||||
onSelect={() => {
|
||||
void onSetDefault(branch)
|
||||
}}
|
||||
>
|
||||
<Star className="size-3.5" />
|
||||
{isDefault
|
||||
? translate('auto.components.agent.AgentCombobox.1b0d6965fa', 'Current default')
|
||||
: translate('auto.components.agent.AgentCombobox.9c6b59fe58', 'Set as default')}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
},
|
||||
[effectiveDefault, onSetDefault]
|
||||
)
|
||||
|
||||
const cancelFocusFrame = React.useCallback((): void => {
|
||||
if (focusFrameRef.current !== null) {
|
||||
cancelAnimationFrame(focusFrameRef.current)
|
||||
@@ -119,7 +167,8 @@ export function CreateFromPicker({
|
||||
setDefaultBaseRef(null)
|
||||
void getRuntimeRepoBaseRefDefault(
|
||||
{ activeRuntimeEnvironmentId: repoRuntimeEnvironmentId },
|
||||
repoId
|
||||
repoId,
|
||||
repoHostId
|
||||
)
|
||||
.then((result) => {
|
||||
if (!stale) {
|
||||
@@ -134,7 +183,7 @@ export function CreateFromPicker({
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [repoRuntimeEnvironmentId, repoId])
|
||||
}, [repoHostId, repoRuntimeEnvironmentId, repoId])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isRuntimeRepoRefSearchQueryWithinLimit(query)) {
|
||||
@@ -158,7 +207,8 @@ export function CreateFromPicker({
|
||||
{ activeRuntimeEnvironmentId: repoRuntimeEnvironmentId },
|
||||
repoId,
|
||||
trimmedQuery,
|
||||
30
|
||||
30,
|
||||
repoHostId
|
||||
)
|
||||
.then((results) => {
|
||||
if (!stale) {
|
||||
@@ -181,108 +231,154 @@ export function CreateFromPicker({
|
||||
stale = true
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [repoRuntimeEnvironmentId, open, query, repoId])
|
||||
}, [repoHostId, repoRuntimeEnvironmentId, open, query, repoId])
|
||||
|
||||
const trigger = compact ? (
|
||||
<button
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-expanded={!readOnly && open}
|
||||
aria-readonly={readOnly || undefined}
|
||||
aria-label={translate(
|
||||
'auto.components.automations.CreateFromPicker.dd3841b442',
|
||||
'Branch from'
|
||||
)}
|
||||
title={
|
||||
readOnly
|
||||
? translate(
|
||||
'auto.components.NewWorkspaceComposerCard.connectProjectFirst',
|
||||
'Connect this project first'
|
||||
)
|
||||
: translate('auto.components.automations.CreateFromPicker.dd3841b442', 'Branch from')
|
||||
}
|
||||
className={cn(
|
||||
'inline-flex h-6 max-w-44 items-center gap-1 rounded-md border border-border bg-muted/30 px-1.5 text-[11px] text-muted-foreground',
|
||||
readOnly
|
||||
? 'cursor-default opacity-70'
|
||||
: 'cursor-pointer hover:bg-accent hover:text-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
||||
triggerClassName
|
||||
)}
|
||||
>
|
||||
<GitBranch className="size-3 shrink-0" aria-hidden="true" />
|
||||
<FilePathCursorTooltip path={compactLabel}>
|
||||
<span className="min-w-0 truncate font-mono">{compactLabel}</span>
|
||||
</FilePathCursorTooltip>
|
||||
{!readOnly ? <ChevronsUpDown className="size-3 shrink-0 opacity-60" /> : null}
|
||||
</button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn('h-9 w-full justify-between px-3 text-sm font-normal', triggerClassName)}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="shrink-0 text-muted-foreground">
|
||||
{translate('auto.components.automations.CreateFromPicker.dd3841b442', 'Branch from')}
|
||||
</span>
|
||||
<span className="truncate">{selectedLabel}</span>
|
||||
</span>
|
||||
<ChevronsUpDown className="size-4 opacity-50" />
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn('h-9 w-full justify-between px-3 text-sm font-normal', triggerClassName)}
|
||||
{readOnly ? (
|
||||
trigger
|
||||
) : (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[var(--radix-popover-trigger-width)] min-w-[18rem] p-0"
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
focusSearchInput()
|
||||
}}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="shrink-0 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.automations.CreateFromPicker.dd3841b442',
|
||||
'Branch from'
|
||||
<Command>
|
||||
<CommandInput
|
||||
ref={setInputNode}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
placeholder={translate(
|
||||
'auto.components.automations.CreateFromPicker.f061f49e3f',
|
||||
'Search repo branches...'
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate">{selectedLabel}</span>
|
||||
</span>
|
||||
<ChevronsUpDown className="size-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[var(--radix-popover-trigger-width)] min-w-[18rem] p-0"
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
focusSearchInput()
|
||||
}}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
ref={setInputNode}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
placeholder={translate(
|
||||
'auto.components.automations.CreateFromPicker.f061f49e3f',
|
||||
'Search repo branches...'
|
||||
)}
|
||||
/>
|
||||
<CommandList className="max-h-72">
|
||||
<CommandEmpty>
|
||||
{isSearching
|
||||
? translate(
|
||||
'auto.components.automations.CreateFromPicker.9ce96621f4',
|
||||
'Searching branches...'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.automations.CreateFromPicker.79512f22a7',
|
||||
'No branches found.'
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandItem
|
||||
value={effectiveDefault ? `${effectiveDefault} default` : 'project default'}
|
||||
onSelect={() => {
|
||||
onValueChange('')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'size-4',
|
||||
selectedValue === DEFAULT_VALUE ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">
|
||||
{effectiveDefault
|
||||
/>
|
||||
<CommandList className="max-h-72">
|
||||
<CommandEmpty>
|
||||
{isSearching
|
||||
? translate(
|
||||
'auto.components.automations.CreateFromPicker.e53d306056',
|
||||
'{{value0}} (default)',
|
||||
{ value0: effectiveDefault }
|
||||
'auto.components.automations.CreateFromPicker.9ce96621f4',
|
||||
'Searching branches...'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.automations.CreateFromPicker.ef6d762538',
|
||||
'Project default'
|
||||
'auto.components.automations.CreateFromPicker.79512f22a7',
|
||||
'No branches found.'
|
||||
)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
{branchOptions
|
||||
.filter((branch) => branch !== effectiveDefault)
|
||||
.map((branch) => (
|
||||
</CommandEmpty>
|
||||
{renderBranchContextMenu(
|
||||
effectiveDefault ?? '',
|
||||
<CommandItem
|
||||
key={branch}
|
||||
value={branch}
|
||||
value={effectiveDefault ? `${effectiveDefault} default` : 'project default'}
|
||||
onSelect={() => {
|
||||
onValueChange(branch)
|
||||
onValueChange('')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn('size-4', value === branch ? 'opacity-100' : 'opacity-0')}
|
||||
className={cn(
|
||||
'size-4',
|
||||
selectedValue === DEFAULT_VALUE ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{branch}</span>
|
||||
<FilePathCursorTooltip path={effectiveDefault ?? projectDefaultLabel}>
|
||||
<span className="min-w-0 truncate">
|
||||
{effectiveDefault
|
||||
? translate(
|
||||
'auto.components.automations.CreateFromPicker.e53d306056',
|
||||
'{{value0}} (default)',
|
||||
{ value0: effectiveDefault }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.automations.CreateFromPicker.ef6d762538',
|
||||
'Project default'
|
||||
)}
|
||||
</span>
|
||||
</FilePathCursorTooltip>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{branchOptions
|
||||
.filter((branch) => branch !== effectiveDefault)
|
||||
.map((branch) =>
|
||||
renderBranchContextMenu(
|
||||
branch,
|
||||
<CommandItem
|
||||
key={branch}
|
||||
value={branch}
|
||||
onSelect={() => {
|
||||
onValueChange(branch)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn('size-4', value === branch ? 'opacity-100' : 'opacity-0')}
|
||||
/>
|
||||
<FilePathCursorTooltip path={branch}>
|
||||
<span className="min-w-0 truncate">{branch}</span>
|
||||
</FilePathCursorTooltip>
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export type ComposerCardSourceProps = Pick<
|
||||
| 'selectedRepoExecutionHostId'
|
||||
| 'selectedRepoProjectId'
|
||||
| 'smartNameSelection'
|
||||
| 'smartNameMode'
|
||||
| 'reuseSelectedBranch'
|
||||
| 'createMultiple'
|
||||
| 'agentPrompt'
|
||||
|
||||
@@ -111,6 +111,7 @@ export function buildComposerCardProps(state: ComposerModel) {
|
||||
shouldWaitForSetupCheck,
|
||||
workspaceSeedName,
|
||||
smartNameJiraSourceContext,
|
||||
smartNameMode,
|
||||
smartNameSelection,
|
||||
sparsePresets,
|
||||
sparseSelectedPresetId,
|
||||
@@ -174,6 +175,7 @@ export function buildComposerCardProps(state: ComposerModel) {
|
||||
onOpenJiraSettings: handleOpenJiraSettings,
|
||||
smartNameGitHubSourceContext: selectedRepoGitHubSourceContext,
|
||||
smartNameJiraSourceContext,
|
||||
smartNameMode,
|
||||
smartNameSelection,
|
||||
onClearSmartNameSelection: handleClearSmartNameSelection,
|
||||
canReuseSelectedBranch:
|
||||
|
||||
@@ -1601,7 +1601,7 @@
|
||||
"326a578923": "orca.yaml + local",
|
||||
"2132b670da": "both",
|
||||
"0e587e31fb": "yaml",
|
||||
"ac3748dcda": "Name or 'Create From'",
|
||||
"ac3748dcda": "Create From",
|
||||
"f660aa1454": "Connecting",
|
||||
"7711ad5122": "Local setup command",
|
||||
"e5db1b0419": "Combined setup command",
|
||||
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"326a578923": "orca.yaml + local",
|
||||
"2132b670da": "ambos",
|
||||
"0e587e31fb": "yaml",
|
||||
"ac3748dcda": "Nombre o 'Crear desde'",
|
||||
"ac3748dcda": "Crear desde",
|
||||
"f660aa1454": "Conectando",
|
||||
"7711ad5122": "Comando de configuración local",
|
||||
"e5db1b0419": "Comando de configuración combinado",
|
||||
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"326a578923": "orca.yaml + ローカル",
|
||||
"2132b670da": "両方",
|
||||
"0e587e31fb": "yaml",
|
||||
"ac3748dcda": "名前または「作成元」",
|
||||
"ac3748dcda": "作成元",
|
||||
"f660aa1454": "接続中",
|
||||
"7711ad5122": "ローカルセットアップコマンド",
|
||||
"e5db1b0419": "組み合わせセットアップコマンド",
|
||||
|
||||
@@ -1317,7 +1317,7 @@
|
||||
"326a578923": "orca.yaml + 로컬",
|
||||
"2132b670da": "둘 다",
|
||||
"0e587e31fb": "yaml",
|
||||
"ac3748dcda": "이름 또는 '다음에서 만들기'",
|
||||
"ac3748dcda": "다음에서 만들기",
|
||||
"f660aa1454": "연결 중",
|
||||
"7711ad5122": "로컬 설정 명령",
|
||||
"e5db1b0419": "결합된 설정 명령",
|
||||
|
||||
@@ -1315,7 +1315,7 @@
|
||||
"326a578923": "orca.yaml + 本地",
|
||||
"2132b670da": "两者",
|
||||
"0e587e31fb": "yaml",
|
||||
"ac3748dcda": "名称或创建来源",
|
||||
"ac3748dcda": "创建来源",
|
||||
"f660aa1454": "连接中",
|
||||
"7711ad5122": "本地设置命令",
|
||||
"e5db1b0419": "组合设置命令",
|
||||
|
||||
Reference in New Issue
Block a user