mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix: scope workspace-creation-project tour target to project picker only
The tour target was previously applied to a container that included both the project picker and the run target picker below it. Restructure the layout to scope the target to only the project-related section, and add a test to verify the tour target does not span into the run target picker.
This commit is contained in:
@@ -104,7 +104,7 @@ vi.mock('@/components/new-workspace/ProjectCombobox', () => ({
|
||||
value: string | null
|
||||
onValueChange: (value: string) => void
|
||||
}) => (
|
||||
<div data-testid="project-combobox" data-value={value ?? ''}>
|
||||
<div data-testid="project-combobox" data-project-combobox-root="true" data-value={value ?? ''}>
|
||||
{options.map((option) => (
|
||||
<button key={option.id} type="button" onClick={() => onValueChange(option.id)}>
|
||||
{option.displayName}
|
||||
@@ -165,32 +165,35 @@ const devboxNeedsSetupHostOption: ProjectHostSetupOption = {
|
||||
canSetLocation: true
|
||||
}
|
||||
|
||||
const disconnectedDevboxNeedsSetupHostOption: ProjectHostSetupOption = {
|
||||
kind: 'needs-setup',
|
||||
id: 'needs-setup:ssh:devbox',
|
||||
projectId: 'project-group:platform',
|
||||
hostId: 'ssh:devbox',
|
||||
label: 'Devbox',
|
||||
detail: 'Connect this host to set up projects',
|
||||
isAvailable: false,
|
||||
attention: false,
|
||||
canSetLocation: false,
|
||||
connectAction: { kind: 'ssh', targetId: 'devbox' }
|
||||
function makeDisconnectedHostOption(targetId: string, label: string): ProjectHostSetupOption {
|
||||
return {
|
||||
kind: 'needs-setup',
|
||||
id: `needs-setup:ssh:${targetId}`,
|
||||
projectId: 'project-group:platform',
|
||||
hostId: `ssh:${targetId}`,
|
||||
label,
|
||||
detail: 'Connect this host to set up projects',
|
||||
isAvailable: false,
|
||||
attention: false,
|
||||
canSetLocation: false,
|
||||
connectAction: { kind: 'ssh', targetId }
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectedBastionNeedsSetupHostOption: ProjectHostSetupOption = {
|
||||
kind: 'needs-setup',
|
||||
id: 'needs-setup:ssh:bastion',
|
||||
projectId: 'project-group:platform',
|
||||
hostId: 'ssh:bastion',
|
||||
label: 'Bastion',
|
||||
detail: 'Connect this host to set up projects',
|
||||
isAvailable: false,
|
||||
attention: false,
|
||||
canSetLocation: false,
|
||||
connectAction: { kind: 'ssh', targetId: 'bastion' }
|
||||
const disconnectedDevboxNeedsSetupHostOption = makeDisconnectedHostOption('devbox', 'Devbox')
|
||||
const disconnectedBastionNeedsSetupHostOption = makeDisconnectedHostOption('bastion', 'Bastion')
|
||||
|
||||
const pnpmInstallSetupConfig = {
|
||||
source: 'yaml' as const,
|
||||
command: 'pnpm install',
|
||||
kind: 'setup' as const
|
||||
}
|
||||
|
||||
const vmRecipeHostOptions: ProjectHostSetupOption[] = [
|
||||
{ kind: 'ready', id: 'setup-local', label: 'Local Mac', path: '/Users/alice/orca' },
|
||||
{ kind: 'ready', id: 'setup-builder', label: 'Builder', path: '/workspace/orca' }
|
||||
] as never
|
||||
|
||||
function findConnectButton(label: string): HTMLButtonElement | undefined {
|
||||
const item = findRunTargetItem(label)
|
||||
return [...(item?.querySelectorAll('button') ?? [])].find((button) =>
|
||||
@@ -313,6 +316,12 @@ function unmountCurrent(): void {
|
||||
current?.container.remove()
|
||||
}
|
||||
|
||||
function findWaitSwitch(container: HTMLElement): HTMLButtonElement | null {
|
||||
return container.querySelector(
|
||||
'[role="switch"][aria-label="Wait for setup to complete before starting agent"]'
|
||||
)
|
||||
}
|
||||
|
||||
describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
beforeEach(() => {
|
||||
;(window as unknown as { api: unknown }).api = {
|
||||
@@ -378,20 +387,30 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
)
|
||||
expect(projectSection?.textContent).not.toContain('Task Source')
|
||||
expect(nameSection?.textContent).toContain("Name or 'Create From'")
|
||||
expect(
|
||||
current.container
|
||||
.querySelector('[aria-label="workspace name"]')
|
||||
?.getAttribute('data-repo-backed-search-count')
|
||||
).toBe('2')
|
||||
expect(
|
||||
current.container
|
||||
.querySelector('[aria-label="workspace name"]')
|
||||
?.getAttribute('data-repo-backed-search-names')
|
||||
).toBe('Repo A,Repo B')
|
||||
const nameInput = current.container.querySelector('[aria-label="workspace name"]')
|
||||
expect(nameInput?.getAttribute('data-repo-backed-search-count')).toBe('2')
|
||||
expect(nameInput?.getAttribute('data-repo-backed-search-names')).toBe('Repo A,Repo B')
|
||||
expect(current.container.querySelector('[data-testid="repo-backed-source-trigger"]')).toBeNull()
|
||||
expect(current.container.querySelectorAll('[data-testid="project-combobox"]')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('scopes the workspace-creation-project tour target to the project picker rather than the run target picker', () => {
|
||||
current = renderCard({ projectHostSetupOptions: [localReadyHostOption] })
|
||||
|
||||
const projectTourTarget = current.container.querySelector(
|
||||
'[data-contextual-tour-target="workspace-creation-project"]'
|
||||
)
|
||||
expect(projectTourTarget).toBeTruthy()
|
||||
expect(projectTourTarget?.querySelector('[data-project-combobox-root="true"]')).toBeTruthy()
|
||||
expect(projectTourTarget?.querySelector('[data-run-target-combobox-root="true"]')).toBeNull()
|
||||
expect(projectTourTarget?.textContent).not.toContain('Run on')
|
||||
|
||||
const runTargetPicker = current.container.querySelector(
|
||||
'div[data-run-target-combobox-root="true"]'
|
||||
)
|
||||
expect(runTargetPicker).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps the reuse-branch row collapsed until a local branch is reusable', () => {
|
||||
// Why: the row stays mounted (for the smooth height transition) but is
|
||||
// collapsed + aria-hidden when reuse isn't possible.
|
||||
@@ -467,11 +486,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
current = renderCard({
|
||||
advancedOpen: true,
|
||||
setupControlsEnabled: true,
|
||||
setupConfig: {
|
||||
source: 'yaml',
|
||||
command: 'pnpm install',
|
||||
kind: 'setup'
|
||||
}
|
||||
setupConfig: pnpmInstallSetupConfig
|
||||
})
|
||||
expect(current.container.textContent).toContain(
|
||||
'Wait for setup to complete before starting agent'
|
||||
@@ -484,17 +499,11 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
advancedOpen: true,
|
||||
setupControlsEnabled: true,
|
||||
resolvedSetupDecision: 'run',
|
||||
setupConfig: {
|
||||
source: 'yaml',
|
||||
command: 'pnpm install',
|
||||
kind: 'setup'
|
||||
},
|
||||
setupConfig: pnpmInstallSetupConfig,
|
||||
onSetupAgentStartupPolicyChange: (next) => changes.push(next)
|
||||
})
|
||||
|
||||
const waitSwitch = current.container.querySelector<HTMLButtonElement>(
|
||||
'[role="switch"][aria-label="Wait for setup to complete before starting agent"]'
|
||||
)
|
||||
const waitSwitch = findWaitSwitch(current.container)
|
||||
expect(waitSwitch).toBeTruthy()
|
||||
expect(waitSwitch?.disabled).toBe(false)
|
||||
act(() => waitSwitch?.click())
|
||||
@@ -507,17 +516,11 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
advancedOpen: true,
|
||||
setupControlsEnabled: true,
|
||||
resolvedSetupDecision: 'skip',
|
||||
setupConfig: {
|
||||
source: 'yaml',
|
||||
command: 'pnpm install',
|
||||
kind: 'setup'
|
||||
},
|
||||
setupConfig: pnpmInstallSetupConfig,
|
||||
onSetupAgentStartupPolicyChange: (next) => changes.push(next)
|
||||
})
|
||||
|
||||
const waitSwitch = current.container.querySelector<HTMLButtonElement>(
|
||||
'[role="switch"][aria-label="Wait for setup to complete before starting agent"]'
|
||||
)
|
||||
const waitSwitch = findWaitSwitch(current.container)
|
||||
expect(waitSwitch?.disabled).toBe(true)
|
||||
// Nothing to wait for when setup won't run — clicking is inert.
|
||||
act(() => waitSwitch?.click())
|
||||
@@ -799,20 +802,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
const hostChanges: string[] = []
|
||||
const recipeChanges: (string | null)[] = []
|
||||
current = renderCard({
|
||||
projectHostSetupOptions: [
|
||||
{
|
||||
kind: 'ready',
|
||||
id: 'setup-local',
|
||||
label: 'Local Mac',
|
||||
path: '/Users/alice/orca'
|
||||
},
|
||||
{
|
||||
kind: 'ready',
|
||||
id: 'setup-builder',
|
||||
label: 'Builder',
|
||||
path: '/workspace/orca'
|
||||
}
|
||||
] as never,
|
||||
projectHostSetupOptions: vmRecipeHostOptions,
|
||||
selectedProjectHostSetupId: 'setup-local',
|
||||
onProjectHostSetupChange: (setupId) => hostChanges.push(setupId),
|
||||
ephemeralVmRecipes: [
|
||||
@@ -853,20 +843,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
|
||||
const hostChanges: string[] = []
|
||||
const recipeChanges: (string | null)[] = []
|
||||
current = renderCard({
|
||||
projectHostSetupOptions: [
|
||||
{
|
||||
kind: 'ready',
|
||||
id: 'setup-local',
|
||||
label: 'Local Mac',
|
||||
path: '/Users/alice/orca'
|
||||
},
|
||||
{
|
||||
kind: 'ready',
|
||||
id: 'setup-builder',
|
||||
label: 'Builder',
|
||||
path: '/workspace/orca'
|
||||
}
|
||||
] as never,
|
||||
projectHostSetupOptions: vmRecipeHostOptions,
|
||||
selectedProjectHostSetupId: 'setup-local',
|
||||
onProjectHostSetupChange: (setupId) => hostChanges.push(setupId),
|
||||
ephemeralVmRecipes: [
|
||||
|
||||
@@ -80,62 +80,64 @@ export function NewWorkspaceComposerProjectSection({
|
||||
selectedProjectName
|
||||
}: NewWorkspaceComposerProjectSectionProps): React.JSX.Element {
|
||||
return (
|
||||
<div className="space-y-1" data-contextual-tour-target="workspace-creation-project">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{projectLabel ??
|
||||
translate('auto.components.NewWorkspaceComposerCard.969a8bff66', 'Project')}
|
||||
</label>
|
||||
{showAddProjectButton ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={onAddProject}
|
||||
className="size-5 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
|
||||
aria-label={translate(
|
||||
'auto.components.NewWorkspaceComposerCard.d6b0a96f32',
|
||||
'Add project'
|
||||
)}
|
||||
>
|
||||
<FolderPlus className="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>
|
||||
{translate('auto.components.NewWorkspaceComposerCard.d6b0a96f32', 'Add project')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1" data-contextual-tour-target="workspace-creation-project">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{projectLabel ??
|
||||
translate('auto.components.NewWorkspaceComposerCard.969a8bff66', 'Project')}
|
||||
</label>
|
||||
{showAddProjectButton ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={onAddProject}
|
||||
className="size-5 shrink-0 rounded-sm text-muted-foreground hover:text-foreground"
|
||||
aria-label={translate(
|
||||
'auto.components.NewWorkspaceComposerCard.d6b0a96f32',
|
||||
'Add project'
|
||||
)}
|
||||
>
|
||||
<FolderPlus className="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>
|
||||
{translate('auto.components.NewWorkspaceComposerCard.d6b0a96f32', 'Add project')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<ProjectCombobox
|
||||
options={projectOptions}
|
||||
value={selectedProjectId}
|
||||
onValueChange={onProjectChange}
|
||||
onValueSelected={focusNameInput}
|
||||
onAddProject={onAddProject}
|
||||
placeholder={
|
||||
projectPlaceholder ??
|
||||
translate('auto.components.NewWorkspaceComposerCard.dccd26d4e4', 'Choose project')
|
||||
}
|
||||
triggerClassName="h-9 w-full border-input text-sm focus:border-ring focus:ring-[3px] focus:ring-ring/50"
|
||||
invalid={Boolean(projectError)}
|
||||
describedBy={projectDescriptionId}
|
||||
/>
|
||||
{projectError ? (
|
||||
<p id={projectDescriptionId} className="text-[11px] text-destructive">
|
||||
{projectError}
|
||||
</p>
|
||||
) : projectOptions.length === 0 ? (
|
||||
<p id={projectDescriptionId} className="text-[11px] text-muted-foreground">
|
||||
{emptyProjectMessage ??
|
||||
translate(
|
||||
'auto.components.NewWorkspaceComposerCard.addProjectBeforeWorkspace',
|
||||
'Add a project before creating a workspace.'
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<ProjectCombobox
|
||||
options={projectOptions}
|
||||
value={selectedProjectId}
|
||||
onValueChange={onProjectChange}
|
||||
onValueSelected={focusNameInput}
|
||||
onAddProject={onAddProject}
|
||||
placeholder={
|
||||
projectPlaceholder ??
|
||||
translate('auto.components.NewWorkspaceComposerCard.dccd26d4e4', 'Choose project')
|
||||
}
|
||||
triggerClassName="h-9 w-full border-input text-sm focus:border-ring focus:ring-[3px] focus:ring-ring/50"
|
||||
invalid={Boolean(projectError)}
|
||||
describedBy={projectDescriptionId}
|
||||
/>
|
||||
{projectError ? (
|
||||
<p id={projectDescriptionId} className="text-[11px] text-destructive">
|
||||
{projectError}
|
||||
</p>
|
||||
) : projectOptions.length === 0 ? (
|
||||
<p id={projectDescriptionId} className="text-[11px] text-muted-foreground">
|
||||
{emptyProjectMessage ??
|
||||
translate(
|
||||
'auto.components.NewWorkspaceComposerCard.addProjectBeforeWorkspace',
|
||||
'Add a project before creating a workspace.'
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
{shouldShowRunTargetPicker ? (
|
||||
<div className="space-y-1 pt-3">
|
||||
<label className="block min-w-0 truncate text-xs font-medium text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user