mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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/
This commit is contained in:
+4
-1
@@ -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
|
||||
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
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
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<GoogleDocsResponse> = {
|
||||
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' },
|
||||
|
||||
@@ -52,7 +52,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
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',
|
||||
},
|
||||
|
||||
@@ -9,10 +9,7 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
|
||||
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 },
|
||||
|
||||
@@ -9,10 +9,7 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
additionalScopes: [
|
||||
'https://www.googleapis.com/auth/spreadsheets',
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
],
|
||||
additionalScopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
||||
},
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true },
|
||||
|
||||
+1
-1
@@ -11,7 +11,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 {
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
parseProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { saveToStorage } from '@/stores/workflows/persistence'
|
||||
import { OAuthRequiredModal } from '../credential-selector/components/oauth-required-modal'
|
||||
|
||||
const logger = createLogger('CredentialSelector')
|
||||
|
||||
+119
-136
@@ -1,18 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink, FileIcon, RefreshCw, Search, X } from 'lucide-react'
|
||||
import { Check, ChevronDown, ExternalLink, FileIcon, RefreshCw, X } from 'lucide-react'
|
||||
import useDrivePicker from 'react-google-drive-picker'
|
||||
import { GoogleDocsIcon, GoogleSheetsIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
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 {
|
||||
@@ -25,8 +24,9 @@ import {
|
||||
parseProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { saveToStorage } from '@/stores/workflows/persistence'
|
||||
import { OAuthRequiredModal } from '../../credential-selector/components/oauth-required-modal'
|
||||
|
||||
const logger = createLogger('FileSelector')
|
||||
const logger = createLogger('GoogleDrivePicker')
|
||||
|
||||
export interface FileInfo {
|
||||
id: string
|
||||
@@ -41,7 +41,7 @@ export interface FileInfo {
|
||||
owners?: { displayName: string; emailAddress: string }[]
|
||||
}
|
||||
|
||||
interface FileSelectorProps {
|
||||
interface GoogleDrivePickerProps {
|
||||
value: string
|
||||
onChange: (value: string, fileInfo?: FileInfo) => 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<Credential[]>([])
|
||||
const [files, setFiles] = useState<FileInfo[]>([])
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('')
|
||||
const [selectedFileId, setSelectedFileId] = useState(value)
|
||||
const [selectedFile, setSelectedFile] = useState<FileInfo | null>(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<string | null> => {
|
||||
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({
|
||||
)}
|
||||
|
||||
<Command>
|
||||
<CommandInput placeholder="Search files..." onValueChange={handleSearch} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span className="ml-2">Loading files...</span>
|
||||
<span className="ml-2">Loading...</span>
|
||||
</div>
|
||||
) : credentials.length === 0 ? (
|
||||
<div className="p-4 text-center">
|
||||
@@ -441,9 +440,9 @@ export function FileSelector({
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 text-center">
|
||||
<p className="text-sm font-medium">No files found.</p>
|
||||
<p className="text-sm font-medium">Ready to select files.</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Try a different search or account.
|
||||
Click the button below to open the file picker.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -471,25 +470,20 @@ export function FileSelector({
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{/* Files list */}
|
||||
{files.length > 0 && (
|
||||
{/* Open picker button - only show if we have credentials */}
|
||||
{credentials.length > 0 && selectedCredentialId && (
|
||||
<CommandGroup>
|
||||
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
Files
|
||||
</div>
|
||||
{files.map((file) => (
|
||||
<CommandItem
|
||||
key={file.id}
|
||||
value={`file-${file.id}-${file.name}`}
|
||||
onSelect={() => handleSelectFile(file)}
|
||||
<div className="p-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
handleOpenPicker()
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{getFileIcon(file, 'sm')}
|
||||
<span className="font-normal truncate">{file.name}</span>
|
||||
</div>
|
||||
{file.id === selectedFileId && <Check className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
))}
|
||||
Open Google Drive Picker
|
||||
</Button>
|
||||
</div>
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
@@ -504,17 +498,6 @@ export function FileSelector({
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{/* Add another account option
|
||||
{credentials.length > 0 && (
|
||||
<CommandGroup>
|
||||
<CommandItem onSelect={handleAddCredential}>
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<span>Connect Another Account</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)} */}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
+7
-2
@@ -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 (
|
||||
<FileSelector
|
||||
<GoogleDrivePicker
|
||||
value={selectedFileId}
|
||||
onChange={handleFileChange}
|
||||
provider={subBlock.provider || 'google-drive'}
|
||||
@@ -43,6 +46,8 @@ export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileS
|
||||
mimeTypeFilter={subBlock.mimeType}
|
||||
showPreview={true}
|
||||
onFileInfoChange={setFileInfo}
|
||||
clientId={clientId}
|
||||
apiKey={apiKey}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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')
|
||||
|
||||
+2
-4
@@ -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
|
||||
+2
-2
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
case 'webhook-config':
|
||||
return (
|
||||
<WebhookConfig blockId={blockId} subBlockId={config.id} isConnecting={isConnecting} />
|
||||
)
|
||||
case 'slider':
|
||||
return (
|
||||
<SliderInput
|
||||
@@ -128,6 +124,10 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
|
||||
return (
|
||||
<TimeInput blockId={blockId} subBlockId={config.id} placeholder={config.placeholder} />
|
||||
)
|
||||
case 'webhook-config':
|
||||
return (
|
||||
<WebhookConfig blockId={blockId} subBlockId={config.id} isConnecting={isConnecting} />
|
||||
)
|
||||
case 'oauth-input':
|
||||
return (
|
||||
<CredentialSelector
|
||||
|
||||
+12
-2
@@ -45,8 +45,8 @@ const nextConfig: NextConfig = {
|
||||
],
|
||||
},
|
||||
{
|
||||
// Cross-Origin Isolation headers (existing)
|
||||
source: '/:path*',
|
||||
// Apply Cross-Origin Isolation headers to all routes except those that use the Google Drive Picker
|
||||
source: '/((?!w/.*|api/auth/oauth/drive).*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cross-Origin-Embedder-Policy',
|
||||
@@ -58,6 +58,16 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// For routes that use the Google Drive Picker, only apply COOP but not COEP
|
||||
source: '/(w/.*|api/auth/oauth/drive)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cross-Origin-Opener-Policy',
|
||||
value: 'same-origin',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
}),
|
||||
|
||||
Generated
+11
@@ -53,6 +53,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",
|
||||
@@ -8728,6 +8729,16 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-google-drive-picker": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-google-drive-picker/-/react-google-drive-picker-1.2.2.tgz",
|
||||
"integrity": "sha512-x30mYkt9MIwPCgL+fyK75HZ8E6G5L/WGW0bfMG6kbD4NG2kmdlmV9oH5lPa6P6d46y9hj5Y3btAMrZd4JRRkSA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.54.2",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user