From fa2dd5a3072211f1b1fe1d8bc8d94e73bfbe71c4 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 21 Jul 2026 20:35:00 +0000 Subject: [PATCH] fix(connections): tolerate transitional app definitions Co-Authored-By: Paperclip --- ui/src/components/AppConnectionSidebar.tsx | 26 +++++++++---- ui/src/pages/apps/AppDetail.tsx | 22 ++++++++--- ui/src/pages/apps/AppNotConnected.tsx | 16 ++++++-- ui/src/pages/apps/Browse.tsx | 39 +++++++++++-------- ui/src/pages/apps/Connections.tsx | 20 ++++++---- ui/src/pages/apps/app-definition-display.ts | 24 ++++++++++++ .../pages/apps/app-detail/AdvancedPanel.tsx | 4 +- ui/src/pages/apps/app-detail/SetupPanel.tsx | 3 +- 8 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 ui/src/pages/apps/app-definition-display.ts diff --git a/ui/src/components/AppConnectionSidebar.tsx b/ui/src/components/AppConnectionSidebar.tsx index 7b6ec27d465..adc50eef97f 100644 --- a/ui/src/components/AppConnectionSidebar.tsx +++ b/ui/src/components/AppConnectionSidebar.tsx @@ -1,7 +1,7 @@ import { ChevronLeft } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { humanizeConnectionDisplayName } from "@paperclipai/shared"; -import type { AppDefinition, ToolApplication, ToolConnection } from "@paperclipai/shared"; +import type { ToolApplication, ToolConnection } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { toolsApi } from "@/api/tools"; import { useCompany } from "@/context/CompanyContext"; @@ -15,6 +15,12 @@ import { type AppTabKey, } from "@/pages/apps/app-tabs"; import { AppLogo } from "@/pages/apps/AppLogo"; +import { + appDefinitionLogoUrl, + appDefinitionName, + appDefinitionSlug, + type AppGalleryDisplayEntry, +} from "@/pages/apps/app-definition-display"; import { SidebarNavItem } from "./SidebarNavItem"; type AppDetailSidebarProps = @@ -61,7 +67,11 @@ export function AppDetailSidebar(props: AppDetailSidebarProps) { : []; const previousConnection = latestArchivedConnection(appConnections); const appName = connection ? humanizeConnectionDisplayName(connection) : application?.name ?? "App"; - const logoEntry = galleryEntryFor(galleryQuery.data?.apps ?? [], connection, application ?? undefined); + const logoEntry = galleryEntryFor( + (galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[], + connection, + application ?? undefined, + ); const reviewConnectionId = connection?.id ?? previousConnection?.id ?? null; const attentionItem = reviewConnectionId ? attentionQuery.data?.apps.find((app) => app.connection.id === reviewConnectionId) @@ -83,7 +93,7 @@ export function AppDetailSidebar(props: AppDetailSidebarProps) { All apps
- + {appName}
@@ -117,17 +127,19 @@ function tabHref(props: AppDetailSidebarProps, tab: AppTabKey): string { } function galleryEntryFor( - apps: AppDefinition[], + apps: AppGalleryDisplayEntry[], connection: ToolConnection | undefined, application: ToolApplication | undefined, -): AppDefinition | null { +): AppGalleryDisplayEntry | null { if (application?.applicationKey) { - const keyed = apps.find((app) => app.slug === application.applicationKey); + const keyed = apps.find((app) => appDefinitionSlug(app) === application.applicationKey); if (keyed) return keyed; } const name = (connection?.name ?? application?.name)?.toLowerCase(); if (!name) return null; - return apps.find((app) => app.name.toLowerCase() === name) ?? apps.find((app) => app.slug === name) ?? null; + return apps.find((app) => appDefinitionName(app).toLowerCase() === name) ?? + apps.find((app) => appDefinitionSlug(app) === name) ?? + null; } function latestArchivedConnection(connections: ToolConnection[]): ToolConnection | null { diff --git a/ui/src/pages/apps/AppDetail.tsx b/ui/src/pages/apps/AppDetail.tsx index 758e96c6447..010311c37af 100644 --- a/ui/src/pages/apps/AppDetail.tsx +++ b/ui/src/pages/apps/AppDetail.tsx @@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Loader2, Pencil } from "lucide-react"; import type { - AppDefinition, ToolConnection, ToolPolicy, ToolProfileWithDetails, @@ -28,6 +27,12 @@ import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { AppLogo } from "./AppLogo"; +import { + appDefinitionLogoUrl, + appDefinitionName, + appDefinitionSlug, + type AppGalleryDisplayEntry, +} from "./app-definition-display"; import { appTabHref, appTabLabel, isAppTabKey, type AppTabKey } from "./app-tabs"; import { SetupPanel } from "./app-detail/SetupPanel"; import { PermissionsPanel } from "./app-detail/PermissionsPanel"; @@ -147,7 +152,7 @@ export function AppDetail() { return labels; }, [userDirectoryQuery.data, sessionQuery.data]); const logoEntry = useMemo( - () => galleryEntryFor(galleryQuery.data?.apps ?? [], connection), + () => galleryEntryFor((galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[], connection), [galleryQuery.data, connection], ); @@ -479,7 +484,7 @@ function AppDetailHeader({ }: { appName: string; connection: ToolConnection; - logoEntry: AppDefinition | null; + logoEntry: AppGalleryDisplayEntry | null; status: StatusInfo; actionCount: number; renaming: boolean; @@ -493,7 +498,7 @@ function AppDetailHeader({ return (
- +
{renaming ? (
a.name.toLowerCase() === name) ?? apps.find((a) => a.slug === name) ?? null; + return apps.find((app) => appDefinitionName(app).toLowerCase() === name) ?? + apps.find((app) => appDefinitionSlug(app) === name) ?? + null; } function addAll(set: Set, ids: string[]): Set { diff --git a/ui/src/pages/apps/AppNotConnected.tsx b/ui/src/pages/apps/AppNotConnected.tsx index f4cce08a343..94fc8ee89dd 100644 --- a/ui/src/pages/apps/AppNotConnected.tsx +++ b/ui/src/pages/apps/AppNotConnected.tsx @@ -12,6 +12,12 @@ import { agentsApi } from "@/api/agents"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { AppLogo } from "./AppLogo"; +import { + appDefinitionLogoUrl, + appDefinitionName, + appDefinitionSlug, + type AppGalleryDisplayEntry, +} from "./app-definition-display"; import { connectionAddress, connectionTransportLabel, DangerZone } from "./AppDetail"; import { ActivityPanel } from "./app-detail/ActivityPanel"; import { ReviewPanel } from "./app-detail/ReviewPanel"; @@ -121,10 +127,14 @@ export function AppNotConnected() { return ; } - const gallery = galleryQuery.data?.apps ?? []; + const gallery = (galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[]; const logoUrl = - (application.applicationKey ? gallery.find((entry) => entry.slug === application.applicationKey)?.branding.logoUrl : undefined) ?? - gallery.find((entry) => entry.name.toLowerCase() === application.name.toLowerCase())?.branding.logoUrl; + (application.applicationKey + ? appDefinitionLogoUrl(gallery.find((entry) => appDefinitionSlug(entry) === application.applicationKey)) + : undefined) ?? + appDefinitionLogoUrl( + gallery.find((entry) => appDefinitionName(entry).toLowerCase() === application.name.toLowerCase()), + ); const previousAddress = previousConnection ? connectionAddress(previousConnection) : null; const connectHref = reconnectHref({ diff --git a/ui/src/pages/apps/Browse.tsx b/ui/src/pages/apps/Browse.tsx index da2925bbcfb..9a764670b5c 100644 --- a/ui/src/pages/apps/Browse.tsx +++ b/ui/src/pages/apps/Browse.tsx @@ -1,7 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Link2, Search } from "lucide-react"; -import type { AppDefinition } from "@paperclipai/shared"; import { useNavigate } from "@/lib/router"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; @@ -9,6 +8,13 @@ import { queryKeys } from "@/lib/queryKeys"; import { toolsApi } from "@/api/tools"; import { Skeleton } from "@/components/ui/skeleton"; import { AppLogo } from "./AppLogo"; +import { + appDefinitionDescription, + appDefinitionLogoUrl, + appDefinitionName, + appDefinitionSlug, + type AppGalleryDisplayEntry, +} from "./app-definition-display"; import { AdvancedToolsLink, BYO_CONNECT_HREF, @@ -46,11 +52,11 @@ export function Browse() { enabled: !!selectedCompanyId, }); - const gallery = galleryQuery.data?.apps ?? []; + const gallery = (galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[]; const popular = useMemo( () => - POPULAR_KEYS.map((key) => gallery.find((entry) => entry.slug === key)).filter( - (entry): entry is AppDefinition => Boolean(entry), + POPULAR_KEYS.map((key) => gallery.find((entry) => appDefinitionSlug(entry) === key)).filter( + (entry): entry is AppGalleryDisplayEntry => Boolean(entry), ), [gallery], ); @@ -60,9 +66,8 @@ export function Browse() { if (!trimmed) return gallery; return gallery.filter( (entry) => - entry.name.toLowerCase().includes(trimmed) || - entry.description.toLowerCase().includes(trimmed) || - (entry.description?.toLowerCase().includes(trimmed) ?? false), + appDefinitionName(entry).toLowerCase().includes(trimmed) || + appDefinitionDescription(entry).toLowerCase().includes(trimmed), ); }, [gallery, trimmed]); @@ -109,9 +114,9 @@ export function Browse() {
{popular.map((entry) => ( navigate(ZAPIER_CONNECT_HREF) : undefined} + onConnect={appDefinitionSlug(entry) === "zapier" ? () => navigate(ZAPIER_CONNECT_HREF) : undefined} compact /> ))} @@ -132,9 +137,9 @@ export function Browse() {
{filtered.map((entry) => ( navigate(ZAPIER_CONNECT_HREF) : undefined} + onConnect={appDefinitionSlug(entry) === "zapier" ? () => navigate(ZAPIER_CONNECT_HREF) : undefined} /> ))}
@@ -160,7 +165,7 @@ function AppTile({ onConnect, compact = false, }: { - entry: AppDefinition; + entry: AppGalleryDisplayEntry; onConnect?: () => void; compact?: boolean; }) { @@ -175,8 +180,8 @@ function AppTile({ ? "flex cursor-not-allowed flex-col items-center gap-2 rounded-xl border border-border bg-background px-3 py-4 text-center opacity-60" : "flex flex-col items-center gap-2 rounded-xl border border-border bg-background px-3 py-4 text-center transition-colors hover:border-foreground/30 hover:bg-accent/40"} > - - {entry.name} + + {appDefinitionName(entry)} {disabled ? "Coming soon" : "Connect →"} @@ -192,10 +197,10 @@ function AppTile({ ? "flex h-full cursor-not-allowed items-start gap-3 rounded-xl border border-border bg-card px-4 py-4 text-left opacity-60" : "flex h-full items-start gap-3 rounded-xl border border-border bg-card px-4 py-4 text-left transition-colors hover:border-foreground/30 hover:bg-accent/40"} > - +
-
{entry.name}
-
{entry.description}
+
{appDefinitionName(entry)}
+
{appDefinitionDescription(entry)}
{disabled ? "Coming soon" : "Connect →"} diff --git a/ui/src/pages/apps/Connections.tsx b/ui/src/pages/apps/Connections.tsx index 887ee4bac16..6a330e107a6 100644 --- a/ui/src/pages/apps/Connections.tsx +++ b/ui/src/pages/apps/Connections.tsx @@ -2,7 +2,6 @@ import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import { AppWindow, ShieldAlert, ShieldQuestion } from "lucide-react"; import type { - AppDefinition, ToolApplication, ToolConnection, ToolProfileWithDetails, @@ -21,6 +20,12 @@ import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { timeAgo } from "@/lib/timeAgo"; import { AppLogo } from "./AppLogo"; +import { + appDefinitionLogoUrl, + appDefinitionName, + appDefinitionSlug, + type AppGalleryDisplayEntry, +} from "./app-definition-display"; import { useReviewCount } from "./useReviewCount"; import { AdvancedToolsLink } from "./store-cards"; @@ -113,15 +118,15 @@ export function Connections() { enabled: !!selectedCompanyId, }); - const gallery = galleryQuery.data?.apps ?? []; + const gallery = (galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[]; const logoByName = useMemo(() => { - const map = new Map(); - for (const entry of gallery) map.set(entry.name.toLowerCase(), entry); + const map = new Map(); + for (const entry of gallery) map.set(appDefinitionName(entry).toLowerCase(), entry); return map; }, [gallery]); const logoByKey = useMemo(() => { - const map = new Map(); - for (const entry of gallery) map.set(entry.slug, entry); + const map = new Map(); + for (const entry of gallery) map.set(appDefinitionSlug(entry), entry); return map; }, [gallery]); @@ -173,7 +178,8 @@ export function Connections() { status: statusFor(application, appConnections), actionCount, lastUsedAt, - logoUrl: galleryEntry?.branding.logoUrl ?? logoByName.get(application.name.toLowerCase())?.branding.logoUrl, + logoUrl: appDefinitionLogoUrl(galleryEntry) ?? + appDefinitionLogoUrl(logoByName.get(application.name.toLowerCase())), }; }); }, [actionCountByConnection, applications, connectionsByApplication, logoByKey, logoByName]); diff --git a/ui/src/pages/apps/app-definition-display.ts b/ui/src/pages/apps/app-definition-display.ts new file mode 100644 index 00000000000..0e6ab9e0e03 --- /dev/null +++ b/ui/src/pages/apps/app-definition-display.ts @@ -0,0 +1,24 @@ +import type { AppDefinition } from "@paperclipai/shared"; + +export type AppGalleryDisplayEntry = AppDefinition & { + key?: string; + logoUrl?: string; + tagline?: string; + branding?: AppDefinition["branding"]; +}; + +export function appDefinitionSlug(entry: AppGalleryDisplayEntry | null | undefined): string { + return entry?.slug ?? entry?.key ?? ""; +} + +export function appDefinitionName(entry: AppGalleryDisplayEntry | null | undefined): string { + return entry?.name ?? appDefinitionSlug(entry) ?? "App"; +} + +export function appDefinitionDescription(entry: AppGalleryDisplayEntry | null | undefined): string { + return entry?.description ?? entry?.tagline ?? ""; +} + +export function appDefinitionLogoUrl(entry: AppGalleryDisplayEntry | null | undefined): string | undefined { + return entry?.branding?.logoUrl ?? entry?.logoUrl; +} diff --git a/ui/src/pages/apps/app-detail/AdvancedPanel.tsx b/ui/src/pages/apps/app-detail/AdvancedPanel.tsx index 807bfe2d88e..bc0c2e3c3c2 100644 --- a/ui/src/pages/apps/app-detail/AdvancedPanel.tsx +++ b/ui/src/pages/apps/app-detail/AdvancedPanel.tsx @@ -110,7 +110,9 @@ function ReconnectForm({ onReconnected: () => void; }) { const { pushToast } = useToast(); - const method = galleryEntry ? getAvailableConnectionMethod(galleryEntry) : null; + const method = galleryEntry && Array.isArray(galleryEntry.methods) + ? getAvailableConnectionMethod(galleryEntry) + : null; const fields = (method?.credentialFields ?? []).map((field) => ({ ...field, configPath: credentialConfigPath(field), diff --git a/ui/src/pages/apps/app-detail/SetupPanel.tsx b/ui/src/pages/apps/app-detail/SetupPanel.tsx index 8c1573486c5..6d031242964 100644 --- a/ui/src/pages/apps/app-detail/SetupPanel.tsx +++ b/ui/src/pages/apps/app-detail/SetupPanel.tsx @@ -3,6 +3,7 @@ import type { ToolCatalogEntry, ToolConnection } from "@paperclipai/shared"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; +import { appDefinitionSlug } from "../app-definition-display"; import type { AppDetailSectionProps } from "./types"; import { googleSheetsConfigWithAllowlist, parseGoogleSheetIds } from "../google-sheets"; @@ -35,7 +36,7 @@ export function SetupPanel({ {description && (

{description}

)} - {galleryEntry?.slug === "google-sheets" && ( + {appDefinitionSlug(galleryEntry) === "google-sheets" && (