mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(mobile): redirect tasks after host mount
This commit is contained in:
@@ -65,7 +65,6 @@ import { BottomDrawer } from '../../../src/components/BottomDrawer'
|
||||
import { useHostProtocolGates } from '../../../src/components/HostProtocolGate'
|
||||
import { AuthFailedBanner } from '../../../src/components/AuthFailedBanner'
|
||||
import { MobileSearchField } from '../../../src/components/MobileSearchField'
|
||||
import { WorkspaceDetailPlaceholder } from '../../../src/components/WorkspaceDetailPlaceholder'
|
||||
import { getCachedWorktrees, setCachedWorktrees } from '../../../src/cache/worktree-cache'
|
||||
import { setCachedRepos } from '../../../src/cache/repo-cache'
|
||||
import { colors, radii, spacing, typography } from '../../../src/theme/mobile-theme'
|
||||
@@ -102,6 +101,7 @@ import {
|
||||
import type { RepoSummary } from '../../../src/worktree/host-worktree-rpc-types'
|
||||
import type { WorkspaceStatusDefinition } from '../../../../src/shared/types'
|
||||
import { DEFAULT_MOBILE_WORKSPACE_STATUSES } from '../../../src/worktree/mobile-workspace-statuses'
|
||||
import { MobileTaskHostRoute } from '../../../src/tasks/MobileTaskHostRoute'
|
||||
|
||||
function isErrorVerdict(v: ConnectionVerdict): boolean {
|
||||
return v.kind === 'warning' || v.kind === 'unreachable' || v.kind === 'auth-failed'
|
||||
@@ -1396,11 +1396,7 @@ export function HostScreen({
|
||||
|
||||
// On wide layouts the sidebar hosts the list, so this route is just the empty detail pane.
|
||||
export default function HostWorktreeRoute() {
|
||||
const { isWideLayout } = useResponsiveLayout()
|
||||
if (isWideLayout) {
|
||||
return <WorkspaceDetailPlaceholder />
|
||||
}
|
||||
return <HostScreen />
|
||||
return <MobileTaskHostRoute hostScreen={HostScreen} />
|
||||
}
|
||||
|
||||
function ListSeparator() {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { MobileTaskHostRoute } from './MobileTaskHostRoute'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
params: {} as { hostId?: string; action?: string; taskSource?: string },
|
||||
isWideLayout: false
|
||||
}))
|
||||
|
||||
vi.mock('expo-router', () => ({
|
||||
Redirect: 'Redirect',
|
||||
useLocalSearchParams: () => mocks.params
|
||||
}))
|
||||
|
||||
vi.mock('../layout/responsive-layout', () => ({
|
||||
useResponsiveLayout: () => ({ isWideLayout: mocks.isWideLayout })
|
||||
}))
|
||||
|
||||
vi.mock('../components/WorkspaceDetailPlaceholder', () => ({
|
||||
WorkspaceDetailPlaceholder: 'WorkspaceDetailPlaceholder'
|
||||
}))
|
||||
|
||||
function HostScreen() {
|
||||
return null
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mocks.params = {}
|
||||
mocks.isWideLayout = false
|
||||
})
|
||||
|
||||
describe('MobileTaskHostRoute', () => {
|
||||
it('redirects a mounted host action to its concrete Tasks route', () => {
|
||||
mocks.params = { hostId: 'host/1', action: 'tasks', taskSource: 'github' }
|
||||
|
||||
expect(MobileTaskHostRoute({ hostScreen: HostScreen })).toMatchObject({
|
||||
type: 'Redirect',
|
||||
props: { href: '/h/host%2F1/tasks?taskSource=github' }
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the normal host screen without the Tasks action', () => {
|
||||
expect(MobileTaskHostRoute({ hostScreen: HostScreen })).toMatchObject({ type: HostScreen })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { Redirect, useLocalSearchParams } from 'expo-router'
|
||||
import { useResponsiveLayout } from '../layout/responsive-layout'
|
||||
import { WorkspaceDetailPlaceholder } from '../components/WorkspaceDetailPlaceholder'
|
||||
import { mobileTasksRouteFromHostAction } from './mobile-task-navigation'
|
||||
|
||||
export function MobileTaskHostRoute({ hostScreen: HostScreen }: { hostScreen: ComponentType }) {
|
||||
const params = useLocalSearchParams<{
|
||||
hostId?: string
|
||||
action?: string
|
||||
taskSource?: string
|
||||
}>()
|
||||
const { isWideLayout } = useResponsiveLayout()
|
||||
const tasksRoute = mobileTasksRouteFromHostAction(params.hostId, params.action, params.taskSource)
|
||||
|
||||
if (tasksRoute) {
|
||||
return <Redirect href={tasksRoute} />
|
||||
}
|
||||
return isWideLayout ? <WorkspaceDetailPlaceholder /> : <HostScreen />
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mobileTasksRoute, navigateToMobileTasks } from './mobile-task-navigation'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
mobileTasksHostRoute,
|
||||
mobileTasksRoute,
|
||||
mobileTasksRouteFromHostAction,
|
||||
navigateToMobileTasks
|
||||
} from './mobile-task-navigation'
|
||||
|
||||
describe('mobileTasksRoute', () => {
|
||||
it('builds a concrete encoded host route', () => {
|
||||
@@ -11,20 +12,18 @@ describe('mobileTasksRoute', () => {
|
||||
expect(mobileTasksRoute('host/1', 'linear')).toBe('/h/host%2F1/tasks?taskSource=linear')
|
||||
})
|
||||
|
||||
it('mounts a cold host navigator before replacing its index with Tasks', () => {
|
||||
let nextFrame: FrameRequestCallback | null = null
|
||||
it('routes through the host index so its nested navigator owns the redirect', () => {
|
||||
const push = vi.fn()
|
||||
const replace = vi.fn()
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
||||
nextFrame = callback
|
||||
return 1
|
||||
})
|
||||
|
||||
navigateToMobileTasks({ push, replace }, 'host/1', 'github')
|
||||
expect(push).toHaveBeenCalledWith('/h/host%2F1')
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
navigateToMobileTasks({ push }, 'host/1', 'github')
|
||||
expect(push).toHaveBeenCalledWith(mobileTasksHostRoute('host/1', 'github'))
|
||||
})
|
||||
|
||||
nextFrame?.(0)
|
||||
expect(replace).toHaveBeenCalledWith(mobileTasksRoute('host/1', 'github'))
|
||||
it('resolves the host-owned Tasks action after the nested navigator mounts', () => {
|
||||
expect(mobileTasksRouteFromHostAction('host/1', 'tasks', 'linear')).toBe(
|
||||
'/h/host%2F1/tasks?taskSource=linear'
|
||||
)
|
||||
expect(mobileTasksRouteFromHostAction('host-1', 'newWorktree', 'github')).toBeNull()
|
||||
expect(mobileTasksRouteFromHostAction(undefined, 'tasks', 'github')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,9 +4,12 @@ export type MobileTasksRoute =
|
||||
| `/h/${string}/tasks`
|
||||
| `/h/${string}/tasks?taskSource=${TaskProvider}`
|
||||
|
||||
export type MobileTasksHostRoute =
|
||||
| `/h/${string}?action=tasks`
|
||||
| `/h/${string}?action=tasks&taskSource=${TaskProvider}`
|
||||
|
||||
type MobileTasksRouter = {
|
||||
push: (href: `/h/${string}`) => void
|
||||
replace: (href: MobileTasksRoute) => void
|
||||
push: (href: MobileTasksHostRoute) => void
|
||||
}
|
||||
|
||||
export function mobileTasksRoute(hostId: string, provider?: TaskProvider): MobileTasksRoute {
|
||||
@@ -15,14 +18,33 @@ export function mobileTasksRoute(hostId: string, provider?: TaskProvider): Mobil
|
||||
return provider ? `${pathname}?taskSource=${provider}` : pathname
|
||||
}
|
||||
|
||||
export function mobileTasksHostRoute(
|
||||
hostId: string,
|
||||
provider?: TaskProvider
|
||||
): MobileTasksHostRoute {
|
||||
const pathname = `/h/${encodeURIComponent(hostId)}?action=tasks` as const
|
||||
return provider ? `${pathname}&taskSource=${provider}` : pathname
|
||||
}
|
||||
|
||||
export function mobileTasksRouteFromHostAction(
|
||||
hostId: string | undefined,
|
||||
action: string | undefined,
|
||||
taskSource: string | undefined
|
||||
): MobileTasksRoute | null {
|
||||
if (!hostId || action !== 'tasks') {
|
||||
return null
|
||||
}
|
||||
const provider =
|
||||
taskSource === 'github' || taskSource === 'gitlab' || taskSource === 'linear'
|
||||
? taskSource
|
||||
: undefined
|
||||
return mobileTasksRoute(hostId, provider)
|
||||
}
|
||||
|
||||
export function navigateToMobileTasks(
|
||||
router: MobileTasksRouter,
|
||||
hostId: string,
|
||||
provider?: TaskProvider
|
||||
): void {
|
||||
// Why: a cold nested host navigator resolves a deep push to its index route.
|
||||
router.push(`/h/${encodeURIComponent(hostId)}`)
|
||||
requestAnimationFrame(() => {
|
||||
router.replace(mobileTasksRoute(hostId, provider))
|
||||
})
|
||||
router.push(mobileTasksHostRoute(hostId, provider))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user