diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx index 39ad880d9..b46467aa3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx @@ -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 ( +
+ { + // 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]', + }} + /> + +
+ ) +} + 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 &&
{uploadError}
} - {/* File list with consistent spacing */} - {(hasFiles || isUploading) && ( + {/* File list with consistent spacing - only show for multiple mode or when uploading */} + {((hasFiles && multiple) || isUploading) && (
- {/* 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 && ( + 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 && ( { + e.preventDefault() + e.stopPropagation() + if (readOnly || disabled) return + setStoreValue(null) + setInputValue('') + onOptionChange?.('') + }, + [setStoreValue, onOptionChange, readOnly, disabled] + ) + + const showClearButton = Boolean(activeValue) && !disabled && !readOnly + return (
{({ ref, onDrop, onDragOver }) => ( - { - 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} - inputProps={{ - onDrop: onDrop as (e: React.DragEvent) => void, - onDragOver: onDragOver as (e: React.DragEvent) => void, - }} - isLoading={isLoading} - error={error instanceof Error ? error.message : null} - /> +
+ { + 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} + inputProps={{ + onDrop: onDrop as (e: React.DragEvent) => void, + onDragOver: onDragOver as (e: React.DragEvent) => void, + className: showClearButton ? 'pr-[60px]' : undefined, + }} + isLoading={isLoading} + error={error instanceof Error ? error.message : null} + /> + {showClearButton && ( + + )} +
)}
diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index d14168d5a..aebca65dc 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -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)' }, diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index e2e3545fb..58c183553 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -140,10 +140,10 @@ export const OneDriveBlock: BlockConfig = { }, { - 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 = { 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 = { }, // 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 = { }, // 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 = { 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 = { }, // 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 = { }, // 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 = { params: (params) => { const { credential, - folderId, - fileId, + folderSelector, + manualFolderId, + fileSelector, + manualFileId, mimeType, values, downloadFileName, @@ -373,13 +376,19 @@ export const OneDriveBlock: BlockConfig = { // 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 = { 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' }, }, diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index e22c654c4..622667d9f 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -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) }