From 0fe666b72e47fed745d5d4c5c714dcabaa305b11 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 11 Apr 2026 11:17:07 -0500 Subject: [PATCH] Add join request badge to access sidebar --- .../CompanySettingsSidebar.test.tsx | 34 ++++++++++++++++++- ui/src/components/CompanySettingsSidebar.tsx | 32 +++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/ui/src/components/CompanySettingsSidebar.test.tsx b/ui/src/components/CompanySettingsSidebar.test.tsx index 8b33169357a..fa9a777af76 100644 --- a/ui/src/components/CompanySettingsSidebar.test.tsx +++ b/ui/src/components/CompanySettingsSidebar.test.tsx @@ -2,10 +2,14 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { CompanySettingsSidebar } from "./CompanySettingsSidebar"; const sidebarNavItemMock = vi.hoisted(() => vi.fn()); +const mockSidebarBadgesApi = vi.hoisted(() => ({ + get: vi.fn(), +})); vi.mock("@/lib/router", () => ({ Link: ({ @@ -25,6 +29,7 @@ vi.mock("@/lib/router", () => ({ vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ + selectedCompanyId: "company-1", selectedCompany: { id: "company-1", name: "Paperclip" }, }), })); @@ -41,21 +46,39 @@ vi.mock("./SidebarNavItem", () => ({ to: string; label: string; end?: boolean; + badge?: number; }) => { sidebarNavItemMock(props); return
{props.label}
; }, })); +vi.mock("@/api/sidebarBadges", () => ({ + sidebarBadgesApi: mockSidebarBadgesApi, +})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; +async function flushReact() { + await act(async () => { + await Promise.resolve(); + await new Promise((resolve) => window.setTimeout(resolve, 0)); + }); +} + describe("CompanySettingsSidebar", () => { let container: HTMLDivElement; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); + mockSidebarBadgesApi.get.mockResolvedValue({ + inbox: 0, + approvals: 0, + failedRuns: 0, + joinRequests: 2, + }); }); afterEach(() => { @@ -66,10 +89,18 @@ describe("CompanySettingsSidebar", () => { it("renders the company back link and the settings sections in the sidebar", async () => { const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); await act(async () => { - root.render(); + root.render( + + + , + ); }); + await flushReact(); expect(container.textContent).toContain("Paperclip"); expect(container.textContent).toContain("Company Settings"); @@ -87,6 +118,7 @@ describe("CompanySettingsSidebar", () => { expect.objectContaining({ to: "/company/settings/access", label: "Access", + badge: 2, end: true, }), ); diff --git a/ui/src/components/CompanySettingsSidebar.tsx b/ui/src/components/CompanySettingsSidebar.tsx index 598b448a659..6339cf1fddc 100644 --- a/ui/src/components/CompanySettingsSidebar.tsx +++ b/ui/src/components/CompanySettingsSidebar.tsx @@ -1,12 +1,34 @@ +import { useQuery } from "@tanstack/react-query"; import { ChevronLeft, MailPlus, Settings, Shield, SlidersHorizontal } from "lucide-react"; +import { sidebarBadgesApi } from "@/api/sidebarBadges"; +import { ApiError } from "@/api/client"; import { Link } from "@/lib/router"; +import { queryKeys } from "@/lib/queryKeys"; import { useCompany } from "@/context/CompanyContext"; import { useSidebar } from "@/context/SidebarContext"; import { SidebarNavItem } from "./SidebarNavItem"; export function CompanySettingsSidebar() { - const { selectedCompany } = useCompany(); + const { selectedCompany, selectedCompanyId } = useCompany(); const { isMobile, setSidebarOpen } = useSidebar(); + const { data: badges } = useQuery({ + queryKey: selectedCompanyId + ? queryKeys.sidebarBadges(selectedCompanyId) + : ["sidebar-badges", "__disabled__"] as const, + queryFn: async () => { + try { + return await sidebarBadgesApi.get(selectedCompanyId!); + } catch (error) { + if (error instanceof ApiError && (error.status === 401 || error.status === 403)) { + return null; + } + throw error; + } + }, + enabled: !!selectedCompanyId, + retry: false, + refetchInterval: 15_000, + }); return (