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],
);