Skip to content

Commit 51e2b39

Browse files
waleedlatif1dependabot[bot]emir-karabeg
authored
feat(oauth): general oauth improvements, added x and supabase oauth (#186)
* added x oauth * added x oauth to x tools * fix(deps)(deps): bump vite in /sim in the dependencies group (#183) * improvement(vars): variable rename reference change * fix(ui): scrollbar styling across all browsers * feat(error-handling): created error path and upgraded ports * improvement(ui/ux): console * fix(connection-block): spacing * added supabase oauth * ui improvements for action required banner in settings > credentials * improvement(oauth): if there is already an account connected, don't display the connect button in the credential selector * fixed typo in marketplace for customer_service --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
1 parent 601d77f commit 51e2b39

12 files changed

Lines changed: 372 additions & 212 deletions

File tree

sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
4747
'read:page:confluence': 'Read Confluence pages',
4848
'write:confluence-content': 'Write Confluence content',
4949
'read:me': 'Read your profile information',
50+
'database.read': 'Read your database',
51+
'database.write': 'Write to your database',
52+
'projects.read': 'Read your projects',
5053
offline_access: 'Access your account when you are not using the application',
5154
repo: 'Access your repositories',
5255
workflow: 'Manage repository workflows',
@@ -105,6 +108,7 @@ export function OAuthRequiredModal({
105108
saveToStorage<string[]>('pending_oauth_scopes', requiredScopes)
106109
saveToStorage<string>('pending_oauth_return_url', window.location.href)
107110
saveToStorage<string>('pending_oauth_provider_id', providerId)
111+
saveToStorage<boolean>('from_oauth_modal', true)
108112

109113
// Close the modal
110114
onClose()

sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useMemo, useState } from 'react'
44
import { Check, ChevronDown, ExternalLink, RefreshCw } from 'lucide-react'
55
import { Button } from '@/components/ui/button'
66
import {
@@ -50,27 +50,21 @@ export function CredentialSelector({
5050
const [isLoading, setIsLoading] = useState(false)
5151
const [showOAuthModal, setShowOAuthModal] = useState(false)
5252
const [selectedId, setSelectedId] = useState(value)
53-
const initialFetchRef = useRef(false)
5453

55-
// Determine the appropriate service ID based on provider and scopes
56-
const getServiceId = (): string => {
57-
if (serviceId) return serviceId
58-
return getServiceIdFromScopes(provider, requiredScopes)
59-
}
54+
// Derive service and provider IDs using useMemo
55+
const effectiveServiceId = useMemo(() => {
56+
return serviceId || getServiceIdFromScopes(provider, requiredScopes)
57+
}, [provider, requiredScopes, serviceId])
6058

61-
// Determine the appropriate provider ID based on service and scopes
62-
const getProviderId = (): string => {
63-
const effectiveServiceId = getServiceId()
59+
const effectiveProviderId = useMemo(() => {
6460
return getProviderIdFromServiceId(effectiveServiceId)
65-
}
61+
}, [effectiveServiceId])
6662

6763
// Fetch available credentials for this provider
6864
const fetchCredentials = useCallback(async () => {
6965
setIsLoading(true)
7066
try {
71-
const providerId = getProviderId()
72-
73-
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
67+
const response = await fetch(`/api/auth/oauth/credentials?provider=${effectiveProviderId}`)
7468
if (response.ok) {
7569
const data = await response.json()
7670
setCredentials(data.credentials)
@@ -105,28 +99,44 @@ export function CredentialSelector({
10599
} finally {
106100
setIsLoading(false)
107101
}
108-
}, [provider, onChange, selectedId, getProviderId])
109-
110-
// Fetch credentials on initial mount and when dependencies change
111-
useEffect(() => {
112-
if (!initialFetchRef.current) {
113-
fetchCredentials()
114-
initialFetchRef.current = true
115-
}
116-
}, [fetchCredentials])
102+
}, [effectiveProviderId, onChange, selectedId])
117103

118-
// Also fetch credentials when opening the popover
104+
// Fetch credentials on initial mount
119105
useEffect(() => {
120-
if (open) {
121-
fetchCredentials()
122-
}
123-
}, [open, fetchCredentials])
106+
fetchCredentials()
107+
// This effect should only run once on mount, so empty dependency array
108+
// eslint-disable-next-line react-hooks/exhaustive-deps
109+
}, [])
124110

125111
// Update local state when external value changes
126112
useEffect(() => {
127113
setSelectedId(value)
128114
}, [value])
129115

116+
// Listen for visibility changes to update credentials when user returns from settings
117+
useEffect(() => {
118+
const handleVisibilityChange = () => {
119+
if (document.visibilityState === 'visible') {
120+
fetchCredentials()
121+
}
122+
}
123+
124+
document.addEventListener('visibilitychange', handleVisibilityChange)
125+
126+
return () => {
127+
document.removeEventListener('visibilitychange', handleVisibilityChange)
128+
}
129+
}, [fetchCredentials])
130+
131+
// Handle popover open to fetch fresh credentials
132+
const handleOpenChange = (isOpen: boolean) => {
133+
setOpen(isOpen)
134+
if (isOpen) {
135+
// Fetch fresh credentials when opening the dropdown
136+
fetchCredentials()
137+
}
138+
}
139+
130140
// Get the selected credential
131141
const selectedCredential = credentials.find((cred) => cred.id === selectedId)
132142

@@ -139,14 +149,11 @@ export function CredentialSelector({
139149

140150
// Handle adding a new credential
141151
const handleAddCredential = () => {
142-
const effectiveServiceId = getServiceId()
143-
const providerId = getProviderId()
144-
145152
// Store information about the required connection
146153
saveToStorage<string>('pending_service_id', effectiveServiceId)
147154
saveToStorage<string[]>('pending_oauth_scopes', requiredScopes)
148155
saveToStorage<string>('pending_oauth_return_url', window.location.href)
149-
saveToStorage<string>('pending_oauth_provider_id', providerId)
156+
saveToStorage<string>('pending_oauth_provider_id', effectiveProviderId)
150157

151158
// Show the OAuth modal
152159
setShowOAuthModal(true)
@@ -184,7 +191,7 @@ export function CredentialSelector({
184191

185192
return (
186193
<>
187-
<Popover open={open} onOpenChange={setOpen}>
194+
<Popover open={open} onOpenChange={handleOpenChange}>
188195
<PopoverTrigger asChild>
189196
<Button
190197
variant="outline"
@@ -243,14 +250,16 @@ export function CredentialSelector({
243250
))}
244251
</CommandGroup>
245252
)}
246-
<CommandGroup>
247-
<CommandItem onSelect={handleAddCredential}>
248-
<div className="flex items-center gap-2 text-primary">
249-
{getProviderIcon(provider)}
250-
<span>Connect {getProviderName(provider)} account</span>
251-
</div>
252-
</CommandItem>
253-
</CommandGroup>
253+
{credentials.length === 0 && (
254+
<CommandGroup>
255+
<CommandItem onSelect={handleAddCredential}>
256+
<div className="flex items-center gap-2 text-primary">
257+
{getProviderIcon(provider)}
258+
<span>Connect {getProviderName(provider)} account</span>
259+
</div>
260+
</CommandItem>
261+
</CommandGroup>
262+
)}
254263
</CommandList>
255264
</Command>
256265
</PopoverContent>
@@ -263,7 +272,7 @@ export function CredentialSelector({
263272
provider={provider}
264273
toolName={getProviderName(provider)}
265274
requiredScopes={requiredScopes}
266-
serviceId={getServiceId()}
275+
serviceId={effectiveServiceId}
267276
/>
268277
)}
269278
</>

sim/app/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use client'
22

3-
import { useEffect, useState } from 'react'
3+
import { useEffect, useRef, useState } from 'react'
44
import { useRouter, useSearchParams } from 'next/navigation'
5-
import { Check, ExternalLink, Plus, RefreshCw } from 'lucide-react'
5+
import { Check, ChevronDown, ExternalLink, Plus, RefreshCw } from 'lucide-react'
66
import { Button } from '@/components/ui/button'
77
import { Card } from '@/components/ui/card'
88
import { Skeleton } from '@/components/ui/skeleton'
@@ -29,13 +29,15 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
2929
const searchParams = useSearchParams()
3030
const { data: session } = useSession()
3131
const userId = session?.user?.id
32+
const pendingServiceRef = useRef<HTMLDivElement>(null)
3233

3334
const [services, setServices] = useState<ServiceInfo[]>([])
3435
const [isLoading, setIsLoading] = useState(true)
3536
const [isConnecting, setIsConnecting] = useState<string | null>(null)
3637
const [pendingService, setPendingService] = useState<string | null>(null)
3738
const [pendingScopes, setPendingScopes] = useState<string[]>([])
3839
const [authSuccess, setAuthSuccess] = useState(false)
40+
const [showActionRequired, setShowActionRequired] = useState(false)
3941

4042
// Define available services from our standardized OAuth providers
4143
const defineServices = (): ServiceInfo[] => {
@@ -161,16 +163,21 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
161163
const serviceId = loadFromStorage<string>('pending_service_id')
162164
const scopes = loadFromStorage<string[]>('pending_oauth_scopes') || []
163165
const returnUrl = loadFromStorage<string>('pending_oauth_return_url')
166+
const fromOAuthModal = loadFromStorage<boolean>('from_oauth_modal')
164167

165168
if (serviceId) {
166169
setPendingService(serviceId)
167170
setPendingScopes(scopes)
168171

172+
// Only show action required notification if navigated from the OAuth modal
173+
setShowActionRequired(!!fromOAuthModal)
174+
169175
// Clear the pending connection after a short delay
170176
// This gives the user time to see the highlighted connection
171177
setTimeout(() => {
172178
removeFromStorage('pending_service_id')
173179
removeFromStorage('pending_oauth_scopes')
180+
removeFromStorage('from_oauth_modal')
174181
}, 500)
175182
}
176183

@@ -238,7 +245,7 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
238245
},
239246
body: JSON.stringify({
240247
provider: service.providerId.split('-')[0],
241-
accountId,
248+
providerId: service.providerId,
242249
}),
243250
})
244251

@@ -285,6 +292,15 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
285292
{} as Record<string, ServiceInfo[]>
286293
)
287294

295+
const scrollToHighlightedService = () => {
296+
if (pendingServiceRef.current) {
297+
pendingServiceRef.current.scrollIntoView({
298+
behavior: 'smooth',
299+
block: 'center',
300+
})
301+
}
302+
}
303+
288304
return (
289305
<div className="p-6 space-y-6">
290306
<div>
@@ -308,17 +324,27 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
308324
</div>
309325
)}
310326

311-
{/* Pending service message */}
312-
{pendingService && (
313-
<div className="mb-6 p-4 bg-primary/10 border border-primary rounded-md text-sm flex items-start gap-2">
314-
<div className="min-w-4 mt-0.5">
327+
{/* Pending service message - only shown when coming from OAuth required modal */}
328+
{pendingService && showActionRequired && (
329+
<div className="mb-6 p-5 bg-primary/5 border border-primary/20 rounded-md text-sm flex items-start gap-3 shadow-sm">
330+
<div className="min-w-5 mt-0.5">
315331
<ExternalLink className="h-4 w-4 text-primary" />
316332
</div>
317-
<p>
318-
<span className="font-medium text-primary">Action Required:</span> Please connect your
319-
account to enable the requested features. The required service will be highlighted
320-
below.
321-
</p>
333+
<div className="flex flex-col flex-1">
334+
<p className="text-muted-foreground">
335+
<span className="font-medium text-primary">Action Required:</span> Please connect your
336+
account to enable the requested features. The required service is highlighted below.
337+
</p>
338+
<Button
339+
variant="outline"
340+
size="sm"
341+
onClick={scrollToHighlightedService}
342+
className="mt-3 self-start text-sm font-medium text-primary border-primary/20 hover:bg-primary/10 hover:text-primary hover:border-primary transition-colors flex items-center gap-1.5 px-3 h-8"
343+
>
344+
<span>Go to service</span>
345+
<ChevronDown className="h-3.5 w-3.5" />
346+
</Button>
347+
</div>
322348
</div>
323349
)}
324350

@@ -346,6 +372,7 @@ export function Credentials({ onOpenChange }: CredentialsProps) {
346372
'p-6 transition-all hover:shadow-md',
347373
pendingService === service.id && 'border-primary shadow-md'
348374
)}
375+
ref={pendingService === service.id ? pendingServiceRef : undefined}
349376
>
350377
<div className="flex items-start justify-between gap-4">
351378
<div className="flex items-start gap-4">

sim/app/w/marketplace/components/toolbar/toolbar.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,9 @@
11
'use client'
22

33
import { useEffect, useState } from 'react'
4-
import {
5-
BotMessageSquare,
6-
Clock,
7-
Code,
8-
LineChart,
9-
MailIcon,
10-
PanelLeftClose,
11-
PanelRight,
12-
Sparkles,
13-
Star,
14-
Store,
15-
} from 'lucide-react'
4+
import { Clock, Star } from 'lucide-react'
165
import { Button } from '@/components/ui/button'
17-
import { ScrollArea } from '@/components/ui/scroll-area'
18-
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
19-
import { CATEGORIES, getCategoryIcon } from '../../constants/categories'
6+
import { CATEGORIES, getCategoryIcon, getCategoryLabel } from '../../constants/categories'
207

218
export type MarketplaceCategory = 'popular' | 'programming' | 'marketing' | 'all'
229

@@ -62,7 +49,11 @@ export function Toolbar({ scrollToSection, activeSection }: ToolbarProps) {
6249
onClick={() => scrollToSection(category)}
6350
>
6451
{specialIcons[category] || getCategoryIcon(category)}
65-
{category}
52+
{category === 'popular'
53+
? 'Popular'
54+
: category === 'recent'
55+
? 'Recent'
56+
: getCategoryLabel(category)}
6657
</Button>
6758
))}
6859
</nav>

sim/app/w/marketplace/marketplace.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
'use client'
22

33
import { useEffect, useMemo, useRef, useState } from 'react'
4-
import Link from 'next/link'
5-
import { motion } from 'framer-motion'
6-
import { AlertCircle, ArrowLeft, Search } from 'lucide-react'
7-
import { Input } from '@/components/ui/input'
4+
import { AlertCircle } from 'lucide-react'
85
import { ControlBar } from './components/control-bar/control-bar'
96
import { ErrorMessage } from './components/error-message'
107
import { Section } from './components/section'
118
import { Toolbar } from './components/toolbar/toolbar'
129
import { WorkflowCard } from './components/workflow-card'
1310
import { WorkflowCardSkeleton } from './components/workflow-card-skeleton'
14-
import { CATEGORIES } from './constants/categories'
11+
import { CATEGORIES, getCategoryLabel } from './constants/categories'
1512

1613
// Types
1714
export interface Workflow {
@@ -545,7 +542,7 @@ export default function Marketplace() {
545542
<Section
546543
key={category}
547544
id={category}
548-
title={category}
545+
title={getCategoryLabel(category)}
549546
ref={(el) => {
550547
if (el) {
551548
sectionRefs.current[category] = el

0 commit comments

Comments
 (0)