mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
fix(connections): tolerate transitional app definitions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c65c078205
commit
fa2dd5a307
8 files changed
+112
-42
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 →"}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user