From aebc8e9578ad84db3a0ee9c4512c4559adaadf1c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 16 Mar 2025 13:56:29 -0700 Subject: [PATCH] improvement: use google file/folder picker instead of custom picker to remove requirement of restricted scopes. co-localize many input components and move them out of general components/ui/ --- sim/.env.example | 5 +- sim/app/api/auth/oauth/token/route.ts | 61 ++++- sim/app/blocks/blocks/docs.ts | 4 +- sim/app/blocks/blocks/sheets.ts | 2 +- sim/app/tools/docs/read.ts | 5 +- sim/app/tools/sheets/read.ts | 5 +- .../components}/oauth-required-modal.tsx | 0 .../credential-selector.tsx | 2 +- .../components/google-drive-picker.tsx} | 255 ++++++++---------- .../file-selector-input.tsx | 9 +- .../components}/folder-selector-input.tsx | 2 +- .../folder-selector}/folder-selector.tsx | 2 +- .../webhook/components}/webhook-modal.tsx | 6 +- .../{ => webhook}/webhook-config.tsx | 4 +- .../components/sub-block/sub-block.tsx | 16 +- sim/next.config.ts | 14 +- sim/package-lock.json | 11 + sim/package.json | 1 + 18 files changed, 227 insertions(+), 177 deletions(-) rename sim/app/{components/ui => w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components}/oauth-required-modal.tsx (100%) rename sim/app/w/[id]/components/workflow-block/components/sub-block/components/{ => credential-selector}/credential-selector.tsx (98%) rename sim/app/{components/ui/file-selector.tsx => w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx} (78%) rename sim/app/w/[id]/components/workflow-block/components/sub-block/components/{ => file-selector}/file-selector-input.tsx (83%) rename sim/app/w/[id]/components/workflow-block/components/sub-block/components/{ => folder-selector/components}/folder-selector-input.tsx (95%) rename sim/app/{components/ui => w/[id]/components/workflow-block/components/sub-block/components/folder-selector}/folder-selector.tsx (98%) rename sim/app/{components/ui => w/[id]/components/workflow-block/components/sub-block/components/webhook/components}/webhook-modal.tsx (99%) rename sim/app/w/[id]/components/workflow-block/components/sub-block/components/{ => webhook}/webhook-config.tsx (98%) diff --git a/sim/.env.example b/sim/.env.example index 58f2df988b..5a6375afa4 100644 --- a/sim/.env.example +++ b/sim/.env.example @@ -9,4 +9,7 @@ BETTER_AUTH_URL=http://localhost:3000 RESEND_API_KEY=your_resend_api_key # Get from https://resend.com # App URL (Required) -NEXT_PUBLIC_APP_URL=http://localhost:3000 \ No newline at end of file +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# StackBlitz (Webcontainer) API Key (Optional, for handling sandboxed code execution for functions/custom-tools) +WEBCONTAINER_CLIENT_ID=your_webcontainer_client_id \ No newline at end of file diff --git a/sim/app/api/auth/oauth/token/route.ts b/sim/app/api/auth/oauth/token/route.ts index b67128d47c..d5d76c0cdf 100644 --- a/sim/app/api/auth/oauth/token/route.ts +++ b/sim/app/api/auth/oauth/token/route.ts @@ -113,15 +113,18 @@ export async function POST(request: NextRequest) { } /** - * Get an OAuth token for a given credential ID + * Get the access token for a specific credential */ export async function GET(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) // Short request ID for correlation + try { // Get the session const session = await getSession() // Check if the user is authenticated if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthenticated request rejected`) return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) } @@ -130,30 +133,72 @@ export async function GET(request: NextRequest) { const credentialId = searchParams.get('credentialId') if (!credentialId) { + logger.warn(`[${requestId}] Missing credential ID`) return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) } // Get the credential from the database - const credentials = await db - .select() - .from(account) - .where(and(eq(account.id, credentialId), eq(account.userId, session.user.id))) - .limit(1) + const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1) if (!credentials.length) { + logger.warn(`[${requestId}] Credential not found`, { credentialId }) return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } const credential = credentials[0] + // Check if the credential belongs to the user + if (credential.userId !== session.user.id) { + logger.warn(`[${requestId}] Unauthorized credential access attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) + } + // Check if the access token is valid if (!credential.accessToken) { + logger.warn(`[${requestId}] No access token available for credential`) return NextResponse.json({ error: 'No access token available' }, { status: 400 }) } - return NextResponse.json({ accessToken: credential.accessToken }, { status: 200 }) + // Check if the token is expired and refresh if needed + const now = new Date() + const tokenExpiry = credential.accessTokenExpiresAt + let accessToken = credential.accessToken + + if (tokenExpiry && tokenExpiry < now && credential.refreshToken) { + logger.info(`[${requestId}] Access token expired, attempting to refresh`) + + try { + // Refresh the token using the centralized utility + const refreshedToken = await refreshOAuthToken( + credential.providerId, + credential.refreshToken + ) + + if (refreshedToken) { + logger.info(`[${requestId}] Token refreshed successfully`) + + // Update the token in the database + await db + .update(account) + .set({ + accessToken: refreshedToken, + accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), // Default 1 hour expiry + updatedAt: new Date(), + }) + .where(eq(account.id, credentialId)) + + accessToken = refreshedToken + } + } catch (refreshError) { + logger.error(`[${requestId}] Error refreshing token`, refreshError) + return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) + } + } + + // Return the access token + return NextResponse.json({ accessToken }, { status: 200 }) } catch (error) { - logger.error('Error getting OAuth token:', { error }) + logger.error(`[${requestId}] Error fetching access token`, error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/sim/app/blocks/blocks/docs.ts b/sim/app/blocks/blocks/docs.ts index 9d862ab33e..36636289ff 100644 --- a/sim/app/blocks/blocks/docs.ts +++ b/sim/app/blocks/blocks/docs.ts @@ -52,7 +52,7 @@ export const GoogleDocsBlock: BlockConfig = { layout: 'full', provider: 'google-drive', serviceId: 'google-drive', - requiredScopes: ['https://www.googleapis.com/auth/drive.readonly'], + requiredScopes: [], mimeType: 'application/vnd.google-apps.document', placeholder: 'Select a document', condition: { field: 'operation', value: 'read' }, @@ -65,7 +65,7 @@ export const GoogleDocsBlock: BlockConfig = { layout: 'full', provider: 'google-drive', serviceId: 'google-drive', - requiredScopes: ['https://www.googleapis.com/auth/drive.readonly'], + requiredScopes: [], mimeType: 'application/vnd.google-apps.document', placeholder: 'Select a document', condition: { field: 'operation', value: 'write' }, diff --git a/sim/app/blocks/blocks/sheets.ts b/sim/app/blocks/blocks/sheets.ts index 8a07f5b938..598528a568 100644 --- a/sim/app/blocks/blocks/sheets.ts +++ b/sim/app/blocks/blocks/sheets.ts @@ -52,7 +52,7 @@ export const GoogleSheetsBlock: BlockConfig = { layout: 'full', provider: 'google-drive', serviceId: 'google-drive', - requiredScopes: ['https://www.googleapis.com/auth/drive.readonly'], + requiredScopes: [], mimeType: 'application/vnd.google-apps.spreadsheet', placeholder: 'Select a spreadsheet', }, diff --git a/sim/app/tools/docs/read.ts b/sim/app/tools/docs/read.ts index ee0620810e..28cdbd1362 100644 --- a/sim/app/tools/docs/read.ts +++ b/sim/app/tools/docs/read.ts @@ -9,10 +9,7 @@ export const readTool: ToolConfig oauth: { required: true, provider: 'google-docs', - additionalScopes: [ - 'https://www.googleapis.com/auth/documents', - 'https://www.googleapis.com/auth/drive.readonly', - ], + additionalScopes: ['https://www.googleapis.com/auth/documents'], }, params: { accessToken: { type: 'string', required: true }, diff --git a/sim/app/tools/sheets/read.ts b/sim/app/tools/sheets/read.ts index 85f4e79272..83e6f20be8 100644 --- a/sim/app/tools/sheets/read.ts +++ b/sim/app/tools/sheets/read.ts @@ -9,10 +9,7 @@ export const readTool: ToolConfig void provider: OAuthProvider @@ -52,9 +52,11 @@ interface FileSelectorProps { mimeTypeFilter?: string showPreview?: boolean onFileInfoChange?: (fileInfo: FileInfo | null) => void + clientId: string + apiKey: string } -export function FileSelector({ +export function GoogleDrivePicker({ value, onChange, provider, @@ -65,10 +67,11 @@ export function FileSelector({ mimeTypeFilter, showPreview = true, onFileInfoChange, -}: FileSelectorProps) { + clientId, + apiKey, +}: GoogleDrivePickerProps) { const [open, setOpen] = useState(false) const [credentials, setCredentials] = useState([]) - const [files, setFiles] = useState([]) const [selectedCredentialId, setSelectedCredentialId] = useState('') const [selectedFileId, setSelectedFileId] = useState(value) const [selectedFile, setSelectedFile] = useState(null) @@ -76,6 +79,7 @@ export function FileSelector({ const [isLoadingSelectedFile, setIsLoadingSelectedFile] = useState(false) const [showOAuthModal, setShowOAuthModal] = useState(false) const initialFetchRef = useRef(false) + const [openPicker, authResponse] = useDrivePicker() // Determine the appropriate service ID based on provider and scopes const getServiceId = (): string => { @@ -162,58 +166,6 @@ export function FileSelector({ [selectedCredentialId, onFileInfoChange] ) - // Fetch files from Google Drive - const fetchFiles = useCallback( - async (searchQuery?: string) => { - if (!selectedCredentialId) return - - setIsLoading(true) - try { - // Construct query parameters - const queryParams = new URLSearchParams({ - credentialId: selectedCredentialId, - }) - - if (mimeTypeFilter) { - queryParams.append('mimeType', mimeTypeFilter) - } - - if (searchQuery) { - queryParams.append('query', searchQuery) - } - - const response = await fetch(`/api/auth/oauth/drive/files?${queryParams.toString()}`) - - if (response.ok) { - const data = await response.json() - setFiles(data.files || []) - - // If we have a selected file ID, find the file info - if (selectedFileId) { - const fileInfo = data.files.find((file: FileInfo) => file.id === selectedFileId) - if (fileInfo) { - setSelectedFile(fileInfo) - onFileInfoChange?.(fileInfo) - } else if (!searchQuery) { - // Only try to fetch by ID if this is not a search query - // and we couldn't find the file in the list - fetchFileById(selectedFileId) - } - } - } else { - logger.error('Error fetching files:', { error: await response.text() }) - setFiles([]) - } - } catch (error) { - logger.error('Error fetching files:', { error }) - setFiles([]) - } finally { - setIsLoading(false) - } - }, - [selectedCredentialId, mimeTypeFilter, selectedFileId, onFileInfoChange, fetchFileById] - ) - // Fetch credentials on initial mount useEffect(() => { if (!initialFetchRef.current) { @@ -222,44 +174,101 @@ export function FileSelector({ } }, [fetchCredentials]) - // Fetch files when credential is selected - useEffect(() => { - if (selectedCredentialId) { - fetchFiles() - } - }, [selectedCredentialId, fetchFiles]) - // Update selected file when value changes externally useEffect(() => { if (value !== selectedFileId) { setSelectedFileId(value) - // Find file info if we have files loaded - if (files.length > 0) { - const fileInfo = files.find((file) => file.id === value) || null - setSelectedFile(fileInfo) - onFileInfoChange?.(fileInfo) - } else if (value && selectedCredentialId) { - // If we have a value but no files loaded yet, try to fetch the file by ID + // If we have a value but no file info, try to fetch it + if (value && selectedCredentialId && !selectedFile) { fetchFileById(value) } } - }, [value, files, onFileInfoChange, selectedCredentialId, fetchFileById]) + }, [value, selectedCredentialId, selectedFile, fetchFileById]) - // Try to fetch the file by ID when credentials become available - useEffect(() => { - if (selectedCredentialId && selectedFileId && !selectedFile) { - fetchFileById(selectedFileId) + // Fetch the access token for the selected credential + const fetchAccessToken = async (): Promise => { + if (!selectedCredentialId) { + logger.error('No credential ID selected for Google Drive Picker') + return null } - }, [selectedCredentialId, selectedFileId, selectedFile, fetchFileById]) - // Handle file selection - const handleSelectFile = (file: FileInfo) => { - setSelectedFileId(file.id) - setSelectedFile(file) - onChange(file.id, file) - onFileInfoChange?.(file) - setOpen(false) + setIsLoading(true) + try { + const response = await fetch(`/api/auth/oauth/token?credentialId=${selectedCredentialId}`) + + if (!response.ok) { + throw new Error(`Failed to fetch access token: ${response.status}`) + } + + const data = await response.json() + return data.accessToken || null + } catch (error) { + logger.error('Error fetching access token:', { error }) + return null + } finally { + setIsLoading(false) + } + } + + // Handle opening the Google Drive Picker + const handleOpenPicker = async () => { + try { + // First, get the access token for the selected credential + const accessToken = await fetchAccessToken() + + if (!accessToken) { + logger.error('Failed to get access token for Google Drive Picker') + return + } + + const viewIdForMimeType = () => { + // Return appropriate view based on mime type filter + if (mimeTypeFilter?.includes('spreadsheet')) { + return 'SPREADSHEETS' + } else if (mimeTypeFilter?.includes('document')) { + return 'DOCUMENTS' + } + return 'DOCS' // Default view + } + + openPicker({ + clientId, + developerKey: apiKey, + viewId: viewIdForMimeType(), + token: accessToken, // Use the fetched access token + showUploadView: true, + showUploadFolders: true, + supportDrives: true, + multiselect: false, + callbackFunction: (data) => { + if (data.action === 'picked') { + const file = data.docs[0] + if (file) { + const fileInfo: FileInfo = { + id: file.id, + name: file.name, + mimeType: file.mimeType, + iconLink: file.iconUrl, + webViewLink: file.url, + // thumbnailLink is not directly available from the picker + thumbnailLink: file.iconUrl, // Use iconUrl as fallback + modifiedTime: file.lastEditedUtc + ? new Date(file.lastEditedUtc).toISOString() + : undefined, + } + + setSelectedFileId(file.id) + setSelectedFile(fileInfo) + onChange(file.id, fileInfo) + onFileInfoChange?.(fileInfo) + } + } + }, + }) + } catch (error) { + logger.error('Error opening Google Drive Picker:', { error }) + } } // Handle adding a new credential @@ -278,6 +287,14 @@ export function FileSelector({ setOpen(false) } + // Clear selection + const handleClearSelection = () => { + setSelectedFileId('') + setSelectedFile(null) + onChange('', undefined) + onFileInfoChange?.(null) + } + // Get provider icon const getProviderIcon = (providerName: OAuthProvider) => { const { baseProvider } = parseProvider(providerName) @@ -339,23 +356,6 @@ export function FileSelector({ } } - // Clear selection - const handleClearSelection = () => { - setSelectedFileId('') - setSelectedFile(null) - onChange('', undefined) - onFileInfoChange?.(null) - } - - // Handle search - const handleSearch = (value: string) => { - if (value.length > 2) { - fetchFiles(value) - } else if (value.length === 0) { - fetchFiles() - } - } - // Get file icon based on mime type const getFileIcon = (file: FileInfo, size: 'sm' | 'md' = 'sm') => { const iconSize = size === 'sm' ? 'h-4 w-4' : 'h-5 w-5' @@ -424,13 +424,12 @@ export function FileSelector({ )} - {isLoading ? (
- Loading files... + Loading...
) : credentials.length === 0 ? (
@@ -441,9 +440,9 @@ export function FileSelector({
) : (
-

No files found.

+

Ready to select files.

- Try a different search or account. + Click the button below to open the file picker.

)} @@ -471,25 +470,20 @@ export function FileSelector({ )} - {/* Files list */} - {files.length > 0 && ( + {/* Open picker button - only show if we have credentials */} + {credentials.length > 0 && selectedCredentialId && ( -
- Files -
- {files.map((file) => ( - handleSelectFile(file)} +
+ +
)} @@ -504,17 +498,6 @@ export function FileSelector({ )} - - {/* Add another account option - {credentials.length > 0 && ( - - -
- Connect Another Account -
-
-
- )} */}
diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector-input.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx similarity index 83% rename from sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector-input.tsx rename to sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 47df394f3f..91974e280c 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector-input.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -1,9 +1,9 @@ 'use client' import { useEffect, useState } from 'react' -import { FileInfo, FileSelector } from '@/components/ui/file-selector' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { SubBlockConfig } from '@/blocks/types' +import { FileInfo, GoogleDrivePicker } from './components/google-drive-picker' interface FileSelectorInputProps { blockId: string @@ -31,8 +31,11 @@ export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileS setValue(blockId, subBlock.id, fileId) } + const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '' + const apiKey = process.env.NEXT_PUBLIC_GOOGLE_API_KEY || '' + return ( - ) } diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector-input.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx similarity index 95% rename from sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector-input.tsx rename to sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx index 202d5990da..fbc8f37064 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector-input.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/components/folder-selector-input.tsx @@ -1,9 +1,9 @@ 'use client' import { useEffect, useState } from 'react' -import { FolderInfo, FolderSelector } from '@/components/ui/folder-selector' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { SubBlockConfig } from '@/blocks/types' +import { FolderInfo, FolderSelector } from '../folder-selector' interface FolderSelectorInputProps { blockId: string diff --git a/sim/app/components/ui/folder-selector.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx similarity index 98% rename from sim/app/components/ui/folder-selector.tsx rename to sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx index f1a06b3497..a2a6d18116 100644 --- a/sim/app/components/ui/folder-selector.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx @@ -12,7 +12,6 @@ import { CommandItem, CommandList, } from '@/components/ui/command' -import { OAuthRequiredModal } from '@/components/ui/oauth-required-modal' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { createLogger } from '@/lib/logs/console-logger' import { @@ -22,6 +21,7 @@ import { parseProvider, } from '@/lib/oauth' import { saveToStorage } from '@/stores/workflows/persistence' +import { OAuthRequiredModal } from '@/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal' const logger = createLogger('FolderSelector') diff --git a/sim/app/components/ui/webhook-modal.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx similarity index 99% rename from sim/app/components/ui/webhook-modal.tsx rename to sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx index f6b8ae7116..4541fa0135 100644 --- a/sim/app/components/ui/webhook-modal.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx @@ -24,12 +24,10 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { createLogger } from '@/lib/logs/console-logger' -import { - ProviderConfig, - WEBHOOK_PROVIDERS, -} from '@/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config' +import { ProviderConfig, WEBHOOK_PROVIDERS } from '../webhook-config' const logger = createLogger('WebhookModal') + interface WebhookModalProps { isOpen: boolean onClose: () => void diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx similarity index 98% rename from sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config.tsx rename to sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx index e8faf8721c..cf4ce0c80d 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook-config.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx @@ -3,9 +3,9 @@ import { useParams } from 'next/navigation' import { CheckCircle2, ExternalLink } from 'lucide-react' import { GithubIcon, StripeIcon, WhatsAppIcon } from '@/components/icons' import { Button } from '@/components/ui/button' -import { WebhookModal } from '@/components/ui/webhook-modal' import { createLogger } from '@/lib/logs/console-logger' -import { useSubBlockValue } from '../hooks/use-sub-block-value' +import { useSubBlockValue } from '../../hooks/use-sub-block-value' +import { WebhookModal } from './components/webhook-modal' const logger = createLogger('WebhookConfig') diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index bc96a14ebd..c3145231ff 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -6,12 +6,12 @@ import { SubBlockConfig } from '@/blocks/types' import { CheckboxList } from './components/checkbox-list' import { Code } from './components/code' import { ConditionInput } from './components/condition-input' -import { CredentialSelector } from './components/credential-selector' +import { CredentialSelector } from './components/credential-selector/credential-selector' import { DateInput } from './components/date-input' import { Dropdown } from './components/dropdown' import { EvalInput } from './components/eval-input' -import { FileSelectorInput } from './components/file-selector-input' -import { FolderSelectorInput } from './components/folder-selector-input' +import { FileSelectorInput } from './components/file-selector/file-selector-input' +import { FolderSelectorInput } from './components/folder-selector/components/folder-selector-input' import { LongInput } from './components/long-input' import { ShortInput } from './components/short-input' import { SliderInput } from './components/slider-input' @@ -19,7 +19,7 @@ import { Switch } from './components/switch' import { Table } from './components/table' import { TimeInput } from './components/time-input' import { ToolInput } from './components/tool-input/tool-input' -import { WebhookConfig } from './components/webhook-config' +import { WebhookConfig } from './components/webhook/webhook-config' interface SubBlockProps { blockId: string @@ -75,10 +75,6 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) { /> ) - case 'webhook-config': - return ( - - ) case 'slider': return ( ) + case 'webhook-config': + return ( + + ) case 'oauth-input': return ( =17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/react-hook-form": { "version": "7.54.2", "license": "MIT", diff --git a/sim/package.json b/sim/package.json index e8235019e9..8a290ba4d7 100644 --- a/sim/package.json +++ b/sim/package.json @@ -65,6 +65,7 @@ "react": "^18.2.0", "react-day-picker": "^8.10.1", "react-dom": "^18.2.0", + "react-google-drive-picker": "^1.2.2", "react-hook-form": "^7.54.2", "react-simple-code-editor": "^0.14.1", "reactflow": "^11.11.4",