diff --git a/apps/web/app/connect/AddConnection.client.tsx b/apps/web/app/connect/AddConnection.client.tsx
index b2c024e92..35c77692a 100644
--- a/apps/web/app/connect/AddConnection.client.tsx
+++ b/apps/web/app/connect/AddConnection.client.tsx
@@ -1,11 +1,22 @@
'use client'
+import type {ReactElement} from 'react'
import type {ConnectorConfig} from '@openint/api-v1/trpc/routers/connectorConfig.models'
import type {Id} from '@openint/cdk'
+import {SearchIcon} from 'lucide-react'
+import {useState} from 'react'
import {type ConnectorName} from '@openint/api-v1/trpc/routers/connector.models'
-import {Label} from '@openint/shadcn/ui'
-import {ConnectorConfigCard} from '@openint/ui-v1/domain-components/ConnectorConfigCard'
+import {cn} from '@openint/shadcn/lib/utils'
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ Input,
+} from '@openint/shadcn/ui'
+import {ConnectorDisplay} from '@openint/ui-v1/domain-components/ConnectorDisplay'
+import {useMutableSearchParams} from '@openint/ui-v1/hooks/useStateFromSearchParam'
import {ConnectorConnectContainer} from './ConnectorConnect.client'
export type ConnectorConfigForCustomer = Pick<
@@ -13,6 +24,116 @@ export type ConnectorConfigForCustomer = Pick<
'id' | 'connector_name' | 'connector'
>
+export type AddConnectionPrefetchedCard = {
+ connectorName: ConnectorName
+ connectorDisplayName?: string
+ card: ReactElement
+}
+
+export function AddConnectionClient({
+ cards,
+ className,
+}: {
+ cards: AddConnectionPrefetchedCard[]
+ className?: string
+}) {
+ const [isOpen, setIsOpen] = useState(true)
+ const [_, setSearchParams] = useMutableSearchParams()
+
+ const onClose = () => {
+ setIsOpen(false)
+ setSearchParams({view: 'manage'}, {shallow: false})
+ }
+ const [searchQuery, setSearchQuery] = useState('')
+ const [isFocused, setIsFocused] = useState(false)
+
+ const filteredCards = cards.filter((card) => {
+ const searchLower = searchQuery.toLowerCase()
+ return (
+ card.connectorName.toLowerCase().includes(searchLower) ||
+ card.connectorDisplayName?.toLowerCase().includes(searchLower)
+ )
+ })
+
+ return (
+ !open && onClose()}>
+ {/* TODO: consider making full screen horizontal on mobile https://github.com/radix-ui/themes/issues/142 */}
+
+
+ Add Integrations
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="border-gray-200 bg-gray-50/50 pl-10 transition-all duration-200 focus:bg-white focus:shadow-sm"
+ onFocus={() => setIsFocused(true)}
+ onBlur={() => setIsFocused(false)}
+ />
+
+
+
+
+
{
+ const scrollTop = e.currentTarget.scrollTop
+ const shouldBeFocused = scrollTop > 5
+ if (shouldBeFocused !== isFocused) {
+ setIsFocused(shouldBeFocused)
+ }
+ }}>
+ {/* Mobile view */}
+
+ {filteredCards.map((card, index) => (
+
+ {card.card}
+ {index < filteredCards.length - 1 && (
+
+ )}
+
+ ))}
+
+
+ {/* Desktop view: Grid layout */}
+
+ {filteredCards.map((card, index) => (
+
+ {card.card}
+
+ ))}
+
+
+ {filteredCards.length === 0 && (
+
+ No integrations found matching{' '}
+ {searchQuery ? `"${searchQuery}"` : 'your search'}
+
+ )}
+
+
+
+
+ )
+}
+
export function AddConnectionCard({
connectorConfig,
}: {
@@ -25,20 +146,49 @@ export function AddConnectionCard({
connector={connectorConfig.connector!}
connectorConfigId={connectorConfig.id as Id['ccfg']}>
{({isConnecting, handleConnect}) => (
- }
- onPress={() => {
- if (isConnecting) {
- return
- }
- handleConnect()
- }}>
-
- {isConnecting ? 'Connecting...' : 'Connect'}
-
-
+ <>
+ {/* NOTE/TODO: note sure how to remove duplication,
+ its there as we're using tailwind for consistent breakpoints with AddConnectionClient */}
+ {/* Mobile view (visible below md breakpoint) */}
+
+ {
+ if (isConnecting) {
+ return
+ }
+ handleConnect()
+ }}
+ />
+
+
+ {/* Desktop view (visible at md breakpoint and above) */}
+
+ {
+ if (isConnecting) {
+ return
+ }
+ handleConnect()
+ }}
+ />
+
+ >
)}
)
diff --git a/apps/web/app/connect/MyConnections.client.tsx b/apps/web/app/connect/MyConnections.client.tsx
index f2a33cae1..548c22588 100644
--- a/apps/web/app/connect/MyConnections.client.tsx
+++ b/apps/web/app/connect/MyConnections.client.tsx
@@ -1,6 +1,7 @@
'use client'
import {useSuspenseQuery} from '@tanstack/react-query'
+import {Link} from 'lucide-react'
import React from 'react'
import {type ConnectorName} from '@openint/api-v1/trpc/routers/connector.models'
import {Id} from '@openint/cdk'
@@ -9,7 +10,7 @@ import {
CommandPopover,
ConnectionStatusPill,
DataTileView,
- Spinner,
+ LoadingSpinner,
useMutableSearchParams,
} from '@openint/ui-v1'
import {ConnectionCard} from '@openint/ui-v1/domain-components/ConnectionCard'
@@ -41,75 +42,106 @@ export function MyConnectionsClient(props: {
const definitions = useCommandDefinitionMap()
if (isLoading) {
- return (
-
-
-
- )
+ return
}
if (!res.data?.items?.length || res.data.items.length === 0) {
return (
-
-
- You have no configured integrations.
-
+
+
+
+
+
+
+ Let's Get You Connected
+
+
+ You have not configured any integrations yet. Connect your first app
+ to get started.
+
+
setSearchParams({view: 'add'}, {shallow: true})}>
- Add your first integration
+ size="lg"
+ className="font-medium"
+ onClick={() => {
+ setIsLoading(true)
+ setSearchParams({view: 'add'}, {shallow: false})
+ }}
+ disabled={isLoading}>
+ Add First Integration
)
}
return (
-
conn.id}
- renderItem={(conn) => {
- const renderCard = ({handleConnect}: {handleConnect?: () => void}) => (
-
-
-
-
-
- ({timeSince(conn.updated_at)})
-
-
- >
- }
- />
-
- )
+ <>
+
+
My Integrations
+ {
+ setIsLoading(true)
+ setSearchParams({view: 'add'}, {shallow: false})
+ }}
+ disabled={isLoading}>
+ Add Integration
+
+
+
+
+ conn.id}
+ renderItem={(conn) => {
+ const renderCard = ({
+ handleConnect,
+ }: {
+ handleConnect?: () => void
+ }) => (
+
+
+
+
+
+ ({timeSince(conn.updated_at)})
+
+
+ >
+ }
+ />
+
+ )
- if (conn.status !== 'disconnected') {
- return renderCard({handleConnect: undefined})
- }
+ if (conn.status !== 'disconnected') {
+ return renderCard({handleConnect: undefined})
+ }
- return (
-
- {renderCard}
-
- )
- }}
- />
+ return (
+
+ {renderCard}
+
+ )
+ }}
+ />
+ >
)
}
diff --git a/apps/web/app/connect/page.client.tsx b/apps/web/app/connect/page.client.tsx
deleted file mode 100644
index c53a2c8d0..000000000
--- a/apps/web/app/connect/page.client.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-'use client'
-
-import React from 'react'
-import {Tabs} from '@openint/shadcn/ui'
-import {useStateFromSearchParams} from '@openint/ui-v1'
-
-/**
- * Tabs that automatically sync with search params
- * However this does not work very well so far because it causes the entire server side route to reload
- * which is not actually intentional
- *
- */
-export function TabsClient({
- paramKey,
- ...props
-}: Omit, 'value' | 'onValueChange'> & {
- paramKey: string
-}) {
- const [value, setValue] = useStateFromSearchParams(paramKey, {
- defaultValue: props.defaultValue,
- shallow: true,
- })
-
- return
-}
diff --git a/apps/web/app/connect/page.tsx b/apps/web/app/connect/page.tsx
index d0642baec..a601f69d3 100644
--- a/apps/web/app/connect/page.tsx
+++ b/apps/web/app/connect/page.tsx
@@ -21,7 +21,6 @@ import {
CardHeader,
CardTitle,
} from '@openint/shadcn/ui/card'
-import {TabsContent, TabsList, TabsTrigger} from '@openint/shadcn/ui/tabs'
import {ErrorBoundarySuspense, LoadingSpinner} from '@openint/ui-v1'
import {GlobalCommandBarProvider} from '@/lib-client/GlobalCommandBarProvider'
import {TRPCApp} from '@/lib-client/TRPCApp'
@@ -32,11 +31,10 @@ import {
getServerComponentContext,
serverComponentContextForViewer,
} from '@/lib-server/trpc.server'
-import {AddConnectionCard} from './AddConnection.client'
+import {AddConnectionCard, AddConnectionClient} from './AddConnection.client'
import {ConnectContextProvider} from './ConnectContextProvider'
import {ConnectOpWrapper} from './ConnectOpWrapper'
import {MyConnectionsClient} from './MyConnections.client'
-import {TabsClient} from './page.client'
export default async function ConnectPage(
pageProps: PageProps<
@@ -100,6 +98,8 @@ export default async function ConnectPage(
.map(([key, value]) => [key, value]),
)
+ const isManage = searchParams.view === 'manage' || !searchParams.view
+
const viewerConnections = await queryClient.fetchQuery(
trpc.listConnections.queryOptions({
connector_names: searchParams.connector_names,
@@ -142,7 +142,7 @@ export default async function ConnectPage(
{/* Left Banner - Hidden on mobile and tablets, shown only on lg+ screens */}
{!searchParams.is_embedded && (
-
+
0 ? 'manage' : 'add'
- }
- paramKey="view"
- className="flex-1 p-4 lg:pt-12">
-
-
-
- Manage Integrations
-
- Add New Integration
-
-
- }>
-
-
-
-
- }>
-
-
-
+ {/* QQ: should we always load myConnections and just load AddConnections on top given that it's a modal */}
+
}>
+
+ {isManage ? (
+
+ ) : (
+
+ )}
-
+
@@ -301,23 +286,27 @@ async function AddConnections({
// "You have configured all enabled integrations. If you'd like to enable new ones please contact the {organizationName} support team"
return (
-
-
}>
- {availableToConnect.map((ccfg) => (
-
- ))}
-
-
+ }>
+ ({
+ connectorName: ccfg.connector_name as ConnectorName,
+ connectorDisplayName: ccfg.connector?.display_name,
+ card: (
+
+ ),
+ }))}
+ />
+
)
}
diff --git a/apps/web/app/console/(authenticated)/connect/page.client.tsx b/apps/web/app/console/(authenticated)/connect/page.client.tsx
index e695d32d1..de96d51cb 100644
--- a/apps/web/app/console/(authenticated)/connect/page.client.tsx
+++ b/apps/web/app/console/(authenticated)/connect/page.client.tsx
@@ -10,7 +10,7 @@ import React from 'react'
import {connectRouterModels} from '@openint/api-v1/trpc/routers/connect.models'
import {ConnectButton, ConnectEmbed} from '@openint/connect'
import {getBaseURLs} from '@openint/env'
-import {Spinner} from '@openint/ui-v1'
+import {LoadingSpinner} from '@openint/ui-v1'
import {PreviewWindow} from '@openint/ui-v1/components/PreviewWindow'
import {ZodSchemaForm} from '@openint/ui-v1/components/schema-form'
import {createURL} from '@openint/util/url-utils'
@@ -114,7 +114,7 @@ export function ConnectEmbedPreview(props: {
}>
{tokenRes.status === 'pending' ? (
-
+
) : tokenRes.status === 'error' ? (
// TODO: Show a better error message
diff --git a/packages/ui-v1/components/Spinner.tsx b/packages/ui-v1/components/Spinner.tsx
deleted file mode 100644
index cc6c281f1..000000000
--- a/packages/ui-v1/components/Spinner.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import {Loader2} from 'lucide-react'
-
-export function Spinner() {
- return (
-
-
-
- )
-}
diff --git a/packages/ui-v1/components/index.ts b/packages/ui-v1/components/index.ts
index 62cc1cdc9..b4a71a580 100644
--- a/packages/ui-v1/components/index.ts
+++ b/packages/ui-v1/components/index.ts
@@ -16,7 +16,6 @@ export * from './PropertyListView'
export * from './schema-form'
export * from './SearchInput'
export * from './SecureInput'
-export * from './Spinner'
export * from './StatusDot'
export * from './tabs'
export * from './ThemeProvider'
diff --git a/packages/ui-v1/domain-components/LinkConnectorModal.tsx b/packages/ui-v1/domain-components/LinkConnectorModal.tsx
deleted file mode 100644
index 422e2a8bb..000000000
--- a/packages/ui-v1/domain-components/LinkConnectorModal.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-'use client'
-
-import type {Core} from '@openint/api-v1/models'
-
-import {Search} from 'lucide-react'
-import {useState} from 'react'
-import {cn} from '@openint/shadcn/lib/utils'
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- Input,
-} from '@openint/shadcn/ui'
-import {ConnectorDisplay} from './ConnectorDisplay'
-
-export interface LinkConnectorModalProps {
- isOpen: boolean
- onClose: () => void
- connectors: Array
- onSelectConnector?: (connector: Core['connector']) => void
- title?: string
- className?: string
- initialSearchValue?: string
-}
-
-export function LinkConnectorModal({
- isOpen,
- onClose,
- connectors = [],
- onSelectConnector,
- title = 'Select Integration',
- className,
- initialSearchValue = '',
-}: LinkConnectorModalProps) {
- const [searchQuery, setSearchQuery] = useState(initialSearchValue)
- const [isFocused, setIsFocused] = useState(false)
-
- const filteredConnectors = connectors.filter((connector) => {
- const searchLower = searchQuery.toLowerCase()
- return (
- connector.name.toLowerCase().includes(searchLower) ||
- (connector.display_name &&
- connector.display_name.toLowerCase().includes(searchLower))
- )
- })
-
- return (
- !open && onClose()}>
- e.preventDefault()}>
-
- {title}
-
-
-
-
-
-
-
- setSearchQuery(e.target.value)}
- className="border-gray-200 bg-gray-50/50 pl-10 transition-all duration-200 focus:bg-white focus:shadow-sm"
- onFocus={() => setIsFocused(true)}
- onBlur={() => setIsFocused(false)}
- />
-
-
-
-
-
{
- // When scrolling, set focus to create the separation effect
- if (e.currentTarget.scrollTop > 5) {
- setIsFocused(true)
- } else {
- setIsFocused(false)
- }
- }}>
- {/* Mobile view: Row mode with scrolling */}
-
- {filteredConnectors.map((connector, index) => (
-
-
onSelectConnector?.(connector)}>
- onSelectConnector?.(connector)}
- className="hover:bg-transparent"
- />
-
- {index < filteredConnectors.length - 1 && (
-
- )}
-
- ))}
-
-
- {/* Desktop view: Card grid */}
-
- {filteredConnectors.map((connector, index) => (
-
onSelectConnector?.(connector)}
- className="transition-transform duration-100 hover:scale-[1.01]">
- onSelectConnector?.(connector)}
- />
-
- ))}
-
-
- {filteredConnectors.length === 0 && (
-
- No integrations found matching{' '}
- {searchQuery ? `"${searchQuery}"` : 'your search'}
-
- )}
-
-
-
-
- )
-}
diff --git a/packages/ui-v1/domain-components/__stories__/LinkConnectorModal.stories.tsx b/packages/ui-v1/domain-components/__stories__/LinkConnectorModal.stories.tsx
deleted file mode 100644
index 3612bfc56..000000000
--- a/packages/ui-v1/domain-components/__stories__/LinkConnectorModal.stories.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import type {Meta, StoryObj} from '@storybook/react'
-
-import {useState} from 'react'
-import {Button} from '@openint/shadcn/ui'
-import {LinkConnectorModal} from '../LinkConnectorModal'
-
-const meta: Meta = {
- title: 'Domain Components/LinkConnectorModal',
- component: LinkConnectorModal,
- parameters: {
- layout: 'centered',
- },
-}
-
-export default meta
-type Story = StoryObj
-
-const connectors = [
- {
- name: 'wise',
- display_name: 'Wise',
- logo_url:
- 'https://cdn.iconscout.com/icon/free/png-256/free-wise-3770198-3155604.png',
- stage: 'ga' as 'ga' | 'beta' | 'alpha',
- platforms: ['web'] as Array<'web' | 'mobile' | 'desktop'>,
- },
- {
- name: 'firebase',
- display_name: 'Firebase',
- logo_url:
- 'https://firebase.google.com/static/images/brand-guidelines/logo-logomark.png',
- stage: 'ga' as 'ga' | 'beta' | 'alpha',
- platforms: ['web', 'mobile'] as Array<'web' | 'mobile' | 'desktop'>,
- },
- {
- name: 'hubspot',
- display_name: 'Hubspot',
- logo_url:
- 'https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/168_Hubspot_logo_logos-512.png',
- stage: 'beta' as 'ga' | 'beta' | 'alpha',
- platforms: ['web', 'mobile'] as Array<'web' | 'mobile' | 'desktop'>,
- },
- {
- name: 'ramp',
- display_name: 'Ramp',
- logo_url: 'https://ramp.com/img/logo.png',
- stage: 'beta' as 'ga' | 'beta' | 'alpha',
- platforms: ['web'] as Array<'web' | 'mobile' | 'desktop'>,
- },
- {
- name: 'discord',
- display_name: 'Discord',
- logo_url:
- 'https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.png',
- stage: 'alpha' as 'ga' | 'beta' | 'alpha',
- platforms: ['web', 'desktop'] as Array<'web' | 'mobile' | 'desktop'>,
- },
- {
- name: 'gong',
- display_name: 'Gong',
- logo_url:
- 'https://www.gong.io/wp-content/themes/gong/assets/images/site-icon-gong.png',
- stage: 'alpha' as 'ga' | 'beta' | 'alpha',
- platforms: ['web'] as Array<'web' | 'mobile' | 'desktop'>,
- },
-]
-
-// Create a larger set of connectors for testing scroll behavior
-const manyConnectors = [
- ...connectors,
- // Duplicating existing connectors with slight name variations for testing
- ...connectors.map((connector) => ({
- ...connector,
- name: connector.name, // Keep the same name so ConnectorLogo can find it
- display_name: `${connector.display_name} Pro`,
- })),
- ...connectors.map((connector) => ({
- ...connector,
- name: connector.name, // Keep the same name so ConnectorLogo can find it
- display_name: `${connector.display_name} Team`,
- })),
- ...connectors.map((connector) => ({
- ...connector,
- name: connector.name, // Keep the same name so ConnectorLogo can find it
- display_name: `${connector.display_name} Enterprise`,
- })),
- ...connectors.map((connector) => ({
- ...connector,
- name: connector.name, // Keep the same name so ConnectorLogo can find it
- display_name: `${connector.display_name} Cloud`,
- })),
-]
-
-// Controlled Example
-const ControlledExample = () => {
- const [isOpen, setIsOpen] = useState(false)
-
- return (
-
- setIsOpen(true)}>Open Link Connector Modal
- setIsOpen(false)}
- connectors={connectors}
- onSelectConnector={(connector) => {
- console.log('Selected connector:', connector)
- setIsOpen(false)
- }}
- />
-
- )
-}
-
-export const Default: Story = {
- render: () => ,
-}
-
-export const WithCustomTitle: Story = {
- args: {
- isOpen: true,
- title: 'Choose an Integration',
- connectors: connectors,
- },
-}
-
-export const EmptyState: Story = {
- args: {
- isOpen: true,
- connectors: [],
- },
-}
-
-// Prefiltered Search Example
-const PreFilteredExample = () => {
- const [isOpen, setIsOpen] = useState(false)
- const [prefilterValue] = useState('disc')
-
- return (
-
- setIsOpen(true)}>
- Open with "disc" Search
-
- setIsOpen(false)}
- connectors={connectors}
- onSelectConnector={(connector) => {
- console.log('Selected connector:', connector)
- setIsOpen(false)
- }}
- initialSearchValue={prefilterValue}
- />
-
- )
-}
-
-export const WithSearchPrefiltered: Story = {
- render: () => ,
-}
-
-// Add a story with many connectors to test scrolling
-const ManyConnectorsExample = () => {
- const [isOpen, setIsOpen] = useState(false)
-
- return (
-
- setIsOpen(true)}>
- Open Modal with Many Connectors
-
- setIsOpen(false)}
- connectors={manyConnectors}
- onSelectConnector={(connector) => {
- console.log('Selected connector:', connector)
- setIsOpen(false)
- }}
- />
-
- )
-}
-
-export const WithManyConnectors: Story = {
- render: () => ,
-}
diff --git a/packages/ui-v1/domain-components/index.ts b/packages/ui-v1/domain-components/index.ts
index b34d67341..fc57eeed7 100644
--- a/packages/ui-v1/domain-components/index.ts
+++ b/packages/ui-v1/domain-components/index.ts
@@ -4,4 +4,3 @@ export * from './ConnectorConfigForm'
export * from './ConnectionsCardView'
export * from './tables'
export * from './ConnectionStatus'
-export * from './LinkConnectorModal'