mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
fix(sidebar): Option shows the trash on cloud workspace rows, owned and paired (OPTION-TRASH-CLOUD)
Holding Option showed the delete quick action on local worktrees but never on a cloud workspace's row. The gate (canShowWorkspaceDeleteQuickAction) refused every main worktree, and a cloud workspace's own row is its sandbox's main checkout. The gate now takes cloudPrimary (useIsCloudPrimaryRow: owned, or paired via the runtime catalog or bindings). The live card's DeleteQuickAction (place.deleteQuickAction) reads it, and the click goes through runWorktreeDelete, whose cloud-primary branch opens the cloud delete confirmation (requestCloudPrimaryDelete), never Remove Project. A local repo's primary checkout still gets no trash.
This commit is contained in:
+10
-1
@@ -94,6 +94,15 @@ export function canShowWorkspaceDeleteQuickAction(args: {
|
||||
deleteModifierPressed: boolean
|
||||
isDeleting: boolean
|
||||
isMainWorktree: boolean
|
||||
/**
|
||||
* An Orca Cloud workspace's own checkout, owned or paired (OPTION-TRASH-CLOUD). It is the cloud
|
||||
* workspace, not a repo's primary: the delete funnel destroys the sandbox after the cloud confirm.
|
||||
*/
|
||||
cloudPrimary?: boolean
|
||||
}): boolean {
|
||||
return args.deleteModifierPressed && !args.isDeleting && !args.isMainWorktree
|
||||
return (
|
||||
args.deleteModifierPressed &&
|
||||
!args.isDeleting &&
|
||||
(!args.isMainWorktree || args.cloudPrimary === true)
|
||||
)
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,4 +1,5 @@
|
||||
import { useWorkspaceReferenceDetails } from '../review/use-workspace-reference-details'
|
||||
import { useIsCloudPrimaryRow } from '@/lib/orca-cloud/cloud-host-rows'
|
||||
import { getWorkspaceAttachments } from '../../../../../../shared/workspace-attachments/workspace-attachments'
|
||||
import { summarizeWorkspaceReviewChecks } from '../../../../../../shared/workspace-review-checks'
|
||||
import { canShowWorkspaceDeleteQuickAction } from '../../workspace-actions/deletion/workspace-delete-quick-action'
|
||||
@@ -100,13 +101,16 @@ export function useWorktreeCardController(props: ResolvedWorktreeCardProps) {
|
||||
openModal: foundation.openModal
|
||||
})
|
||||
|
||||
// A cloud workspace's own row (owned or paired) is deletable: the sandbox goes, after the cloud confirm.
|
||||
const cloudPrimary = useIsCloudPrimaryRow(worktree)
|
||||
// Why: delete is destructive, so it only appears while holding Option/Alt, not in the ordinary hover chrome.
|
||||
const showDeleteQuickAction =
|
||||
!props.affiliateListMode &&
|
||||
canShowWorkspaceDeleteQuickAction({
|
||||
deleteModifierPressed: linked.deleteModifierPressed,
|
||||
isDeleting: linked.isDeleting,
|
||||
isMainWorktree: worktree.isMainWorktree
|
||||
isMainWorktree: worktree.isMainWorktree,
|
||||
cloudPrimary
|
||||
})
|
||||
const workspaceActions = useWorktreeCardWorkspaceActions({
|
||||
worktree,
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { useEffect } from 'react'
|
||||
import { getDefaultSettings } from '../../../../../../shared/constants'
|
||||
import { defineFixture } from '../../../../component-fixtures/component-fixture'
|
||||
import { seedCloudWorkspaceSnapshot } from '@/lib/orca-cloud/orca-cloud-workspace-store'
|
||||
import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
|
||||
import WorktreeCard from './WorktreeCard'
|
||||
|
||||
// OPTION-TRASH-CLOUD: the sidebar while Option is held, each card as if hovered. A cloud workspace's
|
||||
// own row (created here, or paired from Home) shows the trash like a local worktree; a local repo's
|
||||
// primary checkout still doesn't (Git can't delete it).
|
||||
const repo = { ...makeRepo(), displayName: 'orca' }
|
||||
const worktrees = [
|
||||
makeWorktree('local-main', 'orca', { isMainWorktree: true }),
|
||||
makeWorktree('local-wt', 'fix-login', { branch: 'refs/heads/fix-login' }),
|
||||
makeWorktree('cloud-owned', 'Add a push test', {
|
||||
isMainWorktree: true,
|
||||
hostId: 'runtime:env-owned',
|
||||
branch: 'refs/heads/add-push-test'
|
||||
}),
|
||||
makeWorktree('cloud-paired', 'Ada’s cloud workspace', {
|
||||
isMainWorktree: true,
|
||||
hostId: 'runtime:env-paired',
|
||||
branch: 'refs/heads/ada/onboarding'
|
||||
})
|
||||
]
|
||||
|
||||
// The created workspace's sandbox server, and a workspace paired from Home.
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the cloud row check reads only id, name, source and orcaCloudPaired.
|
||||
const CLOUD_SERVERS = [
|
||||
{ id: 'env-owned', name: 'orca sandbox a1b2c3d4', source: 'ephemeral-vm' },
|
||||
{ id: 'env-paired', name: 'orca (cloud)', orcaCloudPaired: true }
|
||||
] as never
|
||||
|
||||
function HoldOption({ children }: { children: React.ReactNode }): React.JSX.Element {
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt', altKey: true }))
|
||||
}, [])
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export const optionHeld = defineFixture({
|
||||
width: 320,
|
||||
store: {
|
||||
settings: getDefaultSettings('/home/fixture'),
|
||||
repos: [repo],
|
||||
worktreesByRepo: { [repo.id]: worktrees },
|
||||
worktreeCardProperties: ['status', 'branch'],
|
||||
runtimeEnvironments: CLOUD_SERVERS
|
||||
},
|
||||
setup: () => seedCloudWorkspaceSnapshot({ ids: new Set(['cloud-owned']), idsLoaded: true }),
|
||||
render: () => (
|
||||
<HoldOption>
|
||||
{/* Every card as if hovered: the trash only shows on the card under the pointer. */}
|
||||
<style>
|
||||
{'[data-option-delete-fixture] button[aria-label="Delete workspace"]{opacity:1}'}
|
||||
</style>
|
||||
<div
|
||||
data-option-delete-fixture=""
|
||||
className="flex flex-col gap-1 bg-worktree-sidebar p-2 text-worktree-sidebar-foreground"
|
||||
>
|
||||
{worktrees.map((worktree) => (
|
||||
<WorktreeCard
|
||||
key={worktree.id}
|
||||
worktree={worktree}
|
||||
repo={repo}
|
||||
isActive={false}
|
||||
nativeDragEnabled={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</HoldOption>
|
||||
)
|
||||
})
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
// @vitest-environment happy-dom
|
||||
// OPTION-TRASH-CLOUD: holding Option shows the trash on a cloud workspace's own row (owned or
|
||||
// paired), the live card's DeleteQuickAction, and it opens the cloud delete (sandbox), never the
|
||||
// local "Remove Project". A local repo's primary checkout still gets no trash.
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { seedCloudWorkspaceSnapshot } from '@/lib/orca-cloud/orca-cloud-workspace-store'
|
||||
import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
|
||||
import { canShowWorkspaceDeleteQuickAction } from '../../workspace-actions/deletion/workspace-delete-quick-action'
|
||||
import WorktreeCard from './WorktreeCard'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ cloudDelete: vi.fn() }))
|
||||
vi.mock('@/lib/orca-cloud/cloud-primary-delete-request', () => ({
|
||||
requestCloudPrimaryDelete: mocks.cloudDelete
|
||||
}))
|
||||
vi.mock('@/components/sidebar/workspace-actions/sleep/use-worktree-sleep-state', () => ({
|
||||
useIsSleepingWorktree: () => false,
|
||||
selectIsSleepingWorktree: () => false
|
||||
}))
|
||||
vi.mock('@/components/sidebar/workspace-card/activity/use-worktree-activity-status', () => ({
|
||||
useWorktreeActivityStatus: () => 'inactive'
|
||||
}))
|
||||
|
||||
const repo = makeRepo()
|
||||
const ownedCloud = makeWorktree('cloud-owned', 'Owned cloud', { isMainWorktree: true })
|
||||
const pairedCloud = makeWorktree('cloud-paired', 'Paired cloud', {
|
||||
isMainWorktree: true,
|
||||
hostId: 'runtime:env-paired'
|
||||
})
|
||||
const localPrimary = makeWorktree('local-main', 'Local primary', { isMainWorktree: true })
|
||||
const localWorktree = makeWorktree('local-wt', 'Local worktree')
|
||||
|
||||
function seed(): void {
|
||||
act(() => {
|
||||
useAppStore.setState({
|
||||
repos: [repo],
|
||||
worktreesByRepo: { [repo.id]: [ownedCloud, pairedCloud, localPrimary, localWorktree] },
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the cloud row check reads only id and orcaCloudPaired.
|
||||
runtimeEnvironments: [
|
||||
{ id: 'env-paired', name: 'web (cloud)', orcaCloudPaired: true }
|
||||
] as never
|
||||
})
|
||||
seedCloudWorkspaceSnapshot({ ids: new Set(['cloud-owned']), idsLoaded: true })
|
||||
})
|
||||
}
|
||||
|
||||
function card(worktree: ReturnType<typeof makeWorktree>) {
|
||||
return render(
|
||||
<TooltipProvider>
|
||||
<WorktreeCard worktree={worktree} repo={repo} isActive={false} nativeDragEnabled={false} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const trash = (container: HTMLElement) =>
|
||||
container.querySelector<HTMLButtonElement>('button[aria-label="Delete workspace"]')
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Alt' }))
|
||||
})
|
||||
cleanup()
|
||||
seedCloudWorkspaceSnapshot({})
|
||||
mocks.cloudDelete.mockReset()
|
||||
})
|
||||
|
||||
describe('Option shows the trash on cloud rows (OPTION-TRASH-CLOUD)', () => {
|
||||
it('the gate: a cloud primary is deletable, a local primary is not', () => {
|
||||
const held = { deleteModifierPressed: true, isDeleting: false }
|
||||
expect(canShowWorkspaceDeleteQuickAction({ ...held, isMainWorktree: true })).toBe(false)
|
||||
expect(
|
||||
canShowWorkspaceDeleteQuickAction({ ...held, isMainWorktree: true, cloudPrimary: true })
|
||||
).toBe(true)
|
||||
expect(canShowWorkspaceDeleteQuickAction({ ...held, isMainWorktree: false })).toBe(true)
|
||||
// Never without Option, never while deleting.
|
||||
expect(
|
||||
canShowWorkspaceDeleteQuickAction({
|
||||
deleteModifierPressed: false,
|
||||
isDeleting: false,
|
||||
isMainWorktree: true,
|
||||
cloudPrimary: true
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
canShowWorkspaceDeleteQuickAction({
|
||||
deleteModifierPressed: true,
|
||||
isDeleting: true,
|
||||
isMainWorktree: true,
|
||||
cloudPrimary: true
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['owned', ownedCloud],
|
||||
['paired', pairedCloud]
|
||||
])('%s cloud row: Option shows the trash and it opens the cloud delete', (_kind, worktree) => {
|
||||
seed()
|
||||
const view = card(worktree)
|
||||
expect(trash(view.container)).toBeNull()
|
||||
act(() => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt', altKey: true }))
|
||||
})
|
||||
const button = trash(view.container)
|
||||
expect(button).not.toBeNull()
|
||||
fireEvent.click(button!)
|
||||
expect(mocks.cloudDelete).toHaveBeenCalledWith(expect.objectContaining({ id: worktree.id }))
|
||||
expect(useAppStore.getState().activeModal).not.toBe('confirm-remove-folder')
|
||||
})
|
||||
|
||||
it('a local primary still gets no trash; a local worktree does', () => {
|
||||
seed()
|
||||
const primary = card(localPrimary)
|
||||
const worktree = card(localWorktree)
|
||||
act(() => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt', altKey: true }))
|
||||
})
|
||||
expect(trash(primary.container)).toBeNull()
|
||||
expect(trash(worktree.container)).not.toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user