mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix duplicate workspace shell navigation
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
|||||||
workspaceDeckEntryLocator,
|
workspaceDeckEntryLocator,
|
||||||
expectWorkspaceDeckEntryCount,
|
expectWorkspaceDeckEntryCount,
|
||||||
} from "./helpers/workspace-ui";
|
} from "./helpers/workspace-ui";
|
||||||
|
import { clickSettingsBackToWorkspace } from "./helpers/settings";
|
||||||
|
|
||||||
const LOADING_WORKSPACE_TEXT_PATTERN = /Loading workspace/i;
|
const LOADING_WORKSPACE_TEXT_PATTERN = /Loading workspace/i;
|
||||||
|
|
||||||
@@ -75,6 +76,18 @@ async function expectNoLoadingPane(page: Page): Promise<void> {
|
|||||||
await expect(page.getByText(LOADING_WORKSPACE_TEXT_PATTERN)).toHaveCount(0);
|
await expect(page.getByText(LOADING_WORKSPACE_TEXT_PATTERN)).toHaveCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getVisibleDraftTabCount(page: Page): Promise<number> {
|
||||||
|
return page.locator('[data-testid^="workspace-tab-draft"]').filter({ visible: true }).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeFirstVisibleDraftTab(page: Page): Promise<void> {
|
||||||
|
const closeButton = page.locator('[data-testid^="workspace-draft-close-"]').filter({
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
await expect(closeButton.first()).toBeVisible({ timeout: 30_000 });
|
||||||
|
await closeButton.first().click();
|
||||||
|
}
|
||||||
|
|
||||||
async function installDaemonWebSocketGate(page: Page, daemonPort: string) {
|
async function installDaemonWebSocketGate(page: Page, daemonPort: string) {
|
||||||
let acceptingConnections = true;
|
let acceptingConnections = true;
|
||||||
const activeSockets = new Set<WebSocketRoute>();
|
const activeSockets = new Set<WebSocketRoute>();
|
||||||
@@ -133,6 +146,25 @@ async function installDaemonWebSocketGate(page: Page, daemonPort: string) {
|
|||||||
test.describe("Workspace navigation regression", () => {
|
test.describe("Workspace navigation regression", () => {
|
||||||
test.describe.configure({ timeout: 240_000 });
|
test.describe.configure({ timeout: 240_000 });
|
||||||
|
|
||||||
|
test("keeps one replacement draft after returning from settings and closing the last tab", async ({
|
||||||
|
page,
|
||||||
|
withWorkspace,
|
||||||
|
}) => {
|
||||||
|
const workspace = await withWorkspace({ prefix: "workspace-settings-back-tab-" });
|
||||||
|
|
||||||
|
await workspace.navigateTo();
|
||||||
|
await expect.poll(() => getVisibleDraftTabCount(page), { timeout: 30_000 }).toBe(1);
|
||||||
|
|
||||||
|
await openSettings(page);
|
||||||
|
await clickSettingsBackToWorkspace(page);
|
||||||
|
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
|
||||||
|
await expect.poll(() => getVisibleDraftTabCount(page), { timeout: 30_000 }).toBe(1);
|
||||||
|
|
||||||
|
await closeFirstVisibleDraftTab(page);
|
||||||
|
|
||||||
|
await expect.poll(() => getVisibleDraftTabCount(page), { timeout: 30_000 }).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps the workspace rendered while reconnecting to the host", async ({ page }) => {
|
test("keeps the workspace rendered while reconnecting to the host", async ({ page }) => {
|
||||||
const serverId = process.env.E2E_SERVER_ID;
|
const serverId = process.env.E2E_SERVER_ID;
|
||||||
const daemonPort = process.env.E2E_DAEMON_PORT;
|
const daemonPort = process.env.E2E_DAEMON_PORT;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useLocalSearchParams, useRouter, type Href } from "expo-router";
|
import { useLocalSearchParams, usePathname, useRouter, type Href } from "expo-router";
|
||||||
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useResolveWorkspaceIdByCwd } from "@/stores/session-store-hooks";
|
import { useResolveWorkspaceIdByCwd } from "@/stores/session-store-hooks";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||||
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
|
|
||||||
export default function HostAgentReadyRoute() {
|
export default function HostAgentReadyRoute() {
|
||||||
return (
|
return (
|
||||||
@@ -18,6 +18,7 @@ export default function HostAgentReadyRoute() {
|
|||||||
|
|
||||||
function HostAgentReadyRouteContent() {
|
function HostAgentReadyRouteContent() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
const params = useLocalSearchParams<{
|
const params = useLocalSearchParams<{
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
@@ -50,15 +51,15 @@ function HostAgentReadyRouteContent() {
|
|||||||
|
|
||||||
if (resolvedWorkspaceId) {
|
if (resolvedWorkspaceId) {
|
||||||
redirectedRef.current = true;
|
redirectedRef.current = true;
|
||||||
router.replace(
|
navigateToPreparedWorkspaceTab({
|
||||||
prepareWorkspaceTab({
|
serverId,
|
||||||
serverId,
|
workspaceId: resolvedWorkspaceId,
|
||||||
workspaceId: resolvedWorkspaceId,
|
target: { kind: "agent", agentId },
|
||||||
target: { kind: "agent", agentId },
|
navigationMethod: "replace",
|
||||||
}) as Href,
|
currentPathname: pathname,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}, [agentId, resolvedWorkspaceId, router, serverId]);
|
}, [agentId, pathname, resolvedWorkspaceId, router, serverId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (redirectedRef.current) {
|
if (redirectedRef.current) {
|
||||||
@@ -102,13 +103,13 @@ function HostAgentReadyRouteContent() {
|
|||||||
}
|
}
|
||||||
redirectedRef.current = true;
|
redirectedRef.current = true;
|
||||||
if (workspaceId) {
|
if (workspaceId) {
|
||||||
router.replace(
|
navigateToPreparedWorkspaceTab({
|
||||||
prepareWorkspaceTab({
|
serverId,
|
||||||
serverId,
|
workspaceId,
|
||||||
workspaceId,
|
target: { kind: "agent", agentId },
|
||||||
target: { kind: "agent", agentId },
|
navigationMethod: "replace",
|
||||||
}) as Href,
|
currentPathname: pathname,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.replace(buildHostRootRoute(serverId));
|
router.replace(buildHostRootRoute(serverId));
|
||||||
@@ -125,7 +126,7 @@ function HostAgentReadyRouteContent() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [agentId, client, hasHydratedWorkspaces, isConnected, router, serverId]);
|
}, [agentId, client, hasHydratedWorkspaces, isConnected, pathname, router, serverId]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Archive } from "lucide-react-native";
|
|||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
||||||
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import type { Agent } from "@/stores/session-store";
|
import type { Agent } from "@/stores/session-store";
|
||||||
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
||||||
|
|
||||||
@@ -324,13 +324,12 @@ export function AgentList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
rememberArchivedAgentDetail(agent);
|
rememberArchivedAgentDetail(agent);
|
||||||
const route = prepareWorkspaceTab({
|
navigateToPreparedWorkspaceTab({
|
||||||
serverId,
|
serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
target: { kind: "agent", agentId },
|
target: { kind: "agent", agentId },
|
||||||
pin: Boolean(agent.archivedAt),
|
pin: Boolean(agent.archivedAt),
|
||||||
});
|
});
|
||||||
router.navigate(route);
|
|
||||||
},
|
},
|
||||||
[isActionSheetVisible, onAgentSelect],
|
[isActionSheetVisible, onAgentSelect],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
@@ -83,7 +82,7 @@ import {
|
|||||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import { normalizeInlinePathTarget } from "@/utils/inline-path";
|
import { normalizeInlinePathTarget } from "@/utils/inline-path";
|
||||||
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import {
|
import {
|
||||||
getWorkingIndicatorDotStrength,
|
getWorkingIndicatorDotStrength,
|
||||||
@@ -135,7 +134,6 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const viewportRef = useRef<StreamViewportHandle | null>(null);
|
const viewportRef = useRef<StreamViewportHandle | null>(null);
|
||||||
const router = useRouter();
|
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const streamRenderStrategy = useMemo(
|
const streamRenderStrategy = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -213,12 +211,11 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (workspaceId) {
|
if (workspaceId) {
|
||||||
const route = prepareWorkspaceTab({
|
navigateToPreparedWorkspaceTab({
|
||||||
serverId: resolvedServerId,
|
serverId: resolvedServerId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
target: { kind: "file", path: normalized.file },
|
target: { kind: "file", path: normalized.file },
|
||||||
});
|
});
|
||||||
router.navigate(route);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -247,7 +244,6 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
onOpenWorkspaceFile,
|
onOpenWorkspaceFile,
|
||||||
requestDirectoryListing,
|
requestDirectoryListing,
|
||||||
resolvedServerId,
|
resolvedServerId,
|
||||||
router,
|
|
||||||
setExplorerTabForCheckout,
|
setExplorerTabForCheckout,
|
||||||
openWorkspaceFile,
|
openWorkspaceFile,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-over
|
|||||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
import { getIsElectronRuntime } from "@/constants/layout";
|
import { getIsElectronRuntime } from "@/constants/layout";
|
||||||
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import { focusWithRetries } from "@/utils/web-focus";
|
import { focusWithRetries } from "@/utils/web-focus";
|
||||||
import { useActiveServerId } from "@/hooks/use-active-server-id";
|
import { useActiveServerId } from "@/hooks/use-active-server-id";
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ function resolveActionShortcutKeys(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCommandCenter() {
|
export function useCommandCenter() {
|
||||||
const _pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const routeActiveServerId = useActiveServerId();
|
const routeActiveServerId = useActiveServerId();
|
||||||
const { overrides } = useKeyboardShortcutOverrides();
|
const { overrides } = useKeyboardShortcutOverrides();
|
||||||
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
|
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
|
||||||
@@ -212,14 +212,14 @@ export function useCommandCenter() {
|
|||||||
router.navigate(buildHostAgentDetailRoute(agent.serverId, agent.id) as Href);
|
router.navigate(buildHostAgentDetailRoute(agent.serverId, agent.id) as Href);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const route = prepareWorkspaceTab({
|
navigateToPreparedWorkspaceTab({
|
||||||
serverId: agent.serverId,
|
serverId: agent.serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
target: { kind: "agent", agentId: agent.id },
|
target: { kind: "agent", agentId: agent.id },
|
||||||
|
currentPathname: pathname,
|
||||||
});
|
});
|
||||||
router.navigate(route);
|
|
||||||
},
|
},
|
||||||
[setOpen],
|
[pathname, setOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openProjectPicker = useOpenProjectPicker(activeServerId);
|
const openProjectPicker = useOpenProjectPicker(activeServerId);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.hoisted(() => {
|
||||||
|
(globalThis as typeof globalThis & { __DEV__?: boolean }).__DEV__ = false;
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("@react-native-async-storage/async-storage", () => {
|
vi.mock("@react-native-async-storage/async-storage", () => {
|
||||||
const storage = new Map<string, string>();
|
const storage = new Map<string, string>();
|
||||||
return {
|
return {
|
||||||
@@ -19,10 +23,8 @@ const { replaceRoute } = vi.hoisted(() => ({
|
|||||||
replaceRoute: vi.fn(),
|
replaceRoute: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("expo-router", () => ({
|
vi.mock("@/hooks/use-workspace-navigation", () => ({
|
||||||
router: {
|
navigateToWorkspace: replaceRoute,
|
||||||
replace: replaceRoute,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { openProjectDirectly } from "@/hooks/use-open-project";
|
import { openProjectDirectly } from "@/hooks/use-open-project";
|
||||||
@@ -89,7 +91,7 @@ describe("openProjectDirectly", () => {
|
|||||||
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
|
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
|
||||||
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
|
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
|
||||||
openDraftTab: createOpenDraftTab(),
|
openDraftTab: createOpenDraftTab(),
|
||||||
replaceRoute,
|
navigateToWorkspace: replaceRoute,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
@@ -115,7 +117,7 @@ describe("openProjectDirectly", () => {
|
|||||||
const tabs = collectAllTabs(layout.root);
|
const tabs = collectAllTabs(layout.root);
|
||||||
expect(tabs).toHaveLength(1);
|
expect(tabs).toHaveLength(1);
|
||||||
expect(tabs[0]?.target.kind).toBe("draft");
|
expect(tabs[0]?.target.kind).toBe("draft");
|
||||||
expect(replaceRoute).toHaveBeenCalledWith("/h/server-1/workspace/1");
|
expect(replaceRoute).toHaveBeenCalledWith("server-1", "1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not navigate or seed tabs when openProject fails", async () => {
|
it("does not navigate or seed tabs when openProject fails", async () => {
|
||||||
@@ -133,7 +135,7 @@ describe("openProjectDirectly", () => {
|
|||||||
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
|
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
|
||||||
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
|
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
|
||||||
openDraftTab: createOpenDraftTab(),
|
openDraftTab: createOpenDraftTab(),
|
||||||
replaceRoute,
|
navigateToWorkspace: replaceRoute,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { router, type Href } from "expo-router";
|
|
||||||
import type { DaemonClient } from "@server/client/daemon-client";
|
import type { DaemonClient } from "@server/client/daemon-client";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
useWorkspaceLayoutStore,
|
useWorkspaceLayoutStore,
|
||||||
} from "@/stores/workspace-layout-store";
|
} from "@/stores/workspace-layout-store";
|
||||||
import { generateDraftId } from "@/stores/draft-keys";
|
import { generateDraftId } from "@/stores/draft-keys";
|
||||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
|
||||||
|
|
||||||
interface OpenProjectDirectlyInput {
|
interface OpenProjectDirectlyInput {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -22,7 +21,7 @@ interface OpenProjectDirectlyInput {
|
|||||||
mergeWorkspaces: (serverId: string, workspaces: Iterable<WorkspaceDescriptor>) => void;
|
mergeWorkspaces: (serverId: string, workspaces: Iterable<WorkspaceDescriptor>) => void;
|
||||||
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
|
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
|
||||||
openDraftTab: (workspaceKey: string) => string | null;
|
openDraftTab: (workspaceKey: string) => string | null;
|
||||||
replaceRoute: (route: string) => void;
|
navigateToWorkspace: (serverId: string, workspaceId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openProjectDirectly(input: OpenProjectDirectlyInput): Promise<boolean> {
|
export async function openProjectDirectly(input: OpenProjectDirectlyInput): Promise<boolean> {
|
||||||
@@ -50,7 +49,7 @@ export async function openProjectDirectly(input: OpenProjectDirectlyInput): Prom
|
|||||||
}
|
}
|
||||||
|
|
||||||
input.openDraftTab(workspaceKey);
|
input.openDraftTab(workspaceKey);
|
||||||
input.replaceRoute(buildHostWorkspaceRoute(normalizedServerId, workspace.id));
|
input.navigateToWorkspace(normalizedServerId, workspace.id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,9 +74,7 @@ export function useOpenProject(serverId: string | null): (path: string) => Promi
|
|||||||
kind: "draft",
|
kind: "draft",
|
||||||
draftId: generateDraftId(),
|
draftId: generateDraftId(),
|
||||||
}),
|
}),
|
||||||
replaceRoute: (route) => {
|
navigateToWorkspace,
|
||||||
router.replace(route as Href);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client, isConnected, mergeWorkspaces, normalizedServerId, setHasHydratedWorkspaces],
|
[client, isConnected, mergeWorkspaces, normalizedServerId, setHasHydratedWorkspaces],
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const { navigateMock } = vi.hoisted(() => ({
|
const { dismissToMock } = vi.hoisted(() => ({
|
||||||
navigateMock: vi.fn(),
|
dismissToMock: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("expo-router", () => ({
|
vi.mock("expo-router", () => ({
|
||||||
router: {
|
router: {
|
||||||
navigate: navigateMock,
|
dismissTo: dismissToMock,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -19,11 +19,11 @@ import {
|
|||||||
|
|
||||||
describe("navigateToWorkspace", () => {
|
describe("navigateToWorkspace", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
navigateMock.mockReset();
|
dismissToMock.mockReset();
|
||||||
syncNavigationActiveWorkspace({ current: null });
|
syncNavigationActiveWorkspace({ current: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses router navigation from a non-workspace route even when active selection is stale", () => {
|
it("dismisses to the workspace route from a non-workspace route even when active selection is stale", () => {
|
||||||
activateNavigationWorkspaceSelection({
|
activateNavigationWorkspaceSelection({
|
||||||
serverId: "server-1",
|
serverId: "server-1",
|
||||||
workspaceId: "workspace-a",
|
workspaceId: "workspace-a",
|
||||||
@@ -33,7 +33,7 @@ describe("navigateToWorkspace", () => {
|
|||||||
currentPathname: "/h/server-1/sessions",
|
currentPathname: "/h/server-1/sessions",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(navigateMock).toHaveBeenCalledWith("/h/server-1/workspace/workspace-b");
|
expect(dismissToMock).toHaveBeenCalledWith("/h/server-1/workspace/workspace-b");
|
||||||
expect(getNavigationActiveWorkspaceSelection()).toEqual({
|
expect(getNavigationActiveWorkspaceSelection()).toEqual({
|
||||||
serverId: "server-1",
|
serverId: "server-1",
|
||||||
workspaceId: "workspace-a",
|
workspaceId: "workspace-a",
|
||||||
@@ -50,7 +50,7 @@ describe("navigateToWorkspace", () => {
|
|||||||
currentPathname: "/h/server-1/workspace/workspace-a",
|
currentPathname: "/h/server-1/workspace/workspace-a",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(navigateMock).not.toHaveBeenCalled();
|
expect(dismissToMock).not.toHaveBeenCalled();
|
||||||
expect(getNavigationActiveWorkspaceSelection()).toEqual({
|
expect(getNavigationActiveWorkspaceSelection()).toEqual({
|
||||||
serverId: "server-1",
|
serverId: "server-1",
|
||||||
workspaceId: "workspace-b",
|
workspaceId: "workspace-b",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function shouldUseRetainedWorkspaceSwitch(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.currentPathname == null) {
|
if (input.currentPathname == null) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseHostWorkspaceRouteFromPathname(input.currentPathname) !== null;
|
return parseHostWorkspaceRouteFromPathname(input.currentPathname) !== null;
|
||||||
@@ -49,7 +49,7 @@ export function navigateToWorkspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const href = buildHostWorkspaceRoute(serverId, workspaceId);
|
const href = buildHostWorkspaceRoute(serverId, workspaceId);
|
||||||
router.navigate(href);
|
router.dismissTo(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkspaceNavigation() {
|
export function useWorkspaceNavigation() {
|
||||||
|
|||||||
@@ -78,13 +78,13 @@ import { useIsCompactFormFactor } from "@/constants/layout";
|
|||||||
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
||||||
import {
|
import {
|
||||||
buildHostOpenProjectRoute,
|
buildHostOpenProjectRoute,
|
||||||
buildHostWorkspaceRoute,
|
|
||||||
buildProjectsSettingsRoute,
|
buildProjectsSettingsRoute,
|
||||||
buildSettingsHostRoute,
|
buildSettingsHostRoute,
|
||||||
buildSettingsSectionRoute,
|
buildSettingsSectionRoute,
|
||||||
type SettingsSectionSlug,
|
type SettingsSectionSlug,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import { getLastNavigationWorkspaceRouteSelection } from "@/stores/navigation-active-workspace-store";
|
import { getLastNavigationWorkspaceRouteSelection } from "@/stores/navigation-active-workspace-store";
|
||||||
|
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// View model
|
// View model
|
||||||
@@ -963,9 +963,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
|||||||
const handleBackToWorkspace = useCallback(() => {
|
const handleBackToWorkspace = useCallback(() => {
|
||||||
const lastWorkspaceRoute = getLastNavigationWorkspaceRouteSelection();
|
const lastWorkspaceRoute = getLastNavigationWorkspaceRouteSelection();
|
||||||
if (lastWorkspaceRoute) {
|
if (lastWorkspaceRoute) {
|
||||||
router.replace(
|
navigateToWorkspace(lastWorkspaceRoute.serverId, lastWorkspaceRoute.workspaceId);
|
||||||
buildHostWorkspaceRoute(lastWorkspaceRoute.serverId, lastWorkspaceRoute.workspaceId),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (anyOnlineServerId) {
|
if (anyOnlineServerId) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { platformState, routerMock } = vi.hoisted(() => ({
|
|||||||
routerMock: {
|
routerMock: {
|
||||||
back: vi.fn(),
|
back: vi.fn(),
|
||||||
canGoBack: vi.fn(() => false),
|
canGoBack: vi.fn(() => false),
|
||||||
|
dismissTo: vi.fn(),
|
||||||
navigate: vi.fn(),
|
navigate: vi.fn(),
|
||||||
replace: vi.fn(),
|
replace: vi.fn(),
|
||||||
},
|
},
|
||||||
@@ -60,6 +61,7 @@ describe("prepareWorkspaceTab", () => {
|
|||||||
routerMock.back.mockReset();
|
routerMock.back.mockReset();
|
||||||
routerMock.canGoBack.mockReset();
|
routerMock.canGoBack.mockReset();
|
||||||
routerMock.canGoBack.mockReturnValue(false);
|
routerMock.canGoBack.mockReturnValue(false);
|
||||||
|
routerMock.dismissTo.mockReset();
|
||||||
routerMock.navigate.mockReset();
|
routerMock.navigate.mockReset();
|
||||||
routerMock.replace.mockReset();
|
routerMock.replace.mockReset();
|
||||||
syncNavigationActiveWorkspace({ current: null });
|
syncNavigationActiveWorkspace({ current: null });
|
||||||
@@ -106,6 +108,7 @@ describe("prepareWorkspaceTab", () => {
|
|||||||
|
|
||||||
expect(route).toBe("/h/server-1/workspace/b64_L3JlcG8vd29ya3RyZWU");
|
expect(route).toBe("/h/server-1/workspace/b64_L3JlcG8vd29ya3RyZWU");
|
||||||
expect(routerMock.back).toHaveBeenCalledOnce();
|
expect(routerMock.back).toHaveBeenCalledOnce();
|
||||||
|
expect(routerMock.dismissTo).not.toHaveBeenCalled();
|
||||||
expect(routerMock.replace).not.toHaveBeenCalled();
|
expect(routerMock.replace).not.toHaveBeenCalled();
|
||||||
|
|
||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { router, type Href } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { isNative } from "@/constants/platform";
|
import { isNative } from "@/constants/platform";
|
||||||
|
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
|
||||||
import {
|
import {
|
||||||
activateNavigationWorkspaceSelection,
|
activateNavigationWorkspaceSelection,
|
||||||
getLastNavigationWorkspaceRouteSelection,
|
getLastNavigationWorkspaceRouteSelection,
|
||||||
@@ -22,6 +23,7 @@ interface PrepareWorkspaceTabInput {
|
|||||||
|
|
||||||
interface NavigateToPreparedWorkspaceTabInput extends PrepareWorkspaceTabInput {
|
interface NavigateToPreparedWorkspaceTabInput extends PrepareWorkspaceTabInput {
|
||||||
navigationMethod?: "navigate" | "replace";
|
navigationMethod?: "navigate" | "replace";
|
||||||
|
currentPathname?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
|
function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
|
||||||
@@ -65,9 +67,13 @@ export function navigateToPreparedWorkspaceTab(input: NavigateToPreparedWorkspac
|
|||||||
}, 0);
|
}, 0);
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
router.replace(route as Href);
|
navigateToWorkspace(input.serverId, input.workspaceId, {
|
||||||
|
currentPathname: input.currentPathname,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
router.navigate(route as Href);
|
navigateToWorkspace(input.serverId, input.workspaceId, {
|
||||||
|
currentPathname: input.currentPathname,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user