Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ import { getConnectionAwareBrandHeaderOptions } from "./WorkspaceConnectionTitle

export function HomeRouteScreen() {
const { width: windowWidth } = useWindowDimensions();
const { layout, panes } = useAdaptiveWorkspaceLayout();
const { layout, panes, primarySidebarSearchQuery, setPrimarySidebarSearchQuery } =
useAdaptiveWorkspaceLayout();
const projects = useProjects();
const threads = useNavigationThreadShells();
const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState();
const { savedConnectionsById } = useSavedRemoteConnections();
const navigation = useNavigation();
const [searchQuery, setSearchQuery] = useState("");
const [localSearchQuery, setLocalSearchQuery] = useState("");
// Android's controlled fields share search text across compact/split remounts.
// Keep iOS search local because its native fields do not restore text from this state.
const searchQuery = Platform.OS === "android" ? primarySidebarSearchQuery : localSearchQuery;
const setSearchQuery =
Platform.OS === "android" ? setPrimarySidebarSearchQuery : setLocalSearchQuery;
const handleSelectThread = useHomeThreadSelection();
const handleNewThreadOnBranch = useCallback(
(thread: EnvironmentThreadShell) => {
Expand Down Expand Up @@ -93,10 +99,13 @@ export function HomeRouteScreen() {
() => new Set(environments.map((environment) => environment.environmentId)),
[environments],
);
const { options: listOptions, setSelectedEnvironmentId } =
useHomeListOptions(availableEnvironmentIds);
const {
options: listOptions,
setSelectedEnvironmentId,
setSelectedProjectKey,
} = useHomeListOptions(availableEnvironmentIds);
const selectedEnvironmentId = listOptions.selectedEnvironmentId;
const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);
const selectedProjectKey = listOptions.selectedProjectKey;
const projectFilterOptions = useMemo(
() =>
buildHomeProjectScopes({
Expand All @@ -116,7 +125,7 @@ export function HomeRouteScreen() {
) {
setSelectedProjectKey(null);
}
}, [projectFilterOptions, selectedProjectKey]);
}, [projectFilterOptions, selectedProjectKey, setSelectedProjectKey]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// In split layouts the persistent sidebar IS the thread list — Home becomes
// an empty detail pane so selecting a thread never transitions layouts.
Expand Down
95 changes: 95 additions & 0 deletions apps/mobile/src/features/home/home-list-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { EnvironmentId } from "@t3tools/contracts";
import { act, createElement, useLayoutEffect } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";

import { HomeListOptionsProvider, useHomeListOptions } from "./home-list-options";

describe("home list options across layout changes", () => {
const environmentId = EnvironmentId.make("environment-1");
const availableEnvironmentIds = new Set([environmentId]);
let root: Root;
let latest: ReturnType<typeof useHomeListOptions>;

function ThreadList() {
const state = useHomeListOptions(availableEnvironmentIds);
useLayoutEffect(() => {
latest = state;
});
return null;
}

async function showLayout(layout: "compact" | "sidebar") {
await act(() => {
root.render(
createElement(
HomeListOptionsProvider,
{ projectGroupingMode: "repository" },
// Replace the list while keeping the workspace provider mounted.
createElement(ThreadList, { key: layout }),
),
);
});
}

beforeEach(() => {
// The hook probe renders no DOM, but ReactDOM needs an event target.
const document = {
nodeType: 9,
addEventListener() {},
removeEventListener() {},
};
const container = {
nodeType: 1,
tagName: "DIV",
namespaceURI: "http://www.w3.org/1999/xhtml",
ownerDocument: document,
addEventListener() {},
removeEventListener() {},
};
vi.stubGlobal("document", document);
vi.stubGlobal("window", { document, HTMLIFrameElement: EventTarget });
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
root = createRoot(container as unknown as HTMLElement);
});

afterEach(async () => {
await act(() => root.unmount());
vi.unstubAllGlobals();
});

it.each([
["compact", "sidebar"],
["sidebar", "compact"],
] as const)(
"retains filters through repeated %s → %s round trips and keeps clears",
async (initialLayout, otherLayout) => {
await showLayout(initialLayout);

for (const [selectedEnvironmentId, selectedProjectKey] of [
[environmentId, "repository:one"],
[environmentId, "repository:two"],
[environmentId, null],
[null, null],
[null, "repository:one"],
] as const) {
await act(() => {
latest.setSelectedEnvironmentId(selectedEnvironmentId);
latest.setSelectedProjectKey(selectedProjectKey);
});
for (let cycle = 0; cycle < 3; cycle += 1) {
for (const layout of [otherLayout, initialLayout]) {
await showLayout(layout);
expect(latest.options).toMatchObject({
selectedEnvironmentId,
selectedProjectKey,
projectGroupingMode: "repository",
});
}
}
// Make the next selection (or clear it) from the opposite layout.
await showLayout(otherLayout);
}
},
);
});
9 changes: 9 additions & 0 deletions apps/mobile/src/features/home/home-list-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { HomeProjectSortOrder } from "./homeThreadList";

export interface HomeListOptions {
readonly selectedEnvironmentId: EnvironmentId | null;
readonly selectedProjectKey: string | null;
readonly projectSortOrder: HomeProjectSortOrder;
}

Expand All @@ -26,6 +27,7 @@ export interface ResolvedHomeListOptions extends HomeListOptions {
function defaultHomeListOptions(): HomeListOptions {
return {
selectedEnvironmentId: null,
selectedProjectKey: null,
projectSortOrder:
DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual"
? "updated_at"
Expand Down Expand Up @@ -78,12 +80,19 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm
const setSelectedEnvironmentId = useCallback((value: EnvironmentId | null) => {
setOptions((current) => ({ ...current, selectedEnvironmentId: value }));
}, []);
const setSelectedProjectKey = useCallback(
(value: string | null) => {
setOptions((current) => ({ ...current, selectedProjectKey: value }));
},
[setOptions],
);
const setProjectSortOrder = useCallback((value: HomeProjectSortOrder) => {
setOptions((current) => ({ ...current, projectSortOrder: value }));
}, []);
return {
options: resolvedOptions,
setSelectedEnvironmentId,
setSelectedProjectKey,
setProjectSortOrder,
} as const;
}
18 changes: 13 additions & 5 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ function ThreadNavigationSidebarPane(
() => new Set(environments.map((environment) => environment.environmentId)),
[environments],
);
const { options, setSelectedEnvironmentId } = useHomeListOptions(availableEnvironmentIds);
const { options, setSelectedEnvironmentId, setSelectedProjectKey } =
useHomeListOptions(availableEnvironmentIds);
const searchEnvironmentIds = useMemo(
() =>
options.selectedEnvironmentId === null
Expand Down Expand Up @@ -203,7 +204,7 @@ function ThreadNavigationSidebarPane(
() => new Set(threadSearch.matches.map(threadSearchMatchKey)),
[threadSearch.matches],
);
const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);
const selectedProjectKey = options.selectedProjectKey;
const projectScopes = useMemo(
() =>
buildHomeProjectScopes({
Expand Down Expand Up @@ -250,7 +251,7 @@ function ThreadNavigationSidebarPane(
) {
setSelectedProjectKey(null);
}
}, [projectFilterOptions, selectedProjectKey]);
}, [projectFilterOptions, selectedProjectKey, setSelectedProjectKey]);
const selectedProjectRefs = useMemo(
() =>
selectedProjectScope === null
Expand Down Expand Up @@ -530,7 +531,7 @@ function ThreadNavigationSidebarPane(
return;
}
},
[environments, projectFilterOptions, setSelectedEnvironmentId],
[environments, projectFilterOptions, setSelectedEnvironmentId, setSelectedProjectKey],
);

const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null);
Expand Down Expand Up @@ -818,7 +819,14 @@ function ThreadNavigationSidebarPane(
onEnvironmentChange: setSelectedEnvironmentId,
onProjectChange: setSelectedProjectKey,
}),
[environments, options, projectFilterOptions, selectedProjectKey, setSelectedEnvironmentId],
[
environments,
options,
projectFilterOptions,
selectedProjectKey,
setSelectedEnvironmentId,
setSelectedProjectKey,
],
);
const nativeHeaderItems = useMemo(
() =>
Expand Down
Loading