Fix workspace navigation regression on web (#772)
Centralize workspace navigation behind a small route-derived API so the workspace shell reads active selection from the current Expo Router route instead of a separately synchronized store.
Remove the stale layout-driven active-workspace write that regressed web after 02e5b564.
This commit is contained in:
@@ -13,6 +13,18 @@ vi.hoisted(() => {
|
||||
(globalThis as unknown as { __DEV__: boolean }).__DEV__ = false;
|
||||
});
|
||||
|
||||
const pathnameState = vi.hoisted(() => ({
|
||||
value: "/",
|
||||
}));
|
||||
|
||||
vi.mock("expo-router", () => ({
|
||||
router: {
|
||||
dismissTo: vi.fn(),
|
||||
},
|
||||
useLocalSearchParams: () => ({}),
|
||||
usePathname: () => pathnameState.value,
|
||||
}));
|
||||
|
||||
import {
|
||||
createSidebarWorkspaceEntry,
|
||||
type SidebarProjectEntry,
|
||||
@@ -28,12 +40,7 @@ import type { HostProfile } from "@/types/host-connection";
|
||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
|
||||
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
||||
import {
|
||||
activateNavigationWorkspaceSelection,
|
||||
syncNavigationActiveWorkspace,
|
||||
useIsNavigationProjectActive,
|
||||
useIsNavigationWorkspaceSelected,
|
||||
} from "@/stores/navigation-active-workspace-store";
|
||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
|
||||
vi.mock("@react-native-async-storage/async-storage", () => ({
|
||||
default: {
|
||||
@@ -222,10 +229,11 @@ function ProjectActiveProbe({
|
||||
project: SidebarProjectEntry;
|
||||
counts: RenderCounts;
|
||||
}): null {
|
||||
useIsNavigationProjectActive({
|
||||
serverId,
|
||||
workspaceIds: project.workspaces.map((entry) => entry.workspaceId),
|
||||
});
|
||||
const activeSelection = useActiveWorkspaceSelection();
|
||||
const isActive =
|
||||
activeSelection?.serverId === serverId &&
|
||||
project.workspaces.some((entry) => entry.workspaceId === activeSelection.workspaceId);
|
||||
void isActive;
|
||||
incrementRecord(counts.projectSelection, project.projectKey);
|
||||
return null;
|
||||
}
|
||||
@@ -239,7 +247,10 @@ function WorkspaceSelectionProbe({
|
||||
workspaceId: string;
|
||||
counts: RenderCounts;
|
||||
}): null {
|
||||
useIsNavigationWorkspaceSelected({ serverId, workspaceId });
|
||||
const activeSelection = useActiveWorkspaceSelection();
|
||||
const selected =
|
||||
activeSelection?.serverId === serverId && activeSelection.workspaceId === workspaceId;
|
||||
void selected;
|
||||
incrementRecord(counts.rowSelection, workspaceId);
|
||||
return null;
|
||||
}
|
||||
@@ -304,12 +315,16 @@ async function renderProbe(counts: RenderCounts): Promise<{ root: Root; containe
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(<SidebarFrameProbe counts={counts} />);
|
||||
renderSidebarFrame(root, counts);
|
||||
});
|
||||
resetCounts(counts);
|
||||
return { root, container };
|
||||
}
|
||||
|
||||
function renderSidebarFrame(root: Root, counts: RenderCounts) {
|
||||
root.render(<SidebarFrameProbe counts={counts} />);
|
||||
}
|
||||
|
||||
describe("sidebar workspace render isolation", () => {
|
||||
let root: Root | null = null;
|
||||
let container: HTMLElement | null = null;
|
||||
@@ -328,7 +343,7 @@ describe("sidebar workspace render isolation", () => {
|
||||
container?.remove();
|
||||
container = null;
|
||||
act(() => {
|
||||
syncNavigationActiveWorkspace({ current: null });
|
||||
pathnameState.value = "/";
|
||||
getHostRuntimeStore().syncHosts([]);
|
||||
useSessionStore.getState().clearSession(SERVER_ID);
|
||||
useSidebarOrderStore.setState({
|
||||
@@ -417,7 +432,7 @@ describe("sidebar workspace render isolation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("isolates active selection updates to affected row and project boolean probes", async () => {
|
||||
it("updates active selection probes from the active workspace route", async () => {
|
||||
const counts: RenderCounts = {
|
||||
frame: 0,
|
||||
headers: {},
|
||||
@@ -427,29 +442,28 @@ describe("sidebar workspace render isolation", () => {
|
||||
};
|
||||
|
||||
act(() => {
|
||||
activateNavigationWorkspaceSelection({
|
||||
serverId: SERVER_ID,
|
||||
workspaceId: "a-one",
|
||||
});
|
||||
pathnameState.value = `/h/${SERVER_ID}/workspace/a-one`;
|
||||
});
|
||||
({ root, container } = await renderProbe(counts));
|
||||
|
||||
act(() => {
|
||||
activateNavigationWorkspaceSelection({
|
||||
serverId: SERVER_ID,
|
||||
workspaceId: "b-two",
|
||||
});
|
||||
pathnameState.value = `/h/${SERVER_ID}/workspace/b-two`;
|
||||
if (root) {
|
||||
renderSidebarFrame(root, counts);
|
||||
}
|
||||
});
|
||||
|
||||
expect(counts.frame).toBe(0);
|
||||
expect(counts.headers).toEqual({});
|
||||
expect(counts.rows).toEqual({});
|
||||
expect(counts.frame).toBe(1);
|
||||
expect(counts.projectSelection).toEqual({
|
||||
"project-a": 1,
|
||||
"project-b": 1,
|
||||
});
|
||||
expect(counts.rowSelection).toEqual({
|
||||
"a-main": 1,
|
||||
"a-one": 1,
|
||||
"a-two": 1,
|
||||
"b-main": 1,
|
||||
"b-one": 1,
|
||||
"b-two": 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,10 +97,7 @@ import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||
import { type PrHint, useWorkspacePrHint } from "@/hooks/use-checkout-pr-status-query";
|
||||
import { buildSidebarProjectRowModel } from "@/utils/sidebar-project-row-model";
|
||||
import {
|
||||
useIsNavigationProjectActive,
|
||||
useIsNavigationWorkspaceSelected,
|
||||
} from "@/stores/navigation-active-workspace-store";
|
||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
||||
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
||||
@@ -1482,6 +1479,7 @@ function WorkspaceRowWithMenu({
|
||||
isCreating?: boolean;
|
||||
}) {
|
||||
const toast = useToast();
|
||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||
const archiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree);
|
||||
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
||||
const workspaceDirectory = resolveWorkspaceExecutionDirectory({
|
||||
@@ -1502,8 +1500,9 @@ function WorkspaceRowWithMenu({
|
||||
redirectIfArchivingActiveWorkspace({
|
||||
serverId: workspace.serverId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
activeWorkspaceSelection,
|
||||
});
|
||||
}, [workspace.serverId, workspace.workspaceId]);
|
||||
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
|
||||
|
||||
const handleArchiveWorktree = useCallback(() => {
|
||||
if (isArchiving) {
|
||||
@@ -1686,13 +1685,15 @@ function NonGitProjectRowWithMenuContent({
|
||||
}) {
|
||||
const toast = useToast();
|
||||
const contextMenu = useContextMenu();
|
||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
||||
const redirectAfterArchive = useCallback(() => {
|
||||
redirectIfArchivingActiveWorkspace({
|
||||
serverId: workspace.serverId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
activeWorkspaceSelection,
|
||||
});
|
||||
}, [workspace.serverId, workspace.workspaceId]);
|
||||
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
|
||||
|
||||
const handleArchiveWorkspace = useCallback(() => {
|
||||
if (isArchivingWorkspace) {
|
||||
@@ -1839,11 +1840,11 @@ function FlattenedProjectRow({
|
||||
selectionEnabled: boolean;
|
||||
}) {
|
||||
const workspace = useSidebarWorkspaceEntry(serverId, rowModel.workspace.workspaceId);
|
||||
const selected = useIsNavigationWorkspaceSelected({
|
||||
serverId,
|
||||
workspaceId: rowModel.workspace.workspaceId,
|
||||
enabled: selectionEnabled,
|
||||
});
|
||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||
const selected =
|
||||
selectionEnabled &&
|
||||
activeWorkspaceSelection?.serverId === serverId &&
|
||||
activeWorkspaceSelection.workspaceId === rowModel.workspace.workspaceId;
|
||||
|
||||
if (!workspace) {
|
||||
return null;
|
||||
@@ -1970,11 +1971,11 @@ function WorkspaceRow({
|
||||
selectionEnabled: boolean;
|
||||
}) {
|
||||
const hydratedWorkspace = useSidebarWorkspaceEntry(workspace.serverId, workspace.workspaceId);
|
||||
const selected = useIsNavigationWorkspaceSelected({
|
||||
serverId: workspace.serverId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
enabled: selectionEnabled,
|
||||
});
|
||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||
const selected =
|
||||
selectionEnabled &&
|
||||
activeWorkspaceSelection?.serverId === workspace.serverId &&
|
||||
activeWorkspaceSelection.workspaceId === workspace.workspaceId;
|
||||
|
||||
if (!hydratedWorkspace) {
|
||||
return null;
|
||||
@@ -2050,11 +2051,11 @@ function ProjectBlock({
|
||||
() => project.workspaces.map((workspace) => workspace.workspaceId),
|
||||
[project.workspaces],
|
||||
);
|
||||
const isProjectActive = useIsNavigationProjectActive({
|
||||
serverId,
|
||||
workspaceIds: projectWorkspaceIds,
|
||||
enabled: selectionEnabled,
|
||||
});
|
||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||
const isProjectActive =
|
||||
selectionEnabled &&
|
||||
activeWorkspaceSelection?.serverId === serverId &&
|
||||
projectWorkspaceIds.includes(activeWorkspaceSelection.workspaceId);
|
||||
|
||||
const renderWorkspaceRow = useCallback(
|
||||
(
|
||||
|
||||
@@ -195,7 +195,6 @@ export function WorkspaceSetupDialog() {
|
||||
serverId: pendingWorkspaceSetup.serverId,
|
||||
workspaceId,
|
||||
target,
|
||||
navigationMethod: pendingWorkspaceSetup.navigationMethod,
|
||||
});
|
||||
},
|
||||
[clearWorkspaceSetup, pendingWorkspaceSetup],
|
||||
|
||||
@@ -67,7 +67,7 @@ vi.mock("expo-router", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/navigation-active-workspace-store", () => ({
|
||||
useNavigationActiveWorkspaceSelection: () => activeSelection.value,
|
||||
useActiveWorkspaceSelection: () => activeSelection.value,
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/session-store-hooks", () => ({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect } from "react";
|
||||
import { useSidebarCallouts } from "@/contexts/sidebar-callout-context";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import { useHostRuntimeClient } from "@/runtime/host-runtime";
|
||||
import { useNavigationActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { buildProjectSettingsRoute } from "@/utils/host-routes";
|
||||
@@ -45,7 +45,7 @@ function hasSetupCommands(config: PaseoConfigRaw): boolean {
|
||||
}
|
||||
|
||||
export function WorktreeSetupCalloutSource() {
|
||||
const selection = useNavigationActiveWorkspaceSelection();
|
||||
const selection = useActiveWorkspaceSelection();
|
||||
const activeProject = useWorkspaceFields(
|
||||
selection?.serverId ?? null,
|
||||
selection?.workspaceId ?? null,
|
||||
|
||||
Reference in New Issue
Block a user