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
48 changes: 43 additions & 5 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1475,18 +1475,46 @@ describe("resolveSidebarRouteReveal", () => {
const hidden = makeThreadFixture({ projectId: ProjectId.make("project-other") });
const hiddenKey = "environment-test:thread-test";
const findThread = (key: string) => (key === hiddenKey ? hidden : undefined);
const scopeKeys = ["scoped"];
const scopeGroups = [
{
projectKey: "scoped",
memberProjectRefs: [{ environmentId: "environment-test", projectId: "project-scoped" }],
},
{
projectKey: "other",
memberProjectRefs: [{ environmentId: "environment-test", projectId: "project-other" }],
},
];

it("widens the scope for a newly opened thread the scope hides", () => {
it("adds the project of a newly opened thread the scope hides", () => {
expect(
resolveSidebarRouteReveal({
reveal: { observedKey: "environment-test:other", pendingKey: null },
routeKey: hiddenKey,
isSearching: false,
scopeKeys,
scopedProjectKeys,
scopeGroups,
delegatedUnderParent: new Set(),
findThread,
}),
).toEqual({ pendingKey: hiddenKey, widensScope: true });
).toEqual({ pendingKey: hiddenKey, widensScope: true, scopeKeys: ["scoped", "other"] });
});

it("widens to all projects for a hidden thread outside every scopable project", () => {
expect(
resolveSidebarRouteReveal({
reveal: { observedKey: "environment-test:other", pendingKey: null },
routeKey: hiddenKey,
isSearching: false,
scopeKeys,
scopedProjectKeys,
scopeGroups: scopeGroups.slice(0, 1),
delegatedUnderParent: new Set(),
findThread,
}),
).toEqual({ pendingKey: hiddenKey, widensScope: true, scopeKeys: [] });
});

it("never widens for a thread whose reveal already completed", () => {
Expand All @@ -1496,11 +1524,13 @@ describe("resolveSidebarRouteReveal", () => {
reveal: { observedKey: hiddenKey, pendingKey: null },
routeKey: hiddenKey,
isSearching: false,
scopeKeys,
scopedProjectKeys,
scopeGroups,
delegatedUnderParent: new Set(),
findThread,
}),
).toEqual({ pendingKey: null, widensScope: false });
).toEqual({ pendingKey: null, widensScope: false, scopeKeys });
});

it("widens again for a thread reopened before the thread between them listed its row", () => {
Expand All @@ -1512,12 +1542,18 @@ describe("resolveSidebarRouteReveal", () => {
reveal,
routeKey,
isSearching: false,
scopeKeys,
scopedProjectKeys,
scopeGroups,
delegatedUnderParent: new Set(),
findThread,
});
resolve("environment-test:unlisted");
expect(resolve(hiddenKey)).toEqual({ pendingKey: hiddenKey, widensScope: true });
expect(resolve(hiddenKey)).toEqual({
pendingKey: hiddenKey,
widensScope: true,
scopeKeys: ["scoped", "other"],
});
});

it("holds a pending reveal without widening while searching", () => {
Expand All @@ -1526,11 +1562,13 @@ describe("resolveSidebarRouteReveal", () => {
reveal: { observedKey: hiddenKey, pendingKey: hiddenKey },
routeKey: hiddenKey,
isSearching: true,
scopeKeys,
scopedProjectKeys,
scopeGroups,
delegatedUnderParent: new Set(),
findThread,
}),
).toEqual({ pendingKey: hiddenKey, widensScope: false });
).toEqual({ pendingKey: hiddenKey, widensScope: false, scopeKeys });
});
});

Expand Down
25 changes: 14 additions & 11 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1122,15 +1122,18 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
// scrolled away; a remount starts at the top, so each mount scrolls to the open
// thread once. Render decides the widening, so the first frame of the new route
// already lists the row; the active row's ref callback scrolls to it.
const { widensScope: revealWidensScope } = resolveSidebarRouteReveal({
reveal: routeReveal,
routeKey: routeRowThreadKey,
isSearching: isSearchingThreads,
scopedProjectKeys: chosenScopeProjectKeys,
delegatedUnderParent,
findThread: (key) => threads.find((candidate) => threadShellKey(candidate) === key),
});
const projectScopeKeys = revealWidensScope ? [] : chosenProjectScopeKeys;
const { widensScope: revealWidensScope, scopeKeys: projectScopeKeys } = resolveSidebarRouteReveal(
{
reveal: routeReveal,
routeKey: routeRowThreadKey,
isSearching: isSearchingThreads,
scopeKeys: chosenProjectScopeKeys,
scopedProjectKeys: chosenScopeProjectKeys,
scopeGroups,
delegatedUnderParent,
findThread: (key) => threads.find((candidate) => threadShellKey(candidate) === key),
},
);
const projectScopeKey = projectScopeKeys.length === 0 ? null : projectScopeKeys.join("\n");
const toggleProjectScopeKey = (key: string) =>
setProjectScopeKeys(
Expand All @@ -1146,8 +1149,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
useEffect(() => {
if (!revealWidensScope) return;
clearSelection();
setPersistedProjectScopeKeys([]);
}, [clearSelection, revealWidensScope, setPersistedProjectScopeKeys]);
setPersistedProjectScopeKeys(projectScopeKey === null ? [] : projectScopeKey.split("\n"));
}, [clearSelection, revealWidensScope, projectScopeKey, setPersistedProjectScopeKeys]);
const revealActiveRow = useActiveRowReveal(routeReveal, routeRowThreadKey);
const showProjectEnvironments = projectGroupsSpanEnvironments(projectGroups);
const scratchMachineLabelFor = (
Expand Down
38 changes: 33 additions & 5 deletions apps/web/src/components/sidebar/routeReveal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,35 @@ type RevealableThread = Parameters<typeof filterSidebarV2VisibleThreads>[0][numb

// Render records the route before deciding, so the first render of a new route
// already widens the scope. Only a route reveal widens: the once-per-mount
// scroll never changes a scope the user chose.
// scroll never changes a scope the user chose. Widening adds the thread's project
// to the scope; only a thread outside every scopable project widens to all projects.
export function resolveSidebarRouteReveal<T extends RevealableThread>(input: {
readonly reveal: SidebarRouteReveal;
readonly routeKey: string | null;
readonly isSearching: boolean;
readonly scopeKeys: readonly string[];
readonly scopedProjectKeys: ReadonlySet<string> | null;
readonly scopeGroups: readonly {
readonly projectKey: string;
readonly memberProjectRefs: readonly { environmentId: string; projectId: string }[];
}[];
readonly delegatedUnderParent: ReadonlySet<string>;
readonly findThread: (key: string) => T | undefined;
}): { readonly pendingKey: string | null; readonly widensScope: boolean } {
const { reveal, routeKey, isSearching, scopedProjectKeys, delegatedUnderParent, findThread } =
input;
}): {
readonly pendingKey: string | null;
readonly widensScope: boolean;
readonly scopeKeys: readonly string[];
} {
const {
reveal,
routeKey,
isSearching,
scopeKeys,
scopedProjectKeys,
scopeGroups,
delegatedUnderParent,
findThread,
} = input;
const pendingKey = pendingSidebarRouteReveal(reveal, routeKey);
// Search results replace the thread list; the reveal waits for it to return.
const thread = pendingKey === null || isSearching ? undefined : findThread(pendingKey);
Expand All @@ -68,5 +86,15 @@ export function resolveSidebarRouteReveal<T extends RevealableThread>(input: {
scopedProjectKeys !== null &&
filterSidebarV2VisibleThreads([thread], scopedProjectKeys, delegatedUnderParent).length === 0 &&
filterSidebarV2VisibleThreads([thread], null, delegatedUnderParent).length === 1;
return { pendingKey, widensScope };
if (!widensScope) return { pendingKey, widensScope, scopeKeys };
const threadProjectKey = scopeGroups.find((group) =>
group.memberProjectRefs.some(
(ref) => ref.environmentId === thread.environmentId && ref.projectId === thread.projectId,
),
)?.projectKey;
return {
pendingKey,
widensScope,
scopeKeys: threadProjectKey === undefined ? [] : [...scopeKeys, threadProjectKey],
};
}
Loading