mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-05 12:14:59 -05:00
fix(onedrive): canonical param required validation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)' },
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user