Keep composer autocomplete visible after route hops (#1851)
* fix(app): keep composer autocomplete visible after route hops Workspace callers now express only the target workspace. The navigation helper chooses whether to pop to a mounted host route or fall back, and active-workspace tracking ignores stale hidden route params while app-wide routes are foregrounded. * test(app): address workspace navigation review * fix(app): parse decoded legacy workspace routes * fix(app): ignore decoded legacy tab routes * test(app): use canonical offline workspace route
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { NavigationContainerRefWithCurrent } from "@react-navigation/native";
|
||||
import {
|
||||
navigateToHostWorkspaceRoute,
|
||||
registerWorkspaceRouteNavigationRef,
|
||||
} from "./workspace-route-navigation";
|
||||
|
||||
function createNavigationRef(rootState: unknown, options: { ready?: boolean } = {}) {
|
||||
const dispatch = vi.fn();
|
||||
const navigationRef = {
|
||||
current: {
|
||||
isReady: () => options.ready ?? true,
|
||||
getRootState: () => rootState,
|
||||
dispatch,
|
||||
},
|
||||
} as unknown as NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>;
|
||||
|
||||
return { navigationRef, dispatch };
|
||||
}
|
||||
|
||||
describe("navigateToHostWorkspaceRoute", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
registerWorkspaceRouteNavigationRef({
|
||||
current: null,
|
||||
} as unknown as NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>)();
|
||||
});
|
||||
|
||||
it("falls back to route navigation when no host route is mounted yet", () => {
|
||||
const { navigationRef, dispatch } = createNavigationRef({
|
||||
key: "root-stack",
|
||||
routeNames: ["index", "settings/[section]", "h/[serverId]"],
|
||||
routes: [{ key: "settings-general", name: "settings/[section]" }],
|
||||
});
|
||||
registerWorkspaceRouteNavigationRef(navigationRef);
|
||||
const dismissTo = vi.fn();
|
||||
|
||||
navigateToHostWorkspaceRoute("/h/server-1/workspace/workspace-a", { dismissTo });
|
||||
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
expect(dismissTo).toHaveBeenCalledWith("/h/server-1/workspace/workspace-a");
|
||||
});
|
||||
|
||||
it("pops to the mounted host route and targets the requested workspace", () => {
|
||||
const { navigationRef, dispatch } = createNavigationRef({
|
||||
key: "root-stack",
|
||||
routeNames: ["index", "settings/[section]", "h/[serverId]"],
|
||||
routes: [
|
||||
{
|
||||
key: "host-server-1",
|
||||
name: "h/[serverId]",
|
||||
params: { serverId: "server-1" },
|
||||
},
|
||||
{ key: "settings-general", name: "settings/[section]" },
|
||||
],
|
||||
});
|
||||
registerWorkspaceRouteNavigationRef(navigationRef);
|
||||
const dismissTo = vi.fn();
|
||||
|
||||
navigateToHostWorkspaceRoute("/h/server-1/workspace/workspace-a", { dismissTo });
|
||||
|
||||
expect(dismissTo).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
type: "POP_TO",
|
||||
target: "root-stack",
|
||||
payload: {
|
||||
name: "h/[serverId]",
|
||||
params: {
|
||||
serverId: "server-1",
|
||||
screen: "workspace/[workspaceId]/index",
|
||||
params: {
|
||||
serverId: "server-1",
|
||||
workspaceId: "workspace-a",
|
||||
},
|
||||
pop: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,13 +8,17 @@ import {
|
||||
const ROOT_HOST_ROUTE_NAME = "h/[serverId]";
|
||||
const HOST_WORKSPACE_ROUTE_NAME = "workspace/[workspaceId]/index";
|
||||
|
||||
interface NavigateToHostWorkspaceRouteDeps {
|
||||
dismissTo(route: string): void;
|
||||
}
|
||||
|
||||
const defaultNavigateToHostWorkspaceRouteDeps: NavigateToHostWorkspaceRouteDeps = {
|
||||
dismissTo: (route) => router.dismissTo(route as Href),
|
||||
};
|
||||
|
||||
let rootNavigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList> | null =
|
||||
null;
|
||||
|
||||
interface NavigateToHostWorkspaceRouteOptions {
|
||||
popToExistingHostRoute?: boolean;
|
||||
}
|
||||
|
||||
export function registerWorkspaceRouteNavigationRef(
|
||||
ref: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>,
|
||||
): () => void {
|
||||
@@ -26,33 +30,38 @@ export function registerWorkspaceRouteNavigationRef(
|
||||
};
|
||||
}
|
||||
|
||||
function findStackKeyWithRouteName(state: unknown, routeName: string): string | null {
|
||||
function findStackKeyWithMountedRouteName(state: unknown, routeName: string): string | null {
|
||||
if (!state || typeof state !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = state as {
|
||||
key?: unknown;
|
||||
routeNames?: unknown;
|
||||
routes?: unknown;
|
||||
};
|
||||
if (
|
||||
typeof candidate.key === "string" &&
|
||||
Array.isArray(candidate.routeNames) &&
|
||||
candidate.routeNames.includes(routeName)
|
||||
) {
|
||||
return candidate.key;
|
||||
}
|
||||
|
||||
if (!Array.isArray(candidate.routes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof candidate.key === "string" &&
|
||||
candidate.routes.some(
|
||||
(route) =>
|
||||
!!route && typeof route === "object" && (route as { name?: unknown }).name === routeName,
|
||||
)
|
||||
) {
|
||||
return candidate.key;
|
||||
}
|
||||
|
||||
for (const route of candidate.routes) {
|
||||
if (!route || typeof route !== "object") {
|
||||
continue;
|
||||
}
|
||||
const childKey = findStackKeyWithRouteName((route as { state?: unknown }).state, routeName);
|
||||
const childKey = findStackKeyWithMountedRouteName(
|
||||
(route as { state?: unknown }).state,
|
||||
routeName,
|
||||
);
|
||||
if (childKey) {
|
||||
return childKey;
|
||||
}
|
||||
@@ -69,7 +78,7 @@ function dispatchHostWorkspacePopTo(route: string): boolean {
|
||||
}
|
||||
|
||||
const rootState = navigation.getRootState();
|
||||
const target = findStackKeyWithRouteName(rootState, ROOT_HOST_ROUTE_NAME);
|
||||
const target = findStackKeyWithMountedRouteName(rootState, ROOT_HOST_ROUTE_NAME);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
@@ -99,11 +108,11 @@ function dispatchHostWorkspacePopTo(route: string): boolean {
|
||||
|
||||
export function navigateToHostWorkspaceRoute(
|
||||
route: string,
|
||||
options: NavigateToHostWorkspaceRouteOptions = {},
|
||||
deps: NavigateToHostWorkspaceRouteDeps = defaultNavigateToHostWorkspaceRouteDeps,
|
||||
): void {
|
||||
if (options.popToExistingHostRoute && dispatchHostWorkspacePopTo(route)) {
|
||||
if (dispatchHostWorkspacePopTo(route)) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.dismissTo(route as Href);
|
||||
deps.dismissTo(route);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user