mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(new-workspace): keep workspace creation reachable with zero projects (#15234)
The sidebar +, the landing Create button, the board lane +, the palette create row, and the tour CTA all disabled themselves when repos was empty — a dead end, since the composer's project field can add the first project inline and auto-select it. Drop the repo-count gate from every entry point. Submitting without a project still shows the inline "Choose or add a project" error.
This commit is contained in:
@@ -231,7 +231,7 @@ export default function Landing(): React.JSX.Element {
|
||||
|
||||
const createTargetLabel =
|
||||
repos.length > 0 && repos.every((repo) => isGitRepoKind(repo)) ? 'Worktree' : 'Workspace'
|
||||
const canCreateWorktree = repos.length > 0
|
||||
const hasProjects = repos.length > 0
|
||||
const hasGitHubProject = useMemo(() => hasGitHubBackedProject(repos), [repos])
|
||||
const showGitHubSupportFooter = repos.length === 0 || hasGitHubProject
|
||||
|
||||
@@ -274,7 +274,7 @@ export default function Landing(): React.JSX.Element {
|
||||
{preflightIssues.length > 0 && <PreflightBanner issues={preflightIssues} repos={repos} />}
|
||||
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
{canCreateWorktree
|
||||
{hasProjects
|
||||
? translate(
|
||||
'auto.components.Landing.9c00bd4adf',
|
||||
'Select a workspace from the sidebar to begin.'
|
||||
@@ -292,13 +292,7 @@ export default function Landing(): React.JSX.Element {
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="inline-flex items-center gap-1.5 bg-secondary/70 border border-border/80 text-foreground font-medium text-sm px-4 py-2 rounded-md transition-colors disabled:opacity-40 disabled:cursor-not-allowed enabled:cursor-pointer enabled:hover:bg-accent"
|
||||
disabled={!canCreateWorktree}
|
||||
title={
|
||||
!canCreateWorktree
|
||||
? translate('auto.components.Landing.f05d237049', 'Add a project first')
|
||||
: undefined
|
||||
}
|
||||
className="inline-flex items-center gap-1.5 bg-secondary/70 border border-border/80 text-foreground font-medium text-sm px-4 py-2 rounded-md cursor-pointer hover:bg-accent transition-colors"
|
||||
onClick={() => openModal('new-workspace-composer', { telemetrySource: 'unknown' })}
|
||||
>
|
||||
<GitBranchPlus className="size-3.5" />
|
||||
|
||||
@@ -848,7 +848,6 @@ function WorktreeJumpPaletteContent({
|
||||
hostLabelOverrides
|
||||
]
|
||||
)
|
||||
const canCreateWorktree = repos.length > 0
|
||||
|
||||
// Why: host-less repos and worktrees inherit the focused runtime host, exactly
|
||||
// as the sidebar's host headers do — otherwise the two disagree on bucketing.
|
||||
@@ -1815,14 +1814,13 @@ function WorktreeJumpPaletteContent({
|
||||
} = useMemo(
|
||||
() =>
|
||||
getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree,
|
||||
query: deferredQuery
|
||||
}),
|
||||
[canCreateWorktree, deferredQuery]
|
||||
[deferredQuery]
|
||||
)
|
||||
const createWorktreeName = taskSourceUrl ? query.trim() : deferredCreateWorktreeName
|
||||
// Why still gated: a task URL bypasses query deferral, not creation eligibility.
|
||||
const showCreateAction = deferredShowCreateAction || (taskSourceUrl !== null && canCreateWorktree)
|
||||
// Why: a task URL bypasses query deferral, so it arms create on its own.
|
||||
const showCreateAction = deferredShowCreateAction || taskSourceUrl !== null
|
||||
|
||||
// Why: arm the lookup before Enter can target the newly rendered Linear row.
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -39,7 +39,6 @@ export function ContextualTourOverlay(): JSX.Element | null {
|
||||
const keybindings = useAppStore((s) => s.keybindings)
|
||||
const activeTabId = useAppStore((s) => s.activeTabId)
|
||||
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
|
||||
const canCreateWorkspace = useAppStore((s) => s.repos.length > 0)
|
||||
const markContextualToursSeen = useAppStore((s) => s.markContextualToursSeen)
|
||||
const advanceContextualTour = useAppStore((s) => s.advanceContextualTour)
|
||||
const regressContextualTour = useAppStore((s) => s.regressContextualTour)
|
||||
@@ -316,7 +315,6 @@ export function ContextualTourOverlay(): JSX.Element | null {
|
||||
setSidebarOpen,
|
||||
openTaskPage,
|
||||
openModal,
|
||||
canCreateWorkspace,
|
||||
openWorkspaceComposer: openWorkspaceCreationComposerWithTourHandoff,
|
||||
dispatchTerminalPaneSplit: requestActiveTerminalPaneSplit,
|
||||
schedule: (callback) => {
|
||||
|
||||
@@ -18,7 +18,6 @@ describe('performContextualTourStepAction', () => {
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage,
|
||||
openModal: vi.fn(),
|
||||
canCreateWorkspace: true,
|
||||
openWorkspaceComposer: vi.fn(),
|
||||
dispatchTerminalPaneSplit: vi.fn(),
|
||||
schedule: vi.fn()
|
||||
@@ -43,7 +42,6 @@ describe('performContextualTourStepAction', () => {
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
canCreateWorkspace: true,
|
||||
openWorkspaceComposer: vi.fn(),
|
||||
dispatchTerminalPaneSplit,
|
||||
schedule: vi.fn()
|
||||
@@ -71,7 +69,6 @@ describe('performContextualTourStepAction', () => {
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
canCreateWorkspace: true,
|
||||
openWorkspaceComposer,
|
||||
dispatchTerminalPaneSplit: vi.fn(),
|
||||
schedule: vi.fn()
|
||||
@@ -85,7 +82,7 @@ describe('performContextualTourStepAction', () => {
|
||||
expect(finishTour).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not open the workspace composer when workspace creation is unavailable', () => {
|
||||
it('opens the workspace composer with no projects so the first one can be added there', () => {
|
||||
const detachContextualTourSource = vi.fn()
|
||||
const openWorkspaceComposer = vi.fn()
|
||||
|
||||
@@ -99,13 +96,12 @@ describe('performContextualTourStepAction', () => {
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
canCreateWorkspace: false,
|
||||
openWorkspaceComposer,
|
||||
dispatchTerminalPaneSplit: vi.fn(),
|
||||
schedule: vi.fn()
|
||||
})
|
||||
|
||||
expect(detachContextualTourSource).not.toHaveBeenCalled()
|
||||
expect(openWorkspaceComposer).not.toHaveBeenCalled()
|
||||
expect(detachContextualTourSource).toHaveBeenCalledTimes(1)
|
||||
expect(openWorkspaceComposer).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ export function performContextualTourStepAction(args: {
|
||||
setSidebarOpen: (open: boolean) => void
|
||||
openTaskPage: () => void
|
||||
openModal: (modal: 'setup-guide', data?: Record<string, unknown>) => void
|
||||
canCreateWorkspace: boolean
|
||||
openWorkspaceComposer: () => void
|
||||
dispatchTerminalPaneSplit: (detail: RequestActiveTerminalPaneSplitDetail) => void
|
||||
schedule: (callback: () => void) => void
|
||||
@@ -37,14 +36,12 @@ export function performContextualTourStepAction(args: {
|
||||
}
|
||||
return
|
||||
case 'create-worktree':
|
||||
if (args.canCreateWorkspace) {
|
||||
// Why: opening the composer cancels this tour (it isn't allowed over the
|
||||
// modal) and hands off to the workspace-creation tour. Detach first so the
|
||||
// terminal source's unmount cleanup can't record a stray suppression.
|
||||
args.detachContextualTourSource()
|
||||
args.setSidebarOpen(true)
|
||||
args.openWorkspaceComposer()
|
||||
}
|
||||
// Why: opening the composer cancels this tour (it isn't allowed over the
|
||||
// modal) and hands off to the workspace-creation tour. Detach first so the
|
||||
// terminal source's unmount cleanup can't record a stray suppression.
|
||||
args.detachContextualTourSource()
|
||||
args.setSidebarOpen(true)
|
||||
args.openWorkspaceComposer()
|
||||
return
|
||||
case 'show-worktrees':
|
||||
args.setSidebarOpen(true)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React, { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import SidebarHeader from './SidebarHeader'
|
||||
|
||||
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openWorkspaceCreationComposerWithTourHandoff: vi.fn()
|
||||
}))
|
||||
|
||||
type MockState = {
|
||||
repos: { id: string }[]
|
||||
groupBy: string
|
||||
openModal: (modal: string, data?: unknown) => void
|
||||
}
|
||||
|
||||
let mockState: MockState
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: MockState) => unknown) => selector(mockState)
|
||||
}))
|
||||
|
||||
vi.mock('./SidebarWorkspaceOptionsMenu', () => ({ default: () => null }))
|
||||
|
||||
vi.mock('@/hooks/useShortcutLabel', () => ({ useShortcutLabel: () => '⌘N' }))
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
vi.mock('../contextual-tours/workspace-creation-tour-handoff', () => ({
|
||||
openWorkspaceCreationComposerWithTourHandoff: mocks.openWorkspaceCreationComposerWithTourHandoff
|
||||
}))
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
function newWorkspaceButton(): HTMLButtonElement {
|
||||
const button = container.querySelector<HTMLButtonElement>('[aria-label="New workspace"]')
|
||||
if (!button) {
|
||||
throw new Error('New workspace button not rendered')
|
||||
}
|
||||
return button
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.openWorkspaceCreationComposerWithTourHandoff.mockClear()
|
||||
mockState = { repos: [], groupBy: 'repo', openModal: vi.fn() }
|
||||
container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount())
|
||||
container.remove()
|
||||
})
|
||||
|
||||
describe('SidebarHeader', () => {
|
||||
it('keeps New workspace clickable with zero projects, since the composer adds the first one', () => {
|
||||
act(() => {
|
||||
root.render(<SidebarHeader onWorkspaceBoardMenuOpenChange={vi.fn()} />)
|
||||
})
|
||||
|
||||
const button = newWorkspaceButton()
|
||||
expect(button.disabled).toBe(false)
|
||||
|
||||
act(() => {
|
||||
button.click()
|
||||
})
|
||||
|
||||
expect(mocks.openWorkspaceCreationComposerWithTourHandoff).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('opens the composer the same way once projects exist', () => {
|
||||
mockState.repos = [{ id: 'repo-a' }]
|
||||
act(() => {
|
||||
root.render(<SidebarHeader onWorkspaceBoardMenuOpenChange={vi.fn()} />)
|
||||
})
|
||||
|
||||
act(() => {
|
||||
newWorkspaceButton().click()
|
||||
})
|
||||
|
||||
expect(newWorkspaceButton().disabled).toBe(false)
|
||||
expect(mocks.openWorkspaceCreationComposerWithTourHandoff).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -18,7 +18,6 @@ const SidebarHeader = React.memo(function SidebarHeader({
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const newWorktreeShortcutLabel = useShortcutLabel('workspace.create')
|
||||
const groupBy = useAppStore((s) => s.groupBy)
|
||||
const canCreateWorkspace = useAppStore((s) => s.repos.length > 0)
|
||||
const sidebarTitle = groupBy === 'repo' ? 'Projects' : 'Workspaces'
|
||||
|
||||
return (
|
||||
@@ -62,35 +61,24 @@ const SidebarHeader = React.memo(function SidebarHeader({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => {
|
||||
if (!canCreateWorkspace) {
|
||||
return
|
||||
}
|
||||
// Why: the parallel-work tour must click the real sidebar
|
||||
// control so it can hand off to the workspace-creation tour.
|
||||
openWorkspaceCreationComposerWithTourHandoff()
|
||||
}}
|
||||
// Why: the parallel-work tour must click the real sidebar
|
||||
// control so it can hand off to the workspace-creation tour.
|
||||
onClick={openWorkspaceCreationComposerWithTourHandoff}
|
||||
aria-label={translate(
|
||||
'auto.components.sidebar.SidebarHeader.92154beb7e',
|
||||
'New workspace'
|
||||
)}
|
||||
disabled={!canCreateWorkspace}
|
||||
data-contextual-tour-target="workspace-create-control"
|
||||
>
|
||||
<Plus className="size-3.5" strokeWidth={2.25} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={6}>
|
||||
{canCreateWorkspace
|
||||
? translate(
|
||||
'auto.components.sidebar.SidebarHeader.ca6f729da2',
|
||||
'New workspace ({{value0}})',
|
||||
{ value0: newWorktreeShortcutLabel }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.sidebar.SidebarHeader.5c9c7c16aa',
|
||||
'Add a project to create workspaces'
|
||||
)}
|
||||
{translate(
|
||||
'auto.components.sidebar.SidebarHeader.ca6f729da2',
|
||||
'New workspace ({{value0}})',
|
||||
{ value0: newWorktreeShortcutLabel }
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -130,7 +130,6 @@ vi.mock('./use-workspace-kanban-column-resize', () => ({
|
||||
|
||||
vi.mock('./use-workspace-kanban-create-worktree', () => ({
|
||||
useWorkspaceKanbanCreateWorktree: () => ({
|
||||
canCreateWorktree: true,
|
||||
createWorktreeForStatus: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -138,7 +138,6 @@ vi.mock('./use-workspace-kanban-column-resize', () => ({
|
||||
|
||||
vi.mock('./use-workspace-kanban-create-worktree', () => ({
|
||||
useWorkspaceKanbanCreateWorktree: () => ({
|
||||
canCreateWorktree: true,
|
||||
createWorktreeForStatus: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -203,7 +203,7 @@ function WorkspaceKanbanDrawerContent({
|
||||
const activeWorktreeIdentity = activeWorktreeId
|
||||
? composeWorktreeHostIdentity(activeWorkspaceExecutionHostId ?? undefined, activeWorktreeId)
|
||||
: null
|
||||
const { canCreateWorktree, createWorktreeForStatus } = useWorkspaceKanbanCreateWorktree()
|
||||
const { createWorktreeForStatus } = useWorkspaceKanbanCreateWorktree()
|
||||
const visibleWorktreeIdSet = useVisibleWorkspaceKanbanWorktreeIds({
|
||||
allWorktrees,
|
||||
repoMap
|
||||
@@ -944,7 +944,6 @@ function WorkspaceKanbanDrawerContent({
|
||||
columnWidth={columnWidth}
|
||||
isResizingColumn={isResizingColumn}
|
||||
dragOverStatus={dragOverStatus}
|
||||
canCreateWorktree={canCreateWorktree}
|
||||
renderCards={renderCards}
|
||||
selectedWorktreeIds={selectedWorktreeIds}
|
||||
selectedWorktrees={renderedSelectedWorktrees}
|
||||
|
||||
@@ -108,7 +108,6 @@ function makeGrid(activeWorktreeIdentity: string | null = null): React.JSX.Eleme
|
||||
columnWidth={308}
|
||||
isResizingColumn={false}
|
||||
dragOverStatus={null}
|
||||
canCreateWorktree={true}
|
||||
renderCards={true}
|
||||
selectedWorktreeIds={new Set()}
|
||||
selectedWorktrees={[]}
|
||||
|
||||
@@ -35,7 +35,6 @@ type WorkspaceKanbanLaneGridProps = {
|
||||
columnWidth: number
|
||||
isResizingColumn: boolean
|
||||
dragOverStatus: WorkspaceStatus | null
|
||||
canCreateWorktree: boolean
|
||||
renderCards: boolean
|
||||
selectedWorktreeIds: ReadonlySet<string>
|
||||
selectedWorktrees: readonly Worktree[]
|
||||
@@ -65,7 +64,6 @@ export default function WorkspaceKanbanLaneGrid({
|
||||
columnWidth,
|
||||
isResizingColumn,
|
||||
dragOverStatus,
|
||||
canCreateWorktree,
|
||||
renderCards,
|
||||
selectedWorktreeIds,
|
||||
selectedWorktrees,
|
||||
@@ -210,7 +208,6 @@ export default function WorkspaceKanbanLaneGrid({
|
||||
columnWidth={columnWidth}
|
||||
isResizingColumn={isResizingColumn}
|
||||
isDragTarget={dragOverStatus === status.id}
|
||||
canCreateWorktree={canCreateWorktree}
|
||||
renderCards={renderCards && renderedLaneIds.has(status.id)}
|
||||
selectedWorktreeIds={selectedWorktreeIds}
|
||||
selectedWorktrees={selectedWorktrees}
|
||||
|
||||
@@ -62,7 +62,6 @@ function renderLane(props: {
|
||||
columnWidth={308}
|
||||
isResizingColumn={false}
|
||||
isDragTarget={false}
|
||||
canCreateWorktree={true}
|
||||
renderCards={true}
|
||||
selectedWorktreeIds={new Set()}
|
||||
selectedWorktrees={[]}
|
||||
|
||||
@@ -30,7 +30,6 @@ type WorkspaceKanbanStatusLaneProps = {
|
||||
columnWidth: number
|
||||
isResizingColumn: boolean
|
||||
isDragTarget: boolean
|
||||
canCreateWorktree: boolean
|
||||
nativeDragEnabled?: boolean
|
||||
renderCards: boolean
|
||||
selectedWorktreeIds: ReadonlySet<string>
|
||||
@@ -61,7 +60,6 @@ function WorkspaceKanbanStatusLane({
|
||||
columnWidth,
|
||||
isResizingColumn,
|
||||
isDragTarget,
|
||||
canCreateWorktree,
|
||||
nativeDragEnabled = true,
|
||||
renderCards,
|
||||
selectedWorktreeIds,
|
||||
@@ -94,9 +92,7 @@ function WorkspaceKanbanStatusLane({
|
||||
undefined
|
||||
)
|
||||
}, [fullWorktreeIds, hasQuery, items])
|
||||
const createTooltip = canCreateWorktree
|
||||
? `New workspace in ${status.label}`
|
||||
: 'Add a project to create workspaces'
|
||||
const createTooltip = `New workspace in ${status.label}`
|
||||
const createButton = (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -104,7 +100,6 @@ function WorkspaceKanbanStatusLane({
|
||||
size="icon-xs"
|
||||
className="size-6 text-muted-foreground"
|
||||
aria-label={createTooltip}
|
||||
disabled={!canCreateWorktree}
|
||||
onClick={() => onCreateWorktree(status.id)}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
@@ -224,7 +219,6 @@ function WorkspaceKanbanStatusLane({
|
||||
'group-hover/lane:opacity-100 group-focus-within/lane:opacity-100'
|
||||
)}
|
||||
aria-label={createTooltip}
|
||||
disabled={!canCreateWorktree}
|
||||
onClick={() => onCreateWorktree(status.id)}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
|
||||
@@ -3,11 +3,9 @@ import { useAppStore } from '@/store'
|
||||
import type { WorkspaceStatus } from '../../../../shared/worktree/types'
|
||||
|
||||
export function useWorkspaceKanbanCreateWorktree(): {
|
||||
canCreateWorktree: boolean
|
||||
createWorktreeForStatus: (workspaceStatus: WorkspaceStatus) => void
|
||||
} {
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const canCreateWorktree = useAppStore((s) => s.repos.length > 0)
|
||||
|
||||
const createWorktreeForStatus = useCallback(
|
||||
(workspaceStatus: WorkspaceStatus) => {
|
||||
@@ -19,5 +17,5 @@ export function useWorkspaceKanbanCreateWorktree(): {
|
||||
[openModal]
|
||||
)
|
||||
|
||||
return { canCreateWorktree, createWorktreeForStatus }
|
||||
return { createWorktreeForStatus }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { WORKTREE_PALETTE_QUERY_MAX_BYTES } from './worktree-palette-query-bound
|
||||
describe('worktree-palette-create-action', () => {
|
||||
it('shows create for typed queries with workspace matches but selects the first workspace row', () => {
|
||||
const state = getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree: true,
|
||||
query: 'feature'
|
||||
})
|
||||
|
||||
@@ -33,7 +32,6 @@ describe('worktree-palette-create-action', () => {
|
||||
|
||||
it('skips create for free text even when it is listed before every other row', () => {
|
||||
const state = getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree: true,
|
||||
query: 'opencode-issue'
|
||||
})
|
||||
|
||||
@@ -77,7 +75,6 @@ describe('worktree-palette-create-action', () => {
|
||||
|
||||
it('leaves Enter unarmed for typed queries with no real matches', () => {
|
||||
const state = getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree: true,
|
||||
query: 'new-workspace'
|
||||
})
|
||||
|
||||
@@ -156,19 +153,17 @@ describe('worktree-palette-create-action', () => {
|
||||
).toBe(CREATE_WORKTREE_ITEM_ID)
|
||||
})
|
||||
|
||||
it('hides create when no project is available to create a workspace in', () => {
|
||||
it('offers create with no projects, since the composer adds the first one inline', () => {
|
||||
expect(
|
||||
getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree: false,
|
||||
query: 'new-workspace'
|
||||
})
|
||||
).toEqual({ createWorktreeName: 'new-workspace', showCreateAction: false })
|
||||
).toEqual({ createWorktreeName: 'new-workspace', showCreateAction: true })
|
||||
})
|
||||
|
||||
it('hides create for an empty query', () => {
|
||||
expect(
|
||||
getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree: true,
|
||||
query: ' '
|
||||
}).showCreateAction
|
||||
).toBe(false)
|
||||
@@ -179,7 +174,6 @@ describe('worktree-palette-create-action', () => {
|
||||
|
||||
expect(
|
||||
getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree: true,
|
||||
query: oversizedQuery
|
||||
})
|
||||
).toEqual({
|
||||
|
||||
@@ -8,10 +8,8 @@ export type WorktreePaletteCreateActionState = {
|
||||
}
|
||||
|
||||
export function getWorktreePaletteCreateActionState({
|
||||
canCreateWorktree,
|
||||
query
|
||||
}: {
|
||||
canCreateWorktree: boolean
|
||||
query: string
|
||||
}): WorktreePaletteCreateActionState {
|
||||
const createWorktreeName = query.trim()
|
||||
@@ -21,12 +19,11 @@ export function getWorktreePaletteCreateActionState({
|
||||
showCreateAction: false
|
||||
}
|
||||
}
|
||||
// Why gate on eligibility: creation must not be offered — or reachable by Enter —
|
||||
// when there is no repo to create a workspace in.
|
||||
const showCreateAction = canCreateWorktree && createWorktreeName.length > 0
|
||||
// Why no project gate: the composer can add the first project inline, so
|
||||
// creation stays offered with zero projects.
|
||||
return {
|
||||
createWorktreeName,
|
||||
showCreateAction
|
||||
showCreateAction: createWorktreeName.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user