Restore the previous workspace on app start (#1101)
* Restore previous workspace on app start * Guard workspace startup hydration * Extract last workspace selection store
This commit is contained in:
@@ -171,14 +171,24 @@ describe("resolveStartupRedirectRoute", () => {
|
|||||||
expect(selection).toEqual({ serverId: "server-1", workspaceId: "workspace-a" });
|
expect(selection).toEqual({ serverId: "server-1", workspaceId: "workspace-a" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to the host root when the persisted workspace targets a different server", () => {
|
it("leaves persisted workspace navigation to the workspace navigator when another host is first online", () => {
|
||||||
const route = resolveStartupRedirectRoute({
|
const route = resolveStartupRedirectRoute({
|
||||||
...baseInput,
|
...baseInput,
|
||||||
anyOnlineHostServerId: "server-2",
|
anyOnlineHostServerId: "server-2",
|
||||||
workspaceSelection: { serverId: "server-1", workspaceId: "workspace-a" },
|
workspaceSelection: { serverId: "server-1", workspaceId: "workspace-a" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(route).toBe("/h/server-2");
|
expect(route).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves the persisted workspace when another host is first online", () => {
|
||||||
|
const selection = resolveStartupWorkspaceSelection({
|
||||||
|
...baseInput,
|
||||||
|
anyOnlineHostServerId: "server-2",
|
||||||
|
workspaceSelection: { serverId: "server-1", workspaceId: "workspace-a" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(selection).toEqual({ serverId: "server-1", workspaceId: "workspace-a" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to the host root when no persisted workspace exists", () => {
|
it("redirects to the host root when no persisted workspace exists", () => {
|
||||||
|
|||||||
@@ -79,11 +79,7 @@ export function resolveStartupWorkspaceSelection(
|
|||||||
if (!input.isWorkspaceSelectionLoaded) {
|
if (!input.isWorkspaceSelectionLoaded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (
|
if (!input.workspaceSelection) {
|
||||||
!input.anyOnlineHostServerId ||
|
|
||||||
!input.workspaceSelection ||
|
|
||||||
input.workspaceSelection.serverId !== input.anyOnlineHostServerId
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return input.workspaceSelection;
|
return input.workspaceSelection;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const { navigateToWorkspaceMock, redirectMock, state } = vi.hoisted(() => {
|
|||||||
storeReady: false,
|
storeReady: false,
|
||||||
} as HostRuntimeBootstrapState,
|
} as HostRuntimeBootstrapState,
|
||||||
anyOnlineHostServerId: null as string | null,
|
anyOnlineHostServerId: null as string | null,
|
||||||
|
isWorkspaceSelectionLoaded: true,
|
||||||
workspaceSelection: null as ActiveWorkspaceSelection | null,
|
workspaceSelection: null as ActiveWorkspaceSelection | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +52,8 @@ vi.mock("@/screens/startup-splash-screen", () => ({
|
|||||||
|
|
||||||
vi.mock("@/stores/navigation-active-workspace-store", () => ({
|
vi.mock("@/stores/navigation-active-workspace-store", () => ({
|
||||||
navigateToWorkspace: navigateToWorkspaceMock,
|
navigateToWorkspace: navigateToWorkspaceMock,
|
||||||
useActiveWorkspaceSelection: () => state.workspaceSelection,
|
useIsLastWorkspaceSelectionHydrated: () => state.isWorkspaceSelectionLoaded,
|
||||||
|
useLastWorkspaceSelection: () => state.workspaceSelection,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("Index route startup navigation", () => {
|
describe("Index route startup navigation", () => {
|
||||||
@@ -68,6 +70,7 @@ describe("Index route startup navigation", () => {
|
|||||||
storeReady: false,
|
storeReady: false,
|
||||||
};
|
};
|
||||||
state.anyOnlineHostServerId = null;
|
state.anyOnlineHostServerId = null;
|
||||||
|
state.isWorkspaceSelectionLoaded = true;
|
||||||
state.workspaceSelection = null;
|
state.workspaceSelection = null;
|
||||||
navigateToWorkspaceMock.mockReset();
|
navigateToWorkspaceMock.mockReset();
|
||||||
redirectMock.mockReset();
|
redirectMock.mockReset();
|
||||||
@@ -98,6 +101,17 @@ describe("Index route startup navigation", () => {
|
|||||||
expect(redirectMock).not.toHaveBeenCalled();
|
expect(redirectMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the startup splash while the persisted workspace selection has not loaded", async () => {
|
||||||
|
state.anyOnlineHostServerId = "server-1";
|
||||||
|
state.isWorkspaceSelectionLoaded = false;
|
||||||
|
|
||||||
|
await renderIndex();
|
||||||
|
|
||||||
|
expect(container.querySelector("[data-testid='startup-splash']")).not.toBeNull();
|
||||||
|
expect(navigateToWorkspaceMock).not.toHaveBeenCalled();
|
||||||
|
expect(redirectMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("restores the persisted workspace when the online host matches its server id", async () => {
|
it("restores the persisted workspace when the online host matches its server id", async () => {
|
||||||
state.anyOnlineHostServerId = "server-1";
|
state.anyOnlineHostServerId = "server-1";
|
||||||
state.workspaceSelection = { serverId: "server-1", workspaceId: "workspace-a" };
|
state.workspaceSelection = { serverId: "server-1", workspaceId: "workspace-a" };
|
||||||
@@ -111,13 +125,16 @@ describe("Index route startup navigation", () => {
|
|||||||
expect(container.querySelector("[data-testid='startup-splash']")).not.toBeNull();
|
expect(container.querySelector("[data-testid='startup-splash']")).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("navigates to the host root when the persisted workspace targets a different server", async () => {
|
it("restores the persisted workspace even when the first online host is different", async () => {
|
||||||
state.anyOnlineHostServerId = "server-2";
|
state.anyOnlineHostServerId = "server-2";
|
||||||
state.workspaceSelection = { serverId: "server-1", workspaceId: "workspace-a" };
|
state.workspaceSelection = { serverId: "server-1", workspaceId: "workspace-a" };
|
||||||
|
|
||||||
await renderIndex();
|
await renderIndex();
|
||||||
|
|
||||||
expect(redirectMock).toHaveBeenCalledWith("/h/server-2");
|
expect(navigateToWorkspaceMock).toHaveBeenCalledWith("server-1", "workspace-a", {
|
||||||
|
currentPathname: "/",
|
||||||
|
});
|
||||||
|
expect(redirectMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("navigates to the host root when no persisted workspace exists", async () => {
|
it("navigates to the host root when no persisted workspace exists", async () => {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
} from "@/app/host-runtime-bootstrap";
|
} from "@/app/host-runtime-bootstrap";
|
||||||
import {
|
import {
|
||||||
navigateToWorkspace,
|
navigateToWorkspace,
|
||||||
useActiveWorkspaceSelection,
|
useIsLastWorkspaceSelectionHydrated,
|
||||||
|
useLastWorkspaceSelection,
|
||||||
} from "@/stores/navigation-active-workspace-store";
|
} from "@/stores/navigation-active-workspace-store";
|
||||||
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||||
|
|
||||||
@@ -18,20 +19,21 @@ export default function Index() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const bootstrapState = useHostRuntimeBootstrapState();
|
const bootstrapState = useHostRuntimeBootstrapState();
|
||||||
const anyOnlineHostServerId = useEarliestOnlineHostServerId();
|
const anyOnlineHostServerId = useEarliestOnlineHostServerId();
|
||||||
const workspaceSelection = useActiveWorkspaceSelection();
|
const workspaceSelection = useLastWorkspaceSelection();
|
||||||
|
const isWorkspaceSelectionLoaded = useIsLastWorkspaceSelectionHydrated();
|
||||||
|
|
||||||
const redirectRoute = resolveStartupRedirectRoute({
|
const redirectRoute = resolveStartupRedirectRoute({
|
||||||
pathname,
|
pathname,
|
||||||
anyOnlineHostServerId,
|
anyOnlineHostServerId,
|
||||||
workspaceSelection,
|
workspaceSelection,
|
||||||
isWorkspaceSelectionLoaded: true,
|
isWorkspaceSelectionLoaded,
|
||||||
hasGivenUpWaitingForHost: bootstrapState.hasGivenUpWaitingForHost,
|
hasGivenUpWaitingForHost: bootstrapState.hasGivenUpWaitingForHost,
|
||||||
});
|
});
|
||||||
const startupWorkspaceSelection = resolveStartupWorkspaceSelection({
|
const startupWorkspaceSelection = resolveStartupWorkspaceSelection({
|
||||||
pathname,
|
pathname,
|
||||||
anyOnlineHostServerId,
|
anyOnlineHostServerId,
|
||||||
workspaceSelection,
|
workspaceSelection,
|
||||||
isWorkspaceSelectionLoaded: true,
|
isWorkspaceSelectionLoaded,
|
||||||
hasGivenUpWaitingForHost: bootstrapState.hasGivenUpWaitingForHost,
|
hasGivenUpWaitingForHost: bootstrapState.hasGivenUpWaitingForHost,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
createLastWorkspaceSelectionStore,
|
||||||
|
type ActiveWorkspaceSelection,
|
||||||
|
type LastWorkspaceSelectionStorage,
|
||||||
|
} from "./last-workspace-selection";
|
||||||
|
|
||||||
|
class DelayedWorkspaceSelectionStorage implements LastWorkspaceSelectionStorage {
|
||||||
|
private finishRead: (value: string | null) => void = () => {};
|
||||||
|
private readonly pendingRead = new Promise<string | null>((resolve) => {
|
||||||
|
this.finishRead = resolve;
|
||||||
|
});
|
||||||
|
private saved: string | null = null;
|
||||||
|
|
||||||
|
read(): Promise<string | null> {
|
||||||
|
return this.pendingRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(value: string): Promise<void> {
|
||||||
|
this.saved = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishHydrationWith(selection: ActiveWorkspaceSelection | null) {
|
||||||
|
this.finishRead(selection ? JSON.stringify(selection) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSavedSelection(): ActiveWorkspaceSelection | null {
|
||||||
|
return this.saved ? (JSON.parse(this.saved) as ActiveWorkspaceSelection) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("last workspace selection", () => {
|
||||||
|
it("hydrates the saved workspace selection", async () => {
|
||||||
|
const storage = new DelayedWorkspaceSelectionStorage();
|
||||||
|
const store = createLastWorkspaceSelectionStore(storage);
|
||||||
|
const hydration = store.hydrate();
|
||||||
|
|
||||||
|
storage.finishHydrationWith({ serverId: "server-saved", workspaceId: "workspace-saved" });
|
||||||
|
await hydration;
|
||||||
|
|
||||||
|
expect(store.getSelection()).toEqual({
|
||||||
|
serverId: "server-saved",
|
||||||
|
workspaceId: "workspace-saved",
|
||||||
|
});
|
||||||
|
expect(store.isHydrated()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a newer workspace selection when storage hydration finishes late", async () => {
|
||||||
|
const storage = new DelayedWorkspaceSelectionStorage();
|
||||||
|
const store = createLastWorkspaceSelectionStore(storage);
|
||||||
|
const hydration = store.hydrate();
|
||||||
|
|
||||||
|
store.remember({ serverId: "server-new", workspaceId: "workspace-new" });
|
||||||
|
storage.finishHydrationWith({ serverId: "server-old", workspaceId: "workspace-old" });
|
||||||
|
await hydration;
|
||||||
|
|
||||||
|
expect(store.getSelection()).toEqual({
|
||||||
|
serverId: "server-new",
|
||||||
|
workspaceId: "workspace-new",
|
||||||
|
});
|
||||||
|
expect(storage.getSavedSelection()).toEqual({
|
||||||
|
serverId: "server-new",
|
||||||
|
workspaceId: "workspace-new",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
export interface ActiveWorkspaceSelection {
|
||||||
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LastWorkspaceSelectionStorage {
|
||||||
|
read(): Promise<string | null>;
|
||||||
|
write(value: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorkspaceSelection(input: unknown): ActiveWorkspaceSelection | null {
|
||||||
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const record = input as Record<string, unknown>;
|
||||||
|
const serverId = typeof record.serverId === "string" ? record.serverId.trim() : "";
|
||||||
|
const workspaceId = typeof record.workspaceId === "string" ? record.workspaceId.trim() : "";
|
||||||
|
if (!serverId || !workspaceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { serverId, workspaceId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStoredWorkspaceSelection(stored: string | null): ActiveWorkspaceSelection | null {
|
||||||
|
if (!stored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return normalizeWorkspaceSelection(JSON.parse(stored));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLastWorkspaceSelectionStore(storage: LastWorkspaceSelectionStorage) {
|
||||||
|
let selection: ActiveWorkspaceSelection | null = null;
|
||||||
|
let hydrated = false;
|
||||||
|
let hydrationPromise: Promise<void> | null = null;
|
||||||
|
let revision = 0;
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
function notifyListeners() {
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remember(next: ActiveWorkspaceSelection) {
|
||||||
|
const normalized = normalizeWorkspaceSelection(next);
|
||||||
|
if (!normalized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
selection?.serverId === normalized.serverId &&
|
||||||
|
selection.workspaceId === normalized.workspaceId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selection = normalized;
|
||||||
|
revision += 1;
|
||||||
|
notifyListeners();
|
||||||
|
void storage.write(JSON.stringify(normalized)).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrate(): Promise<void> {
|
||||||
|
if (hydrationPromise) {
|
||||||
|
return hydrationPromise;
|
||||||
|
}
|
||||||
|
const hydrationRevision = revision;
|
||||||
|
hydrationPromise = storage
|
||||||
|
.read()
|
||||||
|
.then((stored) => {
|
||||||
|
if (revision === hydrationRevision) {
|
||||||
|
selection = parseStoredWorkspaceSelection(stored);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (revision === hydrationRevision) {
|
||||||
|
selection = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
hydrated = true;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
return hydrationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSelection: () => selection,
|
||||||
|
hydrate,
|
||||||
|
isHydrated: () => hydrated,
|
||||||
|
remember,
|
||||||
|
subscribe: (listener: () => void): (() => void) => {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ const pathnameState = vi.hoisted(() => ({
|
|||||||
const localParamsState = vi.hoisted(() => ({
|
const localParamsState = vi.hoisted(() => ({
|
||||||
value: {} as { serverId?: string | string[]; workspaceId?: string | string[] },
|
value: {} as { serverId?: string | string[]; workspaceId?: string | string[] },
|
||||||
}));
|
}));
|
||||||
|
const asyncStorageMock = vi.hoisted(() => ({
|
||||||
|
getItem: vi.fn(async () => null as string | null),
|
||||||
|
setItem: vi.fn(async () => undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("expo-router", () => ({
|
vi.mock("expo-router", () => ({
|
||||||
router: routerMock,
|
router: routerMock,
|
||||||
@@ -20,6 +24,10 @@ vi.mock("expo-router", () => ({
|
|||||||
usePathname: () => pathnameState.value,
|
usePathname: () => pathnameState.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@react-native-async-storage/async-storage", () => ({
|
||||||
|
default: asyncStorageMock,
|
||||||
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
navigateToLastWorkspace,
|
navigateToLastWorkspace,
|
||||||
navigateToWorkspace,
|
navigateToWorkspace,
|
||||||
@@ -29,6 +37,7 @@ import {
|
|||||||
describe("workspace navigation", () => {
|
describe("workspace navigation", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
routerMock.dismissTo.mockReset();
|
routerMock.dismissTo.mockReset();
|
||||||
|
asyncStorageMock.setItem.mockClear();
|
||||||
pathnameState.value = "/";
|
pathnameState.value = "/";
|
||||||
localParamsState.value = {};
|
localParamsState.value = {};
|
||||||
});
|
});
|
||||||
@@ -41,6 +50,10 @@ describe("workspace navigation", () => {
|
|||||||
navigateToWorkspace("server-1", "workspace-a");
|
navigateToWorkspace("server-1", "workspace-a");
|
||||||
|
|
||||||
expect(routerMock.dismissTo).toHaveBeenCalledWith("/h/server-1/workspace/workspace-a");
|
expect(routerMock.dismissTo).toHaveBeenCalledWith("/h/server-1/workspace/workspace-a");
|
||||||
|
expect(asyncStorageMock.setItem).toHaveBeenCalledWith(
|
||||||
|
"paseo:last-workspace-route-selection",
|
||||||
|
JSON.stringify({ serverId: "server-1", workspaceId: "workspace-a" }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reads the active workspace from the current route", () => {
|
it("reads the active workspace from the current route", () => {
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { router, useLocalSearchParams, usePathname, type Href } from "expo-router";
|
import { router, useLocalSearchParams, usePathname, type Href } from "expo-router";
|
||||||
|
import { useEffect, useSyncExternalStore } from "react";
|
||||||
|
import {
|
||||||
|
createLastWorkspaceSelectionStore,
|
||||||
|
type ActiveWorkspaceSelection,
|
||||||
|
type LastWorkspaceSelectionStorage,
|
||||||
|
} from "@/stores/last-workspace-selection";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||||
import { pickAttentionAgent } from "@/utils/agent-attention";
|
import { pickAttentionAgent } from "@/utils/agent-attention";
|
||||||
@@ -12,16 +19,34 @@ import {
|
|||||||
resolveWorkspaceMapKeyByIdentity,
|
resolveWorkspaceMapKeyByIdentity,
|
||||||
} from "@/utils/workspace-execution";
|
} from "@/utils/workspace-execution";
|
||||||
|
|
||||||
export interface ActiveWorkspaceSelection {
|
export type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection";
|
||||||
serverId: string;
|
|
||||||
workspaceId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NavigateToWorkspaceOptions {
|
interface NavigateToWorkspaceOptions {
|
||||||
currentPathname?: string | null;
|
currentPathname?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastWorkspaceSelection: ActiveWorkspaceSelection | null = null;
|
const LAST_WORKSPACE_SELECTION_STORAGE_KEY = "paseo:last-workspace-route-selection";
|
||||||
|
|
||||||
|
const lastWorkspaceSelectionStorage: LastWorkspaceSelectionStorage = {
|
||||||
|
read: () => AsyncStorage.getItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY),
|
||||||
|
write: (value) => AsyncStorage.setItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY, value),
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastWorkspaceSelectionStore = createLastWorkspaceSelectionStore(
|
||||||
|
lastWorkspaceSelectionStorage,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function hydrateLastWorkspaceSelection(): Promise<void> {
|
||||||
|
return lastWorkspaceSelectionStore.hydrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastWorkspaceSelection(): ActiveWorkspaceSelection | null {
|
||||||
|
return lastWorkspaceSelectionStore.getSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIsLastWorkspaceSelectionHydrated(): boolean {
|
||||||
|
return lastWorkspaceSelectionStore.isHydrated();
|
||||||
|
}
|
||||||
|
|
||||||
function getParamValue(value: string | string[] | undefined): string {
|
function getParamValue(value: string | string[] | undefined): string {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
@@ -74,16 +99,17 @@ export function navigateToWorkspace(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
lastWorkspaceSelection = { serverId, workspaceId };
|
lastWorkspaceSelectionStore.remember({ serverId, workspaceId });
|
||||||
const route = buildHostWorkspaceRoute(serverId, workspaceId) as Href;
|
const route = buildHostWorkspaceRoute(serverId, workspaceId) as Href;
|
||||||
router.dismissTo(route);
|
router.dismissTo(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function navigateToLastWorkspace(): boolean {
|
export function navigateToLastWorkspace(): boolean {
|
||||||
if (!lastWorkspaceSelection) {
|
const selection = lastWorkspaceSelectionStore.getSelection();
|
||||||
|
if (!selection) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
navigateToWorkspace(lastWorkspaceSelection.serverId, lastWorkspaceSelection.workspaceId);
|
navigateToWorkspace(selection.serverId, selection.workspaceId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +121,31 @@ export function useActiveWorkspaceSelection(): ActiveWorkspaceSelection | null {
|
|||||||
const selection =
|
const selection =
|
||||||
parseHostWorkspaceRouteFromPathname(usePathname()) ??
|
parseHostWorkspaceRouteFromPathname(usePathname()) ??
|
||||||
parseWorkspaceSelectionFromRouteParams(params);
|
parseWorkspaceSelectionFromRouteParams(params);
|
||||||
if (selection) {
|
const serverId = selection?.serverId ?? null;
|
||||||
lastWorkspaceSelection = selection;
|
const workspaceId = selection?.workspaceId ?? null;
|
||||||
}
|
useEffect(() => {
|
||||||
|
if (!serverId || !workspaceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastWorkspaceSelectionStore.remember({ serverId, workspaceId });
|
||||||
|
}, [serverId, workspaceId]);
|
||||||
return selection;
|
return selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useLastWorkspaceSelection(): ActiveWorkspaceSelection | null {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
lastWorkspaceSelectionStore.subscribe,
|
||||||
|
getLastWorkspaceSelection,
|
||||||
|
getLastWorkspaceSelection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsLastWorkspaceSelectionHydrated(): boolean {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
lastWorkspaceSelectionStore.subscribe,
|
||||||
|
getIsLastWorkspaceSelectionHydrated,
|
||||||
|
getIsLastWorkspaceSelectionHydrated,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hydrateLastWorkspaceSelection();
|
||||||
|
|||||||
Reference in New Issue
Block a user