fix(onedrive): canonical param required validation

This commit is contained in:
Vikhyath Mondreti
2026-02-04 20:59:28 -08:00
parent 36ec68d93e
commit e256261d53
5 changed files with 401 additions and 136 deletions

View File

@@ -34,6 +34,103 @@ interface UploadedFile {
type: string
}
interface SingleFileSelectorProps {
file: UploadedFile
options: Array<{ label: string; value: string; disabled?: boolean }>
selectedValue: string
inputValue: string
onInputChange: (value: string) => void
onClear: (e: React.MouseEvent) => void
onOpenChange: (open: boolean) => void
disabled: boolean
isLoading: boolean
formatFileSize: (bytes: number) => string
truncateMiddle: (text: string, start?: number, end?: number) => string
isDeleting: boolean
}
/**
* Single file selector component that shows the selected file with both
* a clear button (X) and a chevron to change the selection.
* Follows the same pattern as SelectorCombobox for consistency.
*/
function SingleFileSelector({
file,
options,
selectedValue,
inputValue,
onInputChange,
onClear,
onOpenChange,
disabled,
isLoading,
formatFileSize,
truncateMiddle,
isDeleting,
}: SingleFileSelectorProps) {
const displayLabel = `${truncateMiddle(file.name, 20, 12)} (${formatFileSize(file.size)})`
const [localInputValue, setLocalInputValue] = useState(displayLabel)
const [isEditing, setIsEditing] = useState(false)
// Sync display label when file changes
useEffect(() => {
if (!isEditing) {
setLocalInputValue(displayLabel)
}
}, [displayLabel, isEditing])
return (
<div className='relative w-full'>
<Combobox
options={options}
value={localInputValue}
selectedValue={selectedValue}
onChange={(newValue) => {
// Check if user selected an option
const matched = options.find((opt) => opt.value === newValue || opt.label === newValue)
if (matched) {
setIsEditing(false)
setLocalInputValue(displayLabel)
onInputChange(matched.value)
return
}
// User is typing to search
setIsEditing(true)
setLocalInputValue(newValue)
}}
onOpenChange={(open) => {
if (!open) {
setIsEditing(false)
setLocalInputValue(displayLabel)
}
onOpenChange(open)
}}
placeholder={isLoading ? 'Loading files...' : 'Select or upload file'}
disabled={disabled || isDeleting}
editable={true}
filterOptions={isEditing}
isLoading={isLoading}
inputProps={{
className: 'pr-[60px]',
}}
/>
<Button
type='button'
variant='ghost'
className='-translate-y-1/2 absolute top-1/2 right-[28px] z-10 h-6 w-6 p-0'
onClick={onClear}
disabled={isDeleting}
>
{isDeleting ? (
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<X className='h-4 w-4 opacity-50 hover:opacity-100' />
)}
</Button>
</div>
)
}
interface UploadingFile {
id: string
name: string
@@ -500,6 +597,7 @@ export function FileUpload({
const hasFiles = filesArray.length > 0
const isUploading = uploadingFiles.length > 0
// Options for multiple file mode (filters out already selected files)
const comboboxOptions = useMemo(
() => [
{ label: 'Upload New File', value: '__upload_new__' },
@@ -516,10 +614,43 @@ export function FileUpload({
[availableWorkspaceFiles, acceptedTypes]
)
// Options for single file mode (includes all files, selected one will be highlighted)
const singleFileOptions = useMemo(
() => [
{ label: 'Upload New File', value: '__upload_new__' },
...workspaceFiles.map((file) => {
const isAccepted =
!acceptedTypes || acceptedTypes === '*' || isFileTypeAccepted(file.type, acceptedTypes)
return {
label: file.name,
value: file.id,
disabled: !isAccepted,
}
}),
],
[workspaceFiles, acceptedTypes]
)
// Find the selected file's workspace ID for highlighting in single file mode
const selectedFileId = useMemo(() => {
if (!hasFiles || multiple) return ''
const currentFile = filesArray[0]
if (!currentFile) return ''
// Match by key or path
const matchedWorkspaceFile = workspaceFiles.find(
(wf) =>
wf.key === currentFile.key ||
wf.name === currentFile.name ||
currentFile.path?.includes(wf.key)
)
return matchedWorkspaceFile?.id || ''
}, [filesArray, workspaceFiles, hasFiles, multiple])
const handleComboboxChange = (value: string) => {
setInputValue(value)
const selectedFile = availableWorkspaceFiles.find((file) => file.id === value)
// Look in full workspaceFiles list (not filtered) to allow re-selecting same file in single mode
const selectedFile = workspaceFiles.find((file) => file.id === value)
const isAcceptedType =
selectedFile &&
(!acceptedTypes ||
@@ -559,16 +690,17 @@ export function FileUpload({
{/* Error message */}
{uploadError && <div className='mb-2 text-red-600 text-sm'>{uploadError}</div>}
{/* File list with consistent spacing */}
{(hasFiles || isUploading) && (
{/* File list with consistent spacing - only show for multiple mode or when uploading */}
{((hasFiles && multiple) || isUploading) && (
<div className={cn('space-y-2', multiple && 'mb-2')}>
{/* Only show files that aren't currently uploading */}
{filesArray.map((file) => {
const isCurrentlyUploading = uploadingFiles.some(
(uploadingFile) => uploadingFile.name === file.name
)
return !isCurrentlyUploading && renderFileItem(file)
})}
{/* Only show files that aren't currently uploading (for multiple mode only) */}
{multiple &&
filesArray.map((file) => {
const isCurrentlyUploading = uploadingFiles.some(
(uploadingFile) => uploadingFile.name === file.name
)
return !isCurrentlyUploading && renderFileItem(file)
})}
{isUploading && (
<>
{uploadingFiles.map(renderUploadingItem)}
@@ -604,6 +736,26 @@ export function FileUpload({
/>
)}
{/* Single file mode with file selected: show combobox-style UI with X and chevron */}
{hasFiles && !multiple && !isUploading && (
<SingleFileSelector
file={filesArray[0]}
options={singleFileOptions}
selectedValue={selectedFileId}
inputValue={inputValue}
onInputChange={handleComboboxChange}
onClear={(e) => handleRemoveFile(filesArray[0], e)}
onOpenChange={(open) => {
if (open) void loadWorkspaceFiles()
}}
disabled={disabled}
isLoading={loadingWorkspaceFiles}
formatFileSize={formatFileSize}
truncateMiddle={truncateMiddle}
isDeleting={deletingFiles[filesArray[0]?.path || '']}
/>
)}
{/* Show dropdown selector if no files and not uploading */}
{!hasFiles && !isUploading && (
<Combobox

View File

@@ -1,6 +1,7 @@
import type React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Combobox as EditableCombobox } from '@/components/emcn/components'
import { X } from 'lucide-react'
import { Button, Combobox as EditableCombobox } from '@/components/emcn/components'
import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'
@@ -108,6 +109,20 @@ export function SelectorCombobox({
[setStoreValue, onOptionChange, readOnly, disabled]
)
const handleClear = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (readOnly || disabled) return
setStoreValue(null)
setInputValue('')
onOptionChange?.('')
},
[setStoreValue, onOptionChange, readOnly, disabled]
)
const showClearButton = Boolean(activeValue) && !disabled && !readOnly
return (
<div className='w-full'>
<SubBlockInputController
@@ -119,36 +134,49 @@ export function SelectorCombobox({
isPreview={isPreview}
>
{({ ref, onDrop, onDragOver }) => (
<EditableCombobox
options={comboboxOptions}
value={allowSearch ? inputValue : selectedLabel}
selectedValue={activeValue ?? ''}
onChange={(newValue) => {
const matched = optionMap.get(newValue)
if (matched) {
setInputValue(matched.label)
setIsEditing(false)
handleSelection(matched.id)
return
}
if (allowSearch) {
setInputValue(newValue)
setIsEditing(true)
setSearchTerm(newValue)
}
}}
placeholder={placeholder || subBlock.placeholder || 'Select an option'}
disabled={disabled || readOnly}
editable={allowSearch}
filterOptions={allowSearch}
inputRef={ref as React.RefObject<HTMLInputElement>}
inputProps={{
onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void,
onDragOver: onDragOver as (e: React.DragEvent<HTMLInputElement>) => void,
}}
isLoading={isLoading}
error={error instanceof Error ? error.message : null}
/>
<div className='relative w-full'>
<EditableCombobox
options={comboboxOptions}
value={allowSearch ? inputValue : selectedLabel}
selectedValue={activeValue ?? ''}
onChange={(newValue) => {
const matched = optionMap.get(newValue)
if (matched) {
setInputValue(matched.label)
setIsEditing(false)
handleSelection(matched.id)
return
}
if (allowSearch) {
setInputValue(newValue)
setIsEditing(true)
setSearchTerm(newValue)
}
}}
placeholder={placeholder || subBlock.placeholder || 'Select an option'}
disabled={disabled || readOnly}
editable={allowSearch}
filterOptions={allowSearch}
inputRef={ref as React.RefObject<HTMLInputElement>}
inputProps={{
onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void,
onDragOver: onDragOver as (e: React.DragEvent<HTMLInputElement>) => void,
className: showClearButton ? 'pr-[60px]' : undefined,
}}
isLoading={isLoading}
error={error instanceof Error ? error.message : null}
/>
{showClearButton && (
<Button
type='button'
variant='ghost'
className='-translate-y-1/2 absolute top-1/2 right-[28px] z-10 h-6 w-6 p-0'
onClick={handleClear}
>
<X className='h-4 w-4 opacity-50 hover:opacity-100' />
</Button>
)}
</div>
)}
</SubBlockInputController>
</div>

View File

@@ -121,10 +121,10 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
required: false,
},
{
id: 'folderSelector',
id: 'uploadFolderSelector',
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
canonicalParamId: 'uploadFolderId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -137,10 +137,10 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
condition: { field: 'operation', value: ['create_file', 'upload'] },
},
{
id: 'manualFolderId',
id: 'uploadManualFolderId',
title: 'Parent Folder ID',
type: 'short-input',
canonicalParamId: 'folderId',
canonicalParamId: 'uploadFolderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
mode: 'advanced',
condition: { field: 'operation', value: ['create_file', 'upload'] },
@@ -193,10 +193,10 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
required: true,
},
{
id: 'folderSelector',
id: 'createFolderParentSelector',
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
canonicalParamId: 'createFolderParentId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -210,20 +210,20 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
},
// Manual Folder ID input (advanced mode)
{
id: 'manualFolderId',
id: 'createFolderManualParentId',
title: 'Parent Folder ID',
type: 'short-input',
canonicalParamId: 'folderId',
canonicalParamId: 'createFolderParentId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
mode: 'advanced',
condition: { field: 'operation', value: 'create_folder' },
},
// List Fields - Folder Selector (basic mode)
{
id: 'folderSelector',
id: 'listFolderSelector',
title: 'Select Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
canonicalParamId: 'listFolderId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -237,10 +237,10 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
},
// Manual Folder ID input (advanced mode)
{
id: 'manualFolderId',
id: 'listManualFolderId',
title: 'Folder ID',
type: 'short-input',
canonicalParamId: 'folderId',
canonicalParamId: 'listFolderId',
placeholder: 'Enter folder ID (leave empty for root folder)',
mode: 'advanced',
condition: { field: 'operation', value: 'list' },
@@ -279,10 +279,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
},
// Download File Fields - File Selector (basic mode)
{
id: 'fileSelector',
id: 'downloadFileSelector',
title: 'Select File',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'downloadFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -292,13 +292,14 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'download' },
required: true,
},
// Manual File ID input (advanced mode)
{
id: 'manualFileId',
id: 'downloadManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'downloadFileId',
placeholder: 'Enter file ID',
mode: 'advanced',
condition: { field: 'operation', value: 'download' },
@@ -339,10 +340,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
},
// Get File Info Fields
{
id: 'fileSelector',
id: 'getFileSelector',
title: 'Select File',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'getFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -352,12 +353,13 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'get_file' },
required: true,
},
{
id: 'manualFileId',
id: 'getManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'getFileId',
placeholder: 'Enter file ID',
mode: 'advanced',
condition: { field: 'operation', value: 'get_file' },
@@ -365,10 +367,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
},
// Copy File Fields
{
id: 'fileSelector',
id: 'copyFileSelector',
title: 'Select File to Copy',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'copyFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -378,12 +380,13 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'copy' },
required: true,
},
{
id: 'manualFileId',
id: 'copyManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'copyFileId',
placeholder: 'Enter file ID to copy',
mode: 'advanced',
condition: { field: 'operation', value: 'copy' },
@@ -397,10 +400,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
condition: { field: 'operation', value: 'copy' },
},
{
id: 'folderSelector',
id: 'copyDestFolderSelector',
title: 'Destination Folder',
type: 'file-selector',
canonicalParamId: 'destinationFolderId',
canonicalParamId: 'copyDestFolderId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -413,20 +416,20 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
condition: { field: 'operation', value: 'copy' },
},
{
id: 'manualDestinationFolderId',
id: 'copyManualDestFolderId',
title: 'Destination Folder ID',
type: 'short-input',
canonicalParamId: 'destinationFolderId',
canonicalParamId: 'copyDestFolderId',
placeholder: 'Enter destination folder ID (optional)',
mode: 'advanced',
condition: { field: 'operation', value: 'copy' },
},
// Update File Fields
{
id: 'fileSelector',
id: 'updateFileSelector',
title: 'Select File to Update',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'updateFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -436,12 +439,13 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'update' },
required: true,
},
{
id: 'manualFileId',
id: 'updateManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'updateFileId',
placeholder: 'Enter file ID to update',
mode: 'advanced',
condition: { field: 'operation', value: 'update' },
@@ -500,10 +504,10 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
},
// Trash File Fields
{
id: 'fileSelector',
id: 'trashFileSelector',
title: 'Select File to Trash',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'trashFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -513,12 +517,13 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'trash' },
required: true,
},
{
id: 'manualFileId',
id: 'trashManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'trashFileId',
placeholder: 'Enter file ID to trash',
mode: 'advanced',
condition: { field: 'operation', value: 'trash' },
@@ -526,10 +531,10 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
},
// Delete File Fields
{
id: 'fileSelector',
id: 'deleteFileSelector',
title: 'Select File to Delete',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'deleteFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -539,12 +544,13 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'delete' },
required: true,
},
{
id: 'manualFileId',
id: 'deleteManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'deleteFileId',
placeholder: 'Enter file ID to permanently delete',
mode: 'advanced',
condition: { field: 'operation', value: 'delete' },
@@ -552,10 +558,10 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
},
// Share File Fields
{
id: 'fileSelector',
id: 'shareFileSelector',
title: 'Select File to Share',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'shareFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -565,12 +571,13 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'share' },
required: true,
},
{
id: 'manualFileId',
id: 'shareManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'shareFileId',
placeholder: 'Enter file ID to share',
mode: 'advanced',
condition: { field: 'operation', value: 'share' },
@@ -665,10 +672,10 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
},
// Unshare (Remove Permission) Fields
{
id: 'fileSelector',
id: 'unshareFileSelector',
title: 'Select File',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'unshareFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -678,12 +685,13 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'unshare' },
required: true,
},
{
id: 'manualFileId',
id: 'unshareManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'unshareFileId',
placeholder: 'Enter file ID',
mode: 'advanced',
condition: { field: 'operation', value: 'unshare' },
@@ -699,10 +707,10 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
},
// List Permissions Fields
{
id: 'fileSelector',
id: 'listPermissionsFileSelector',
title: 'Select File',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'listPermissionsFileId',
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
@@ -712,12 +720,13 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'list_permissions' },
required: true,
},
{
id: 'manualFileId',
id: 'listPermissionsManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'listPermissionsFileId',
placeholder: 'Enter file ID',
mode: 'advanced',
condition: { field: 'operation', value: 'list_permissions' },
@@ -778,11 +787,22 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
params: (params) => {
const {
credential,
folderSelector,
manualFolderId,
manualDestinationFolderId,
fileSelector,
manualFileId,
// Folder canonical params (per-operation)
uploadFolderId,
createFolderParentId,
listFolderId,
copyDestFolderId,
// File canonical params (per-operation)
downloadFileId,
getFileId,
copyFileId,
updateFileId,
trashFileId,
deleteFileId,
shareFileId,
unshareFileId,
listPermissionsFileId,
// File upload
file,
fileUpload,
mimeType,
@@ -795,17 +815,56 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
// Normalize file input - handles both basic (file-upload) and advanced (short-input) modes
const normalizedFile = normalizeFileInput(file ?? fileUpload, { single: true })
// Use folderSelector if provided, otherwise use manualFolderId
const effectiveFolderId = (folderSelector || manualFolderId || '').trim()
// Resolve folderId based on operation
let effectiveFolderId: string | undefined
switch (params.operation) {
case 'create_file':
case 'upload':
effectiveFolderId = uploadFolderId?.trim() || undefined
break
case 'create_folder':
effectiveFolderId = createFolderParentId?.trim() || undefined
break
case 'list':
effectiveFolderId = listFolderId?.trim() || undefined
break
}
// Use fileSelector if provided, otherwise use manualFileId
const effectiveFileId = (fileSelector || manualFileId || '').trim()
// Resolve fileId based on operation
let effectiveFileId: string | undefined
switch (params.operation) {
case 'download':
effectiveFileId = downloadFileId?.trim() || undefined
break
case 'get_file':
effectiveFileId = getFileId?.trim() || undefined
break
case 'copy':
effectiveFileId = copyFileId?.trim() || undefined
break
case 'update':
effectiveFileId = updateFileId?.trim() || undefined
break
case 'trash':
effectiveFileId = trashFileId?.trim() || undefined
break
case 'delete':
effectiveFileId = deleteFileId?.trim() || undefined
break
case 'share':
effectiveFileId = shareFileId?.trim() || undefined
break
case 'unshare':
effectiveFileId = unshareFileId?.trim() || undefined
break
case 'list_permissions':
effectiveFileId = listPermissionsFileId?.trim() || undefined
break
}
// Use folderSelector for destination or manualDestinationFolderId for copy operation
// Resolve destinationFolderId for copy operation
const effectiveDestinationFolderId =
params.operation === 'copy'
? (folderSelector || manualDestinationFolderId || '').trim()
: undefined
params.operation === 'copy' ? copyDestFolderId?.trim() || undefined : undefined
// Convert starred dropdown to boolean
const starredValue = starred === 'true' ? true : starred === 'false' ? false : undefined
@@ -816,9 +875,9 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
return {
credential,
folderId: effectiveFolderId || undefined,
fileId: effectiveFileId || undefined,
destinationFolderId: effectiveDestinationFolderId || undefined,
folderId: effectiveFolderId,
fileId: effectiveFileId,
destinationFolderId: effectiveDestinationFolderId,
file: normalizedFile,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
mimeType: mimeType,
@@ -834,13 +893,21 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google Drive access token' },
// File selection inputs
fileSelector: { type: 'string', description: 'Selected file' },
manualFileId: { type: 'string', description: 'Manual file identifier' },
// Folder selection inputs
folderSelector: { type: 'string', description: 'Selected folder' },
manualFolderId: { type: 'string', description: 'Manual folder identifier' },
manualDestinationFolderId: { type: 'string', description: 'Destination folder for copy' },
// Folder canonical params (per-operation)
uploadFolderId: { type: 'string', description: 'Parent folder for upload/create' },
createFolderParentId: { type: 'string', description: 'Parent folder for create folder' },
listFolderId: { type: 'string', description: 'Folder to list files from' },
copyDestFolderId: { type: 'string', description: 'Destination folder for copy' },
// File canonical params (per-operation)
downloadFileId: { type: 'string', description: 'File to download' },
getFileId: { type: 'string', description: 'File to get info for' },
copyFileId: { type: 'string', description: 'File to copy' },
updateFileId: { type: 'string', description: 'File to update' },
trashFileId: { type: 'string', description: 'File to trash' },
deleteFileId: { type: 'string', description: 'File to delete' },
shareFileId: { type: 'string', description: 'File to share' },
unshareFileId: { type: 'string', description: 'File to unshare' },
listPermissionsFileId: { type: 'string', description: 'File to list permissions for' },
// Upload and Create inputs
fileName: { type: 'string', description: 'File or folder name' },
file: { type: 'json', description: 'File to upload (UserFile object)' },

View File

@@ -140,10 +140,10 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
},
{
id: 'folderSelector',
id: 'uploadFolderSelector',
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
canonicalParamId: 'uploadFolderId',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -160,10 +160,10 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
condition: { field: 'operation', value: ['create_file', 'upload'] },
},
{
id: 'manualFolderId',
id: 'uploadManualFolderId',
title: 'Parent Folder ID',
type: 'short-input',
canonicalParamId: 'folderId',
canonicalParamId: 'uploadFolderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
@@ -209,10 +209,10 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
},
// List Fields - Folder Selector (basic mode)
{
id: 'folderSelector',
id: 'listFolderSelector',
title: 'Select Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
canonicalParamId: 'listFolderId',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -230,10 +230,10 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
},
// Manual Folder ID input (advanced mode)
{
id: 'manualFolderId',
id: 'listManualFolderId',
title: 'Folder ID',
type: 'short-input',
canonicalParamId: 'folderId',
canonicalParamId: 'listFolderId',
placeholder: 'Enter folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
@@ -273,6 +273,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'download' },
required: true,
},
// Manual File ID input (advanced mode)
{
@@ -294,10 +295,10 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
},
// Delete File Fields - File Selector (basic mode)
{
id: 'fileSelector',
id: 'deleteFileSelector',
title: 'Select File to Delete',
type: 'file-selector',
canonicalParamId: 'fileId',
canonicalParamId: 'deleteFileId',
serviceId: 'onedrive',
requiredScopes: [
'openid',
@@ -316,10 +317,10 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
},
// Manual File ID input (advanced mode)
{
id: 'manualFileId',
id: 'deleteManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'fileId',
canonicalParamId: 'deleteFileId',
placeholder: 'Enter file or folder ID to delete',
mode: 'advanced',
condition: { field: 'operation', value: 'delete' },
@@ -355,8 +356,10 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
params: (params) => {
const {
credential,
folderId,
fileId,
folderSelector,
manualFolderId,
fileSelector,
manualFileId,
mimeType,
values,
downloadFileName,
@@ -373,13 +376,19 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
// Normalize file input from both basic (file-upload) and advanced (short-input) modes
const normalizedFile = normalizeFileInput(file || fileReference, { single: true })
// Resolve folder ID from selector (basic) or manual input (advanced)
const resolvedFolderId = folderSelector || manualFolderId || undefined
// Resolve file ID from selector (basic) or manual input (advanced)
const resolvedFileId = fileSelector || manualFileId || undefined
return {
credential,
...rest,
values: normalizedValues,
file: normalizedFile,
folderId: folderId || undefined,
fileId: fileId || undefined,
folderId: resolvedFolderId,
fileId: resolvedFileId,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
mimeType: mimeType,
...(downloadFileName && { fileName: downloadFileName }),
@@ -390,16 +399,23 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Microsoft account credential' },
// Upload and Create Folder operation inputs
// Upload and Create operation inputs
fileName: { type: 'string', description: 'File name' },
file: { type: 'json', description: 'File to upload (UserFile object)' },
fileReference: { type: 'json', description: 'File reference from previous block' },
content: { type: 'string', description: 'Text content to upload' },
mimeType: { type: 'string', description: 'MIME type of file to create' },
values: { type: 'json', description: 'Cell values for new Excel as JSON' },
fileId: { type: 'string', description: 'File ID to download' },
// Folder canonical params (per-operation)
uploadFolderId: { type: 'string', description: 'Parent folder for upload/create' },
createFolderParentId: { type: 'string', description: 'Parent folder for create folder' },
listFolderId: { type: 'string', description: 'Folder to list files from' },
// File canonical params (per-operation)
downloadFileId: { type: 'string', description: 'File to download' },
deleteFileId: { type: 'string', description: 'File to delete' },
downloadFileName: { type: 'string', description: 'File name override for download' },
folderId: { type: 'string', description: 'Folder ID' },
folderName: { type: 'string', description: 'Folder name for create_folder' },
// List operation inputs
query: { type: 'string', description: 'Search query' },
pageSize: { type: 'number', description: 'Results per page' },
},

View File

@@ -520,7 +520,9 @@ export class Serializer {
}
// Check if value is missing
const fieldValue = params[subBlockConfig.id]
// For canonical subBlocks, look up the canonical param value (original IDs were deleted)
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlockConfig.id]
const fieldValue = canonicalId ? params[canonicalId] : params[subBlockConfig.id]
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
missingFields.push(subBlockConfig.title || subBlockConfig.id)
}