Skip to content
Merged
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
38 changes: 38 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
resolveThreadStatusPill,
shouldNavigateAfterProjectRemoval,
shouldClearThreadSelectionOnMouseDown,
sortLogicalProjectsForSidebar,
sortThreadsForSidebarV2,
sortProjectsForSidebar,
sortScopedProjectsForSidebar,
Expand Down Expand Up @@ -1353,3 +1354,40 @@ describe("sortScopedProjectsForSidebar", () => {
]);
});
});

describe("sortLogicalProjectsForSidebar", () => {
it("uses saved order only in manual mode and activity order otherwise", () => {
const olderProjectId = ProjectId.make("project-older");
const newerProjectId = ProjectId.make("project-newer");
const projects = [
{
...makeProject({ id: olderProjectId, title: "Older project" }),
projectKey: "logical-older",
memberProjectRefs: [{ environmentId: localEnvironmentId, projectId: olderProjectId }],
},
{
...makeProject({ id: newerProjectId, title: "Newer project" }),
projectKey: "logical-newer",
memberProjectRefs: [{ environmentId: localEnvironmentId, projectId: newerProjectId }],
},
];
const threads = [
makeThread({
projectId: olderProjectId,
updatedAt: "2026-03-09T10:01:00.000Z",
}),
makeThread({
id: ThreadId.make("thread-newer"),
projectId: newerProjectId,
updatedAt: "2026-03-09T10:05:00.000Z",
}),
];

expect(sortLogicalProjectsForSidebar(projects, threads, "manual")).toEqual(projects);
expect(
sortLogicalProjectsForSidebar(projects, threads, "updated_at").map(
(project) => project.projectKey,
),
).toEqual(["logical-newer", "logical-older"]);
});
});
46 changes: 46 additions & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ type ScopedSidebarThread = ThreadSortInput & {
archivedAt: string | null;
};

type LogicalSidebarProject = SidebarProject & {
projectKey: string;
memberProjectRefs: readonly {
environmentId: string;
projectId: string;
}[];
};

export type ThreadTraversalDirection = "previous" | "next";

export async function archiveSelectedThreadEntries<
Expand Down Expand Up @@ -727,6 +735,44 @@ export function sortProjectsForSidebar<
);
}

export function sortLogicalProjectsForSidebar<
TProject extends LogicalSidebarProject,
TThread extends ScopedSidebarThread,
>(
projects: readonly TProject[],
threads: readonly TThread[],
sortOrder: SidebarProjectSortOrder,
): TProject[] {
const groupKeyByProjectRef = new Map(
projects.flatMap((project) =>
project.memberProjectRefs.map(
(projectRef) =>
[`${projectRef.environmentId}\0${projectRef.projectId}`, project.projectKey] as const,
),
),
);
const threadsByProjectKey = new Map<string, TThread[]>();
for (const thread of threads) {
if (thread.archivedAt !== null) continue;
const projectKey = groupKeyByProjectRef.get(`${thread.environmentId}\0${thread.projectId}`);
if (!projectKey) continue;
const existing = threadsByProjectKey.get(projectKey);
if (existing) {
existing.push(thread);
} else {
threadsByProjectKey.set(projectKey, [thread]);
}
}

return sortProjectsByActivity(
projects,
sortOrder,
(project) => threadsByProjectKey.get(project.projectKey) ?? [],
(left, right) =>
left.title.localeCompare(right.title) || left.projectKey.localeCompare(right.projectKey),
);
}

/**
* Sorts the cross-environment project collection used by landing surfaces.
* Project ids are only unique within an environment, and archived threads
Expand Down
Loading
Loading