diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index 100b210bd0d..3ddff1a6b19 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -89,7 +89,7 @@ export const queryKeys = { }, access: { invites: (companyId: string, state: string = "all", limit: number = 20) => - ["access", "invites", companyId, state, limit] as const, + ["access", "invites", "paginated-v1", companyId, state, limit] as const, joinRequests: (companyId: string, status: string = "pending_approval") => ["access", "join-requests", companyId, status] as const, companyMembers: (companyId: string) => ["access", "company-members", companyId] as const, diff --git a/ui/src/pages/CompanyInvites.test.tsx b/ui/src/pages/CompanyInvites.test.tsx index 130d6ca0221..274f2f24899 100644 --- a/ui/src/pages/CompanyInvites.test.tsx +++ b/ui/src/pages/CompanyInvites.test.tsx @@ -6,6 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { MemoryRouter } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { CompanyInvites } from "./CompanyInvites"; +import { queryKeys } from "@/lib/queryKeys"; const listInvitesMock = vi.hoisted(() => vi.fn()); const createCompanyInviteMock = vi.hoisted(() => vi.fn()); @@ -227,4 +228,40 @@ describe("CompanyInvites", () => { root.unmount(); }); }); + + it("ignores legacy cached invite arrays and refetches paginated history", async () => { + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + queryClient.setQueryData(["access", "invites", "company-1", "all"], inviteHistory.slice(0, 2)); + + await act(async () => { + root.render( + + + + + , + ); + }); + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("Board User 25"); + expect(container.textContent).not.toContain("Board User 5"); + expect(listInvitesMock).toHaveBeenCalledWith("company-1", { limit: 20, offset: 0 }); + expect(queryClient.getQueryData(queryKeys.access.invites("company-1", "all", 20))).toMatchObject({ + pages: [ + { + invites: expect.any(Array), + nextOffset: 20, + }, + ], + }); + + await act(async () => { + root.unmount(); + }); + }); }); diff --git a/ui/src/pages/CompanyInvites.tsx b/ui/src/pages/CompanyInvites.tsx index efdcef08c22..4bc8ffe7fe1 100644 --- a/ui/src/pages/CompanyInvites.tsx +++ b/ui/src/pages/CompanyInvites.tsx @@ -39,6 +39,11 @@ const inviteRoleOptions = [ const INVITE_HISTORY_PAGE_SIZE = 20; +function isInviteHistoryRow(value: unknown): value is Awaited>["invites"][number] { + if (!value || typeof value !== "object") return false; + return "id" in value && "state" in value && "createdAt" in value; +} + export function CompanyInvites() { const { selectedCompany, selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); @@ -95,7 +100,10 @@ export function CompanyInvites() { getNextPageParam: (lastPage) => lastPage.nextOffset ?? undefined, }); const inviteHistory = useMemo( - () => invitesQuery.data?.pages.flatMap((page) => page.invites) ?? [], + () => + invitesQuery.data?.pages.flatMap((page) => + Array.isArray(page?.invites) ? page.invites.filter(isInviteHistoryRow) : [], + ) ?? [], [invitesQuery.data?.pages], );