mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
Fix invite landing company cache shape
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e35daf5256
commit
387ce15e43
2 files changed
+68
-8
No files matched your search
@@ -6,6 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { InviteLandingPage } from "./InviteLanding";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
|
||||
const getInviteMock = vi.hoisted(() => vi.fn());
|
||||
const acceptInviteMock = vi.hoisted(() => vi.fn());
|
||||
@@ -594,6 +595,55 @@ describe("InviteLandingPage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes the shared company cache envelope before checking membership", async () => {
|
||||
acceptInviteMock.mockResolvedValue({
|
||||
id: "join-1",
|
||||
companyId: "company-1",
|
||||
requestType: "human",
|
||||
status: "pending_approval",
|
||||
});
|
||||
getSessionMock.mockResolvedValue({
|
||||
session: { id: "session-1", userId: "user-1" },
|
||||
user: {
|
||||
id: "user-1",
|
||||
name: "Jane Example",
|
||||
email: "jane@example.com",
|
||||
image: null,
|
||||
},
|
||||
});
|
||||
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
queryClient.setQueryData(queryKeys.companies.all, {
|
||||
companies: [],
|
||||
unauthorized: false,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MemoryRouter initialEntries={["/invite/pcp_invite_test"]}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Routes>
|
||||
<Route path="/invite/:token" element={<InviteLandingPage />} />
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
await flushReact();
|
||||
await flushReact();
|
||||
|
||||
expect(acceptInviteMock).toHaveBeenCalledWith("pcp_invite_test", { requestType: "human" });
|
||||
expect(container.textContent).toContain("Request to join Acme Robotics");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("waits for the membership check before showing invite acceptance to signed-in users", async () => {
|
||||
let resolveCompanies: ((value: Array<{ id: string; name: string }>) => void) | null = null;
|
||||
acceptInviteMock.mockResolvedValue({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared";
|
||||
import type { AgentAdapterType, JoinRequest } from "@paperclipai/shared";
|
||||
import type { AgentAdapterType, Company, JoinRequest } from "@paperclipai/shared";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CompanyPatternIcon } from "@/components/CompanyPatternIcon";
|
||||
import { useCompany } from "@/context/CompanyContext";
|
||||
@@ -17,6 +17,7 @@ import { formatDate } from "../lib/utils";
|
||||
|
||||
type AuthMode = "sign_in" | "sign_up";
|
||||
type AuthFeedback = { tone: "error" | "info"; message: string };
|
||||
type CompanyListQueryData = Company[] | { companies?: Company[]; unauthorized?: boolean };
|
||||
|
||||
const joinAdapterOptions: AgentAdapterType[] = [...AGENT_ADAPTER_TYPES];
|
||||
const ENABLED_INVITE_ADAPTERS = new Set([
|
||||
@@ -27,6 +28,14 @@ const ENABLED_INVITE_ADAPTERS = new Set([
|
||||
"pi_local",
|
||||
"cursor",
|
||||
]);
|
||||
const EMPTY_COMPANIES: Company[] = [];
|
||||
|
||||
function normalizeCompanyList(value: unknown): Company[] {
|
||||
if (Array.isArray(value)) return value as Company[];
|
||||
if (!value || typeof value !== "object") return EMPTY_COMPANIES;
|
||||
const companies = (value as { companies?: unknown }).companies;
|
||||
return Array.isArray(companies) ? (companies as Company[]) : EMPTY_COMPANIES;
|
||||
}
|
||||
|
||||
function readNestedString(value: unknown, path: string[]): string | null {
|
||||
let current: unknown = value;
|
||||
@@ -247,12 +256,14 @@ export function InviteLandingPage() {
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const companiesQuery = useQuery({
|
||||
const companiesQuery = useQuery<CompanyListQueryData, Error, Company[]>({
|
||||
queryKey: queryKeys.companies.all,
|
||||
queryFn: () => companiesApi.list(),
|
||||
enabled: !!sessionQuery.data && !!inviteQuery.data?.companyId,
|
||||
retry: false,
|
||||
select: normalizeCompanyList,
|
||||
});
|
||||
const accessibleCompanies = companiesQuery.data ?? EMPTY_COMPANIES;
|
||||
|
||||
useEffect(() => {
|
||||
if (token) rememberPendingInviteToken(token);
|
||||
@@ -264,14 +275,14 @@ export function InviteLandingPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!companiesQuery.data || !inviteQuery.data?.companyId) return;
|
||||
const isMember = companiesQuery.data.some(
|
||||
const isMember = accessibleCompanies.some(
|
||||
(c) => c.id === inviteQuery.data!.companyId
|
||||
);
|
||||
if (isMember) {
|
||||
clearPendingInviteToken(token);
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
}, [companiesQuery.data, inviteQuery.data, token, navigate]);
|
||||
}, [accessibleCompanies, companiesQuery.data, inviteQuery.data, token, navigate]);
|
||||
|
||||
const invite = inviteQuery.data;
|
||||
const isCheckingExistingMembership =
|
||||
@@ -280,9 +291,7 @@ export function InviteLandingPage() {
|
||||
companiesQuery.isLoading;
|
||||
const isCurrentMember =
|
||||
Boolean(invite?.companyId) &&
|
||||
Boolean(
|
||||
companiesQuery.data?.some((company) => company.id === invite?.companyId),
|
||||
);
|
||||
accessibleCompanies.some((company) => company.id === invite?.companyId);
|
||||
const companyName = invite?.companyName?.trim() || null;
|
||||
const companyDisplayName = companyName || "this Paperclip company";
|
||||
const companyLogoUrl = invite?.companyLogoUrl?.trim() || null;
|
||||
@@ -375,11 +384,12 @@ export function InviteLandingPage() {
|
||||
setAuthFeedback(null);
|
||||
rememberPendingInviteToken(token);
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session });
|
||||
const companies = await queryClient.fetchQuery({
|
||||
const companiesResult = await queryClient.fetchQuery<CompanyListQueryData>({
|
||||
queryKey: queryKeys.companies.all,
|
||||
queryFn: () => companiesApi.list(),
|
||||
retry: false,
|
||||
});
|
||||
const companies = normalizeCompanyList(companiesResult);
|
||||
|
||||
if (invite?.companyId && companies.some((company) => company.id === invite.companyId)) {
|
||||
clearPendingInviteToken(token);
|
||||
|
||||
Reference in new issue
Block a user