feat(apps): confirm connections and cache MCP actions

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-20 21:20:02 +00:00
1 parent e6a7728087
commit 84c2b30358
5 files changed
+153 -27

No files matched your search

+17 -1
View File
@@ -25,6 +25,7 @@ const listUserDirectoryMock = vi.hoisted(() => vi.fn());
const getSessionMock = vi.hoisted(() => vi.fn());
const mockNavigate = vi.hoisted(() => vi.fn());
const mockParams = vi.hoisted(() => ({ connectionId: "conn-1", tab: "setup" as string | undefined }));
const mockSearchParams = vi.hoisted(() => ({ value: new URLSearchParams() }));
const navigateComponentMock = vi.hoisted(() => vi.fn());
const navigateTopLevelMock = vi.hoisted(() => vi.fn());
@@ -80,7 +81,7 @@ vi.mock("@/lib/browserNavigation", () => ({
vi.mock("@/lib/router", () => ({
useParams: () => mockParams,
useNavigate: () => mockNavigate,
useSearchParams: () => [new URLSearchParams(), vi.fn()],
useSearchParams: () => [mockSearchParams.value, vi.fn()],
Navigate: ({ to, replace }: { to: string; replace?: boolean }) => {
navigateComponentMock({ to, replace });
return <div data-navigate-to={to} />;
@@ -189,6 +190,7 @@ describe("AppDetail", () => {
document.body.appendChild(container);
mockParams.connectionId = "conn-1";
mockParams.tab = "setup";
mockSearchParams.value = new URLSearchParams();
getConnectionMock.mockResolvedValue(connection());
getConnectionInstallsMock.mockResolvedValue({ connectionId: "conn-1", installs: [] });
listGalleryMock.mockResolvedValue({
@@ -398,6 +400,20 @@ describe("AppDetail", () => {
expect(container.querySelector(".animate-spin")).toBeTruthy();
});
it("confirms a successful connection on Test and clears the one-time URL flag", async () => {
mockParams.tab = "test";
mockSearchParams.value = new URLSearchParams("success=1");
await renderAppDetail();
expect(pushToastMock).toHaveBeenCalledWith({
title: "GitHub connected",
body: "The connection is ready. You can test an action below.",
tone: "success",
});
expect(mockNavigate).toHaveBeenCalledWith("/apps/conn-1/test", { replace: true });
});
it("hides secret URL parameters in setup technical details", async () => {
mockParams.tab = "setup";
getConnectionMock.mockResolvedValue(
+20 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, Loader2, Pencil } from "lucide-react";
import type {
@@ -11,7 +11,7 @@ import {
humanizeConnectionDisplayName,
isToolConnectionAttentionHealth as isAttentionHealthStatus,
} from "@paperclipai/shared";
import { Navigate, useParams, useNavigate } from "@/lib/router";
import { Navigate, useParams, useNavigate, useSearchParams } from "@/lib/router";
import { useCompany } from "@/context/CompanyContext";
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
import { useToast } from "@/context/ToastContext";
@@ -62,6 +62,7 @@ export { DangerZone, connectionAddress, connectionTransportLabel };
export function AppDetail() {
const { connectionId = "", tab } = useParams<{ connectionId: string; tab?: string }>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const queryClient = useQueryClient();
const { pushToast } = useToast();
const { selectedCompany, selectedCompanyId } = useCompany();
@@ -138,6 +139,23 @@ export function AppDetail() {
const appName = connection
? connectionDisplayNameForOwner(connection, baseAppName, owner)
: "App";
const successNoticeShownFor = useRef<string | null>(null);
useEffect(() => {
if (
activeTab !== "test"
|| searchParams.get("success") !== "1"
|| !connection
|| successNoticeShownFor.current === connection.id
) return;
successNoticeShownFor.current = connection.id;
pushToast({
title: `${appName} connected`,
body: "The connection is ready. You can test an action below.",
tone: "success",
});
navigate(appTabHref(connection.id, "test"), { replace: true });
}, [activeTab, appName, connection, navigate, pushToast, searchParams]);
useEffect(() => {
if (!activeTab) return;