mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(ui): drive project detail selection from the ?project= url param
Opening a project kept selectedProjectId in useState, so the URL never changed; the detail view could not be linked or reloaded and browser Back skipped past the Projects page entirely. Selection now lives in the ?project= query param via nuqs with history: push, matching how Teams, Organizations and Virtual Keys already work.
This commit is contained in:
parent
c3a8962c00
commit
dc178acf5b
2 changed files with 55 additions and 4 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { UrlUpdateEvent } from "nuqs/adapters/testing";
|
||||
import { renderWithProviders, screen, waitFor, within } from "../../../../../tests/test-utils";
|
||||
import { ProjectsPage } from "./ProjectsPage";
|
||||
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";
|
||||
|
|
@ -20,7 +21,12 @@ vi.mock("./ProjectModals/CreateProjectModal", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("./ProjectDetailsPage", () => ({
|
||||
ProjectDetail: ({ projectId }: { projectId: string }) => <div data-testid="project-detail">{projectId}</div>,
|
||||
ProjectDetail: ({ projectId, onBack }: { projectId: string; onBack: () => void }) => (
|
||||
<div data-testid="project-detail">
|
||||
{projectId}
|
||||
<button onClick={onBack}>Back to projects</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockProjects: ProjectResponse[] = [
|
||||
|
|
@ -200,6 +206,47 @@ describe("ProjectsPage", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should open the detail view directly from a ?project= deep link", () => {
|
||||
mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />, { searchParams: "?project=proj-2" });
|
||||
|
||||
expect(screen.getByTestId("project-detail")).toHaveTextContent("proj-2");
|
||||
expect(screen.queryByRole("heading", { name: /projects/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should push ?project= as a new history entry when a project is opened", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
|
||||
mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />, { onUrlUpdate });
|
||||
|
||||
await user.click(screen.getByText("proj-1"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUrlUpdate).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
queryString: "?project=proj-1",
|
||||
options: expect.objectContaining({ history: "push" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear ?project= and return to the list when the detail view is closed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
|
||||
mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false });
|
||||
renderWithProviders(<ProjectsPage />, { searchParams: "?project=proj-1", onUrlUpdate });
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /back to projects/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUrlUpdate).toHaveBeenLastCalledWith(expect.objectContaining({ queryString: "" }));
|
||||
});
|
||||
expect(screen.queryByTestId("project-detail")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should resolve team alias from the teams list in the Team column", () => {
|
||||
mockUseTeams.mockReturnValue({
|
||||
data: [{ team_id: "team-1", team_alias: "Engineering", models: [] }],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
|
|||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Button, Flex, Input, Layout, Space, theme, Typography } from "antd";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { useMemo, useState } from "react";
|
||||
import { CreateProjectModal } from "./ProjectModals/CreateProjectModal";
|
||||
import { ProjectDetail } from "./ProjectDetailsPage";
|
||||
|
|
@ -16,7 +17,10 @@ export function ProjectsPage() {
|
|||
const { data: projects, isLoading } = useProjects();
|
||||
const { data: teams, isLoading: isTeamsLoading } = useTeams();
|
||||
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||
const [selectedProjectId, setSelectedProjectId] = useQueryState(
|
||||
"project",
|
||||
parseAsString.withOptions({ history: "push" }),
|
||||
);
|
||||
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
|
|
@ -44,7 +48,7 @@ export function ProjectsPage() {
|
|||
}, [projects, searchText, teamAliasMap]);
|
||||
|
||||
if (selectedProjectId) {
|
||||
return <ProjectDetail projectId={selectedProjectId} onBack={() => setSelectedProjectId(null)} />;
|
||||
return <ProjectDetail projectId={selectedProjectId} onBack={() => void setSelectedProjectId(null)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -76,7 +80,7 @@ export function ProjectsPage() {
|
|||
projects={filteredProjects}
|
||||
isLoading={isLoading}
|
||||
isFiltered={searchText.trim().length > 0}
|
||||
onProjectClick={setSelectedProjectId}
|
||||
onProjectClick={(id) => void setSelectedProjectId(id)}
|
||||
teamAliasMap={teamAliasMap}
|
||||
isTeamsLoading={isTeamsLoading}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue