Skip to content

Commit a1a6635

Browse files
committed
WIP bringing back modal add
1 parent 8a65c72 commit a1a6635

File tree

10 files changed

+308
-492
lines changed

10 files changed

+308
-492
lines changed

apps/web/app/connect/AddConnection.client.tsx

Lines changed: 173 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,199 @@
33
import type {ConnectorConfig} from '@openint/api-v1/trpc/routers/connectorConfig.models'
44
import type {Id} from '@openint/cdk'
55

6+
import {SearchIcon} from 'lucide-react'
7+
import {ReactElement, useState} from 'react'
68
import {type ConnectorName} from '@openint/api-v1/trpc/routers/connector.models'
7-
import {Label} from '@openint/shadcn/ui'
8-
import {ConnectorConfigCard} from '@openint/ui-v1/domain-components/ConnectorConfigCard'
9+
import {cn} from '@openint/shadcn/lib/utils'
10+
import {
11+
Dialog,
12+
DialogContent,
13+
DialogHeader,
14+
DialogTitle,
15+
Input,
16+
} from '@openint/shadcn/ui'
17+
import {ConnectorDisplay} from '@openint/ui-v1/domain-components/ConnectorDisplay'
18+
import {useMutableSearchParams} from '@openint/ui-v1/hooks/useStateFromSearchParam'
919
import {ConnectorConnectContainer} from './ConnectorConnect.client'
1020

1121
export type ConnectorConfigForCustomer = Pick<
1222
ConnectorConfig<'connector'>,
1323
'id' | 'connector_name' | 'connector'
1424
>
1525

26+
export type AddConnectionPrefetchedCard = {
27+
connectorName: ConnectorName
28+
connectorDisplayName?: string
29+
card: ReactElement
30+
}
31+
32+
export function AddConnectionClient({
33+
cards,
34+
className,
35+
}: {
36+
cards: AddConnectionPrefetchedCard[]
37+
className?: string
38+
}) {
39+
const [isOpen, setIsOpen] = useState(true)
40+
const [_, setSearchParams] = useMutableSearchParams()
41+
42+
const onClose = () => {
43+
setIsOpen(false)
44+
setSearchParams({view: 'manage'}, {shallow: false})
45+
}
46+
const [searchQuery, setSearchQuery] = useState('')
47+
const [isFocused, setIsFocused] = useState(false)
48+
49+
const filteredCards = cards.filter((card) => {
50+
const searchLower = searchQuery.toLowerCase()
51+
return (
52+
card.connectorName.toLowerCase().includes(searchLower) ||
53+
card.connectorDisplayName?.toLowerCase().includes(searchLower)
54+
)
55+
})
56+
57+
return (
58+
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
59+
{/* TODO: consider making full screen horizontal on mobile https://github.com/radix-ui/themes/issues/142 */}
60+
<DialogContent
61+
className={cn(
62+
'flex h-[85vh] max-h-[600px] max-w-2xl flex-col gap-0 overflow-hidden p-0 shadow-xl',
63+
className,
64+
)}>
65+
<DialogHeader className="bg-foreground/5 flex-shrink-0 border-b px-6 py-4">
66+
<DialogTitle className="text-md">Add Integrations</DialogTitle>
67+
</DialogHeader>
68+
69+
<div className="flex flex-1 flex-col overflow-hidden">
70+
<div
71+
className={cn(
72+
'bg-background sticky top-0 z-10 flex-shrink-0 transition-all duration-200',
73+
'border-b border-b-transparent',
74+
isFocused ? 'border-b-gray-100 shadow-sm' : '',
75+
)}>
76+
<div className="p-6 pb-3">
77+
<div className="relative">
78+
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-gray-500" />
79+
<Input
80+
placeholder="Search integrations"
81+
value={searchQuery}
82+
onChange={(e) => setSearchQuery(e.target.value)}
83+
className="border-gray-200 bg-gray-50/50 pl-10 transition-all duration-200 focus:bg-white focus:shadow-sm"
84+
onFocus={() => setIsFocused(true)}
85+
onBlur={() => setIsFocused(false)}
86+
/>
87+
</div>
88+
</div>
89+
</div>
90+
91+
<div
92+
className="relative flex-1 overflow-y-auto"
93+
onScroll={(e) => {
94+
const scrollTop = e.currentTarget.scrollTop
95+
const shouldBeFocused = scrollTop > 5
96+
if (shouldBeFocused !== isFocused) {
97+
setIsFocused(shouldBeFocused)
98+
}
99+
}}>
100+
{/* Mobile view */}
101+
<div className="p-6 pt-3 md:hidden">
102+
{filteredCards.map((card, index) => (
103+
<div key={`${card.connectorName}-${index}`}>
104+
{card.card}
105+
{index < filteredCards.length - 1 && (
106+
<div className="my-1 h-px bg-gray-100" />
107+
)}
108+
</div>
109+
))}
110+
</div>
111+
112+
{/* Desktop view: Grid layout */}
113+
<div className="hidden p-6 pt-3 md:grid md:grid-cols-2 md:gap-4">
114+
{filteredCards.map((card, index) => (
115+
<div
116+
key={`${card.connectorName}-${index}`}
117+
className="transition-transform duration-100 hover:scale-[1.01]">
118+
{card.card}
119+
</div>
120+
))}
121+
</div>
122+
123+
{filteredCards.length === 0 && (
124+
<div className="col-span-2 py-16 text-center text-gray-500">
125+
No integrations found matching{' '}
126+
{searchQuery ? `"${searchQuery}"` : 'your search'}
127+
</div>
128+
)}
129+
</div>
130+
</div>
131+
</DialogContent>
132+
</Dialog>
133+
)
134+
}
135+
16136
export function AddConnectionCard({
17137
connectorConfig,
18138
}: {
19139
connectorConfig: ConnectorConfigForCustomer
20140
onReady?: (ctx: {state: string}, name: string) => void
21141
}) {
142+
const [_, setSearchParams] = useMutableSearchParams()
143+
144+
const handleSelect = () => {
145+
setSearchParams({view: 'manage'}, {shallow: false})
146+
}
147+
22148
return (
23149
<ConnectorConnectContainer
24150
connectorName={connectorConfig.connector_name as ConnectorName}
25151
connector={connectorConfig.connector!}
26152
connectorConfigId={connectorConfig.id as Id['ccfg']}>
27153
{({isConnecting, handleConnect}) => (
28-
<ConnectorConfigCard
29-
displayNameLocation="right"
30-
// TODO: fix this
31-
connectorConfig={connectorConfig as ConnectorConfig<'connector'>}
32-
onPress={() => {
33-
if (isConnecting) {
34-
return
35-
}
36-
handleConnect()
37-
}}>
38-
<Label className="text-muted-foreground pointer-events-none ml-auto text-sm">
39-
{isConnecting ? 'Connecting...' : 'Connect'}
40-
</Label>
41-
</ConnectorConfigCard>
154+
<>
155+
{/* NOTE/TODO: note sure how to remove duplication,
156+
its there as we're using tailwind for consistent breakpoints with AddConnectionClient */}
157+
{/* Mobile view (visible below md breakpoint) */}
158+
<div className="md:hidden">
159+
<ConnectorDisplay
160+
connector={connectorConfig.connector!}
161+
className={cn(
162+
'transition-transform duration-100',
163+
!isConnecting && 'hover:scale-[1.01]',
164+
isConnecting && 'cursor-not-allowed opacity-70',
165+
)}
166+
displayBadges={false}
167+
mode="row"
168+
onPress={() => {
169+
if (isConnecting) {
170+
return
171+
}
172+
handleSelect()
173+
handleConnect()
174+
}}
175+
/>
176+
</div>
177+
178+
{/* Desktop view (visible at md breakpoint and above) */}
179+
<div className="hidden md:block">
180+
<ConnectorDisplay
181+
connector={connectorConfig.connector!}
182+
className={cn(
183+
'transition-transform duration-100',
184+
!isConnecting && 'hover:scale-[1.01]',
185+
isConnecting && 'cursor-not-allowed opacity-70',
186+
)}
187+
displayBadges={false}
188+
mode="card"
189+
onPress={() => {
190+
if (isConnecting) {
191+
return
192+
}
193+
handleSelect()
194+
handleConnect()
195+
}}
196+
/>
197+
</div>
198+
</>
42199
)}
43200
</ConnectorConnectContainer>
44201
)
Lines changed: 90 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import {useSuspenseQuery} from '@tanstack/react-query'
4+
import {Link} from 'lucide-react'
45
import React from 'react'
56
import {type ConnectorName} from '@openint/api-v1/trpc/routers/connector.models'
67
import {Id} from '@openint/cdk'
@@ -9,7 +10,7 @@ import {
910
CommandPopover,
1011
ConnectionStatusPill,
1112
DataTileView,
12-
Spinner,
13+
LoadingSpinner,
1314
useMutableSearchParams,
1415
} from '@openint/ui-v1'
1516
import {ConnectionCard} from '@openint/ui-v1/domain-components/ConnectionCard'
@@ -41,75 +42,106 @@ export function MyConnectionsClient(props: {
4142
const definitions = useCommandDefinitionMap()
4243

4344
if (isLoading) {
44-
return (
45-
<div className="flex min-h-[200px] items-center justify-center">
46-
<Spinner />
47-
</div>
48-
)
45+
return <LoadingSpinner />
4946
}
5047

5148
if (!res.data?.items?.length || res.data.items.length === 0) {
5249
return (
53-
<div className="flex flex-col items-center gap-4 py-8 text-center">
54-
<p className="text-muted-foreground">
55-
You have no configured integrations.
56-
</p>
50+
<div className="flex min-h-[200px] flex-col items-center justify-center gap-6 py-12">
51+
<div className="flex flex-col items-center gap-4 text-center">
52+
<div className="bg-primary/10 rounded-full p-4">
53+
<Link className="text-primary h-8 w-8" />
54+
</div>
55+
<h3 className="text-xl font-semibold">
56+
Let&apos;s Get You Connected
57+
</h3>
58+
<p className="text-muted-foreground">
59+
You have not configured any integrations yet. Connect your first app
60+
to get started.
61+
</p>
62+
</div>
5763
<Button
58-
variant="default"
59-
onClick={() => setSearchParams({view: 'add'}, {shallow: true})}>
60-
Add your first integration
64+
size="lg"
65+
className="font-medium"
66+
onClick={() => {
67+
setIsLoading(true)
68+
setSearchParams({view: 'add'}, {shallow: false})
69+
}}
70+
disabled={isLoading}>
71+
Add First Integration
6172
</Button>
6273
</div>
6374
)
6475
}
6576

6677
return (
67-
<DataTileView
68-
data={res.data.items}
69-
columns={[]}
70-
getItemId={(conn) => conn.id}
71-
renderItem={(conn) => {
72-
const renderCard = ({handleConnect}: {handleConnect?: () => void}) => (
73-
<ConnectionCard
74-
connection={conn}
75-
className="relative"
76-
onReconnect={handleConnect}>
77-
<CommandPopover
78-
className="absolute right-2 top-2"
79-
hideGroupHeadings
80-
initialParams={{
81-
connection_id: conn.id,
82-
}}
83-
ctx={{}}
84-
definitions={definitions}
85-
header={
86-
<>
87-
<div className="flex items-center justify-center gap-1 text-center">
88-
<ConnectionStatusPill status={conn.status} />
89-
<span className="text-muted-foreground text-xs">
90-
({timeSince(conn.updated_at)})
91-
</span>
92-
</div>
93-
</>
94-
}
95-
/>
96-
</ConnectionCard>
97-
)
78+
<>
79+
<div className="flex items-center justify-between">
80+
<h1 className="text-xl font-bold">My Integrations</h1>
81+
<Button
82+
size="lg"
83+
onClick={() => {
84+
setIsLoading(true)
85+
setSearchParams({view: 'add'}, {shallow: false})
86+
}}
87+
disabled={isLoading}>
88+
Add Integration
89+
</Button>
90+
</div>
91+
92+
<div className="mt-4"></div>
93+
<DataTileView
94+
data={res.data.items}
95+
columns={[]}
96+
className="grid grid-cols-2 gap-6 md:grid-cols-4"
97+
getItemId={(conn) => conn.id}
98+
renderItem={(conn) => {
99+
const renderCard = ({
100+
handleConnect,
101+
}: {
102+
handleConnect?: () => void
103+
}) => (
104+
<ConnectionCard
105+
connection={conn}
106+
className="relative"
107+
onReconnect={handleConnect}>
108+
<CommandPopover
109+
className="absolute right-2 top-2"
110+
hideGroupHeadings
111+
initialParams={{
112+
connection_id: conn.id,
113+
}}
114+
ctx={{}}
115+
definitions={definitions}
116+
header={
117+
<>
118+
<div className="flex items-center justify-center gap-1 text-center">
119+
<ConnectionStatusPill status={conn.status} />
120+
<span className="text-muted-foreground text-xs">
121+
({timeSince(conn.updated_at)})
122+
</span>
123+
</div>
124+
</>
125+
}
126+
/>
127+
</ConnectionCard>
128+
)
98129

99-
if (conn.status !== 'disconnected') {
100-
return renderCard({handleConnect: undefined})
101-
}
130+
if (conn.status !== 'disconnected') {
131+
return renderCard({handleConnect: undefined})
132+
}
102133

103-
return (
104-
<ConnectorConnectContainer
105-
connectorName={conn.connector_name as ConnectorName}
106-
connector={{auth_type: 'OAUTH2'} as any}
107-
connectorConfigId={conn.connector_config_id as Id['ccfg']}
108-
connectionId={conn.id as Id['conn']}>
109-
{renderCard}
110-
</ConnectorConnectContainer>
111-
)
112-
}}
113-
/>
134+
return (
135+
<ConnectorConnectContainer
136+
connectorName={conn.connector_name as ConnectorName}
137+
connector={{auth_type: 'OAUTH2'} as any}
138+
connectorConfigId={conn.connector_config_id as Id['ccfg']}
139+
connectionId={conn.id as Id['conn']}>
140+
{renderCard}
141+
</ConnectorConnectContainer>
142+
)
143+
}}
144+
/>
145+
</>
114146
)
115147
}

0 commit comments

Comments
 (0)