fix(connections): tolerate transitional app definitions

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-21 20:35:00 +00:00
1 parent c65c078205
commit fa2dd5a307
8 files changed
+112 -42

No files matched your search

+19 -7
View File
@@ -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) {
<span className="truncate">All apps</span>
</Link>
<div className="flex min-w-0 items-center gap-2 px-2 py-1">
<AppLogo name={appName} logoUrl={logoEntry?.branding.logoUrl} size={28} />
<AppLogo name={appName} logoUrl={appDefinitionLogoUrl(logoEntry)} size={28} />
<span className="flex-1 truncate text-sm font-bold text-foreground">{appName}</span>
</div>
</div>
@@ -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 {
+16 -6
View File
@@ -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 (
<header className="flex flex-wrap items-start justify-between gap-4">
<div className="flex items-center gap-3">
<AppLogo name={appName} logoUrl={logoEntry?.branding.logoUrl} size={44} />
<AppLogo name={appName} logoUrl={appDefinitionLogoUrl(logoEntry)} size={44} />
<div>
{renaming ? (
<form
@@ -607,10 +612,15 @@ function accessFrom(profile: ToolProfileWithDetails | undefined): AccessDraft {
return { mode: "specific", agentIds };
}
function galleryEntryFor(apps: AppDefinition[], connection: ToolConnection | undefined): AppDefinition | null {
function galleryEntryFor(
apps: AppGalleryDisplayEntry[],
connection: ToolConnection | undefined,
): AppGalleryDisplayEntry | null {
if (!connection) return null;
const name = connection.name.toLowerCase();
return apps.find((a) => 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<string>, ids: string[]): Set<string> {
+13 -3
View File
@@ -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 <Navigate to={appTabHref(activeConnection.id, activeTab)} replace />;
}
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({
+22 -17
View File
@@ -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() {
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{popular.map((entry) => (
<AppTile
key={entry.slug}
key={appDefinitionSlug(entry)}
entry={entry}
onConnect={entry.slug === "zapier" ? () => navigate(ZAPIER_CONNECT_HREF) : undefined}
onConnect={appDefinitionSlug(entry) === "zapier" ? () => navigate(ZAPIER_CONNECT_HREF) : undefined}
compact
/>
))}
@@ -132,9 +137,9 @@ export function Browse() {
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((entry) => (
<AppTile
key={entry.slug}
key={appDefinitionSlug(entry)}
entry={entry}
onConnect={entry.slug === "zapier" ? () => navigate(ZAPIER_CONNECT_HREF) : undefined}
onConnect={appDefinitionSlug(entry) === "zapier" ? () => navigate(ZAPIER_CONNECT_HREF) : undefined}
/>
))}
</div>
@@ -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"}
>
<AppLogo name={entry.name} logoUrl={entry.branding.logoUrl} size={36} />
<span className="text-xs font-medium text-foreground">{entry.name}</span>
<AppLogo name={appDefinitionName(entry)} logoUrl={appDefinitionLogoUrl(entry)} size={36} />
<span className="text-xs font-medium text-foreground">{appDefinitionName(entry)}</span>
<span className={disabled ? "text-xs text-muted-foreground" : "text-xs font-semibold text-primary"}>
{disabled ? "Coming soon" : "Connect →"}
</span>
@@ -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"}
>
<AppLogo name={entry.name} logoUrl={entry.branding.logoUrl} size={36} />
<AppLogo name={appDefinitionName(entry)} logoUrl={appDefinitionLogoUrl(entry)} size={36} />
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold text-foreground">{entry.name}</div>
<div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">{entry.description}</div>
<div className="text-sm font-semibold text-foreground">{appDefinitionName(entry)}</div>
<div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">{appDefinitionDescription(entry)}</div>
</div>
<span className={disabled ? "shrink-0 text-xs font-semibold text-muted-foreground" : "shrink-0 text-xs font-semibold text-primary"}>
{disabled ? "Coming soon" : "Connect →"}
+13 -7
View File
@@ -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<string, AppDefinition>();
for (const entry of gallery) map.set(entry.name.toLowerCase(), entry);
const map = new Map<string, AppGalleryDisplayEntry>();
for (const entry of gallery) map.set(appDefinitionName(entry).toLowerCase(), entry);
return map;
}, [gallery]);
const logoByKey = useMemo(() => {
const map = new Map<string, AppDefinition>();
for (const entry of gallery) map.set(entry.slug, entry);
const map = new Map<string, AppGalleryDisplayEntry>();
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]);
@@ -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;
}
@@ -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),
+2 -1
View File
@@ -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 && (
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">{description}</p>
)}
{galleryEntry?.slug === "google-sheets" && (
{appDefinitionSlug(galleryEntry) === "google-sheets" && (
<GoogleSheetsAllowlistSection
connection={connection}
disabled={configUpdateDisabled}