Bound retained workspace screens (#1472)
* fix(app): prune retained workspace deck entries * fix(app): narrow workspace deck retention selectors * fix(app): preserve deck lru during route transitions * test(app): update startup reconnect expectation
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import {
|
||||
pruneMountedWorkspaceSelections,
|
||||
shouldKeepWorkspaceDeckEntryMounted,
|
||||
} from "@/screens/workspace/workspace-deck-retention";
|
||||
|
||||
function workspace(workspaceId: string, serverId = "server"): ActiveWorkspaceSelection {
|
||||
return { serverId, workspaceId };
|
||||
}
|
||||
|
||||
function mountedWorkspaceIds(selections: ActiveWorkspaceSelection[]): string[] {
|
||||
return selections.map((selection) => selection.workspaceId);
|
||||
}
|
||||
|
||||
describe("pruneMountedWorkspaceSelections", () => {
|
||||
it("keeps the active workspace and the two most recent inactive workspaces", () => {
|
||||
const mountedAfterA = pruneMountedWorkspaceSelections({
|
||||
currentSelections: [],
|
||||
activeSelection: workspace("A"),
|
||||
});
|
||||
const mountedAfterB = pruneMountedWorkspaceSelections({
|
||||
currentSelections: mountedAfterA,
|
||||
activeSelection: workspace("B"),
|
||||
});
|
||||
const mountedAfterC = pruneMountedWorkspaceSelections({
|
||||
currentSelections: mountedAfterB,
|
||||
activeSelection: workspace("C"),
|
||||
});
|
||||
const mountedAfterD = pruneMountedWorkspaceSelections({
|
||||
currentSelections: mountedAfterC,
|
||||
activeSelection: workspace("D"),
|
||||
});
|
||||
|
||||
expect(mountedWorkspaceIds(mountedAfterD)).toEqual(["D", "C", "B"]);
|
||||
});
|
||||
|
||||
it("retains the active workspace", () => {
|
||||
const mountedSelections = pruneMountedWorkspaceSelections({
|
||||
currentSelections: [workspace("A")],
|
||||
activeSelection: workspace("A"),
|
||||
});
|
||||
|
||||
expect(mountedWorkspaceIds(mountedSelections)).toEqual(["A"]);
|
||||
});
|
||||
|
||||
it("deduplicates retained workspace selections", () => {
|
||||
const mountedSelections = pruneMountedWorkspaceSelections({
|
||||
currentSelections: [workspace("B"), workspace("A"), workspace("B")],
|
||||
activeSelection: workspace("A"),
|
||||
});
|
||||
|
||||
expect(mountedWorkspaceIds(mountedSelections)).toEqual(["A", "B"]);
|
||||
});
|
||||
|
||||
it("always allows at least the active workspace", () => {
|
||||
const mountedSelections = pruneMountedWorkspaceSelections({
|
||||
currentSelections: [workspace("A"), workspace("B")],
|
||||
activeSelection: workspace("C"),
|
||||
maxMountedWorkspaces: 0,
|
||||
});
|
||||
|
||||
expect(mountedWorkspaceIds(mountedSelections)).toEqual(["C"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldKeepWorkspaceDeckEntryMounted", () => {
|
||||
it("keeps the active workspace mounted even when it is missing from hydrated workspaces", () => {
|
||||
expect(
|
||||
shouldKeepWorkspaceDeckEntryMounted({
|
||||
isActive: true,
|
||||
hasHydratedWorkspaces: true,
|
||||
workspaceExists: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps inactive workspaces mounted until workspace hydration finishes", () => {
|
||||
expect(
|
||||
shouldKeepWorkspaceDeckEntryMounted({
|
||||
isActive: false,
|
||||
hasHydratedWorkspaces: false,
|
||||
workspaceExists: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("unmounts inactive workspaces that are gone after hydration", () => {
|
||||
expect(
|
||||
shouldKeepWorkspaceDeckEntryMounted({
|
||||
isActive: false,
|
||||
hasHydratedWorkspaces: true,
|
||||
workspaceExists: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps inactive workspaces that still exist after hydration", () => {
|
||||
expect(
|
||||
shouldKeepWorkspaceDeckEntryMounted({
|
||||
isActive: false,
|
||||
hasHydratedWorkspaces: true,
|
||||
workspaceExists: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
|
||||
export const WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES = 3;
|
||||
|
||||
interface PruneMountedWorkspaceSelectionsInput {
|
||||
currentSelections: ActiveWorkspaceSelection[];
|
||||
activeSelection: ActiveWorkspaceSelection | null;
|
||||
maxMountedWorkspaces?: number;
|
||||
}
|
||||
|
||||
interface WorkspaceDeckEntryMountInput {
|
||||
isActive: boolean;
|
||||
hasHydratedWorkspaces: boolean;
|
||||
workspaceExists: boolean;
|
||||
}
|
||||
|
||||
export function getWorkspaceSelectionKey(selection: ActiveWorkspaceSelection): string {
|
||||
return `${selection.serverId}:${selection.workspaceId}`;
|
||||
}
|
||||
|
||||
export function areWorkspaceSelectionsEqual(
|
||||
left: ActiveWorkspaceSelection | null,
|
||||
right: ActiveWorkspaceSelection | null,
|
||||
): boolean {
|
||||
return left?.serverId === right?.serverId && left?.workspaceId === right?.workspaceId;
|
||||
}
|
||||
|
||||
export function areWorkspaceSelectionListsEqual(
|
||||
left: ActiveWorkspaceSelection[],
|
||||
right: ActiveWorkspaceSelection[],
|
||||
): boolean {
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
return left.every((selection, index) =>
|
||||
areWorkspaceSelectionsEqual(selection, right[index] ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
export function pruneMountedWorkspaceSelections({
|
||||
currentSelections,
|
||||
activeSelection,
|
||||
maxMountedWorkspaces = WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES,
|
||||
}: PruneMountedWorkspaceSelectionsInput): ActiveWorkspaceSelection[] {
|
||||
if (!activeSelection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const maxSelections = Math.max(1, maxMountedWorkspaces);
|
||||
const nextSelections: ActiveWorkspaceSelection[] = [];
|
||||
const seenSelectionKeys = new Set<string>();
|
||||
|
||||
function appendSelection(selection: ActiveWorkspaceSelection): void {
|
||||
if (nextSelections.length >= maxSelections) {
|
||||
return;
|
||||
}
|
||||
const selectionKey = getWorkspaceSelectionKey(selection);
|
||||
if (seenSelectionKeys.has(selectionKey)) {
|
||||
return;
|
||||
}
|
||||
seenSelectionKeys.add(selectionKey);
|
||||
nextSelections.push(selection);
|
||||
}
|
||||
|
||||
appendSelection(activeSelection);
|
||||
|
||||
for (const selection of currentSelections) {
|
||||
if (areWorkspaceSelectionsEqual(selection, activeSelection)) {
|
||||
continue;
|
||||
}
|
||||
appendSelection(selection);
|
||||
}
|
||||
|
||||
return nextSelections;
|
||||
}
|
||||
|
||||
export function shouldKeepWorkspaceDeckEntryMounted({
|
||||
isActive,
|
||||
hasHydratedWorkspaces,
|
||||
workspaceExists,
|
||||
}: WorkspaceDeckEntryMountInput): boolean {
|
||||
if (isActive) {
|
||||
return true;
|
||||
}
|
||||
if (!hasHydratedWorkspaces) {
|
||||
return true;
|
||||
}
|
||||
return workspaceExists;
|
||||
}
|
||||
Reference in New Issue
Block a user