Skip to content

Commit e256261

Browse files
committed
fix(onedrive): canonical param required validation
1 parent 36ec68d commit e256261

File tree

5 files changed

+401
-136
lines changed

5 files changed

+401
-136
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx

Lines changed: 162 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,103 @@ interface UploadedFile {
3434
type: string
3535
}
3636

37+
interface SingleFileSelectorProps {
38+
file: UploadedFile
39+
options: Array<{ label: string; value: string; disabled?: boolean }>
40+
selectedValue: string
41+
inputValue: string
42+
onInputChange: (value: string) => void
43+
onClear: (e: React.MouseEvent) => void
44+
onOpenChange: (open: boolean) => void
45+
disabled: boolean
46+
isLoading: boolean
47+
formatFileSize: (bytes: number) => string
48+
truncateMiddle: (text: string, start?: number, end?: number) => string
49+
isDeleting: boolean
50+
}
51+
52+
/**
53+
* Single file selector component that shows the selected file with both
54+
* a clear button (X) and a chevron to change the selection.
55+
* Follows the same pattern as SelectorCombobox for consistency.
56+
*/
57+
function SingleFileSelector({
58+
file,
59+
options,
60+
selectedValue,
61+
inputValue,
62+
onInputChange,
63+
onClear,
64+
onOpenChange,
65+
disabled,
66+
isLoading,
67+
formatFileSize,
68+
truncateMiddle,
69+
isDeleting,
70+
}: SingleFileSelectorProps) {
71+
const displayLabel = `${truncateMiddle(file.name, 20, 12)} (${formatFileSize(file.size)})`
72+
const [localInputValue, setLocalInputValue] = useState(displayLabel)
73+
const [isEditing, setIsEditing] = useState(false)
74+
75+
// Sync display label when file changes
76+
useEffect(() => {
77+
if (!isEditing) {
78+
setLocalInputValue(displayLabel)
79+
}
80+
}, [displayLabel, isEditing])
81+
82+
return (
83+
<div className='relative w-full'>
84+
<Combobox
85+
options={options}
86+
value={localInputValue}
87+
selectedValue={selectedValue}
88+
onChange={(newValue) => {
89+
// Check if user selected an option
90+
const matched = options.find((opt) => opt.value === newValue || opt.label === newValue)
91+
if (matched) {
92+
setIsEditing(false)
93+
setLocalInputValue(displayLabel)
94+
onInputChange(matched.value)
95+
return
96+
}
97+
// User is typing to search
98+
setIsEditing(true)
99+
setLocalInputValue(newValue)
100+
}}
101+
onOpenChange={(open) => {
102+
if (!open) {
103+
setIsEditing(false)
104+
setLocalInputValue(displayLabel)
105+
}
106+
onOpenChange(open)
107+
}}
108+
placeholder={isLoading ? 'Loading files...' : 'Select or upload file'}
109+
disabled={disabled || isDeleting}
110+
editable={true}
111+
filterOptions={isEditing}
112+
isLoading={isLoading}
113+
inputProps={{
114+
className: 'pr-[60px]',
115+
}}
116+
/>
117+
<Button
118+
type='button'
119+
variant='ghost'
120+
className='-translate-y-1/2 absolute top-1/2 right-[28px] z-10 h-6 w-6 p-0'
121+
onClick={onClear}
122+
disabled={isDeleting}
123+
>
124+
{isDeleting ? (
125+
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
126+
) : (
127+
<X className='h-4 w-4 opacity-50 hover:opacity-100' />
128+
)}
129+
</Button>
130+
</div>
131+
)
132+
}
133+
37134
interface UploadingFile {
38135
id: string
39136
name: string
@@ -500,6 +597,7 @@ export function FileUpload({
500597
const hasFiles = filesArray.length > 0
501598
const isUploading = uploadingFiles.length > 0
502599

600+
// Options for multiple file mode (filters out already selected files)
503601
const comboboxOptions = useMemo(
504602
() => [
505603
{ label: 'Upload New File', value: '__upload_new__' },
@@ -516,10 +614,43 @@ export function FileUpload({
516614
[availableWorkspaceFiles, acceptedTypes]
517615
)
518616

617+
// Options for single file mode (includes all files, selected one will be highlighted)
618+
const singleFileOptions = useMemo(
619+
() => [
620+
{ label: 'Upload New File', value: '__upload_new__' },
621+
...workspaceFiles.map((file) => {
622+
const isAccepted =
623+
!acceptedTypes || acceptedTypes === '*' || isFileTypeAccepted(file.type, acceptedTypes)
624+
return {
625+
label: file.name,
626+
value: file.id,
627+
disabled: !isAccepted,
628+
}
629+
}),
630+
],
631+
[workspaceFiles, acceptedTypes]
632+
)
633+
634+
// Find the selected file's workspace ID for highlighting in single file mode
635+
const selectedFileId = useMemo(() => {
636+
if (!hasFiles || multiple) return ''
637+
const currentFile = filesArray[0]
638+
if (!currentFile) return ''
639+
// Match by key or path
640+
const matchedWorkspaceFile = workspaceFiles.find(
641+
(wf) =>
642+
wf.key === currentFile.key ||
643+
wf.name === currentFile.name ||
644+
currentFile.path?.includes(wf.key)
645+
)
646+
return matchedWorkspaceFile?.id || ''
647+
}, [filesArray, workspaceFiles, hasFiles, multiple])
648+
519649
const handleComboboxChange = (value: string) => {
520650
setInputValue(value)
521651

522-
const selectedFile = availableWorkspaceFiles.find((file) => file.id === value)
652+
// Look in full workspaceFiles list (not filtered) to allow re-selecting same file in single mode
653+
const selectedFile = workspaceFiles.find((file) => file.id === value)
523654
const isAcceptedType =
524655
selectedFile &&
525656
(!acceptedTypes ||
@@ -559,16 +690,17 @@ export function FileUpload({
559690
{/* Error message */}
560691
{uploadError && <div className='mb-2 text-red-600 text-sm'>{uploadError}</div>}
561692

562-
{/* File list with consistent spacing */}
563-
{(hasFiles || isUploading) && (
693+
{/* File list with consistent spacing - only show for multiple mode or when uploading */}
694+
{((hasFiles && multiple) || isUploading) && (
564695
<div className={cn('space-y-2', multiple && 'mb-2')}>
565-
{/* Only show files that aren't currently uploading */}
566-
{filesArray.map((file) => {
567-
const isCurrentlyUploading = uploadingFiles.some(
568-
(uploadingFile) => uploadingFile.name === file.name
569-
)
570-
return !isCurrentlyUploading && renderFileItem(file)
571-
})}
696+
{/* Only show files that aren't currently uploading (for multiple mode only) */}
697+
{multiple &&
698+
filesArray.map((file) => {
699+
const isCurrentlyUploading = uploadingFiles.some(
700+
(uploadingFile) => uploadingFile.name === file.name
701+
)
702+
return !isCurrentlyUploading && renderFileItem(file)
703+
})}
572704
{isUploading && (
573705
<>
574706
{uploadingFiles.map(renderUploadingItem)}
@@ -604,6 +736,26 @@ export function FileUpload({
604736
/>
605737
)}
606738

739+
{/* Single file mode with file selected: show combobox-style UI with X and chevron */}
740+
{hasFiles && !multiple && !isUploading && (
741+
<SingleFileSelector
742+
file={filesArray[0]}
743+
options={singleFileOptions}
744+
selectedValue={selectedFileId}
745+
inputValue={inputValue}
746+
onInputChange={handleComboboxChange}
747+
onClear={(e) => handleRemoveFile(filesArray[0], e)}
748+
onOpenChange={(open) => {
749+
if (open) void loadWorkspaceFiles()
750+
}}
751+
disabled={disabled}
752+
isLoading={loadingWorkspaceFiles}
753+
formatFileSize={formatFileSize}
754+
truncateMiddle={truncateMiddle}
755+
isDeleting={deletingFiles[filesArray[0]?.path || '']}
756+
/>
757+
)}
758+
607759
{/* Show dropdown selector if no files and not uploading */}
608760
{!hasFiles && !isUploading && (
609761
<Combobox

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox.tsx

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type React from 'react'
22
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3-
import { Combobox as EditableCombobox } from '@/components/emcn/components'
3+
import { X } from 'lucide-react'
4+
import { Button, Combobox as EditableCombobox } from '@/components/emcn/components'
45
import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller'
56
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
67
import type { SubBlockConfig } from '@/blocks/types'
@@ -108,6 +109,20 @@ export function SelectorCombobox({
108109
[setStoreValue, onOptionChange, readOnly, disabled]
109110
)
110111

112+
const handleClear = useCallback(
113+
(e: React.MouseEvent) => {
114+
e.preventDefault()
115+
e.stopPropagation()
116+
if (readOnly || disabled) return
117+
setStoreValue(null)
118+
setInputValue('')
119+
onOptionChange?.('')
120+
},
121+
[setStoreValue, onOptionChange, readOnly, disabled]
122+
)
123+
124+
const showClearButton = Boolean(activeValue) && !disabled && !readOnly
125+
111126
return (
112127
<div className='w-full'>
113128
<SubBlockInputController
@@ -119,36 +134,49 @@ export function SelectorCombobox({
119134
isPreview={isPreview}
120135
>
121136
{({ ref, onDrop, onDragOver }) => (
122-
<EditableCombobox
123-
options={comboboxOptions}
124-
value={allowSearch ? inputValue : selectedLabel}
125-
selectedValue={activeValue ?? ''}
126-
onChange={(newValue) => {
127-
const matched = optionMap.get(newValue)
128-
if (matched) {
129-
setInputValue(matched.label)
130-
setIsEditing(false)
131-
handleSelection(matched.id)
132-
return
133-
}
134-
if (allowSearch) {
135-
setInputValue(newValue)
136-
setIsEditing(true)
137-
setSearchTerm(newValue)
138-
}
139-
}}
140-
placeholder={placeholder || subBlock.placeholder || 'Select an option'}
141-
disabled={disabled || readOnly}
142-
editable={allowSearch}
143-
filterOptions={allowSearch}
144-
inputRef={ref as React.RefObject<HTMLInputElement>}
145-
inputProps={{
146-
onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void,
147-
onDragOver: onDragOver as (e: React.DragEvent<HTMLInputElement>) => void,
148-
}}
149-
isLoading={isLoading}
150-
error={error instanceof Error ? error.message : null}
151-
/>
137+
<div className='relative w-full'>
138+
<EditableCombobox
139+
options={comboboxOptions}
140+
value={allowSearch ? inputValue : selectedLabel}
141+
selectedValue={activeValue ?? ''}
142+
onChange={(newValue) => {
143+
const matched = optionMap.get(newValue)
144+
if (matched) {
145+
setInputValue(matched.label)
146+
setIsEditing(false)
147+
handleSelection(matched.id)
148+
return
149+
}
150+
if (allowSearch) {
151+
setInputValue(newValue)
152+
setIsEditing(true)
153+
setSearchTerm(newValue)
154+
}
155+
}}
156+
placeholder={placeholder || subBlock.placeholder || 'Select an option'}
157+
disabled={disabled || readOnly}
158+
editable={allowSearch}
159+
filterOptions={allowSearch}
160+
inputRef={ref as React.RefObject<HTMLInputElement>}
161+
inputProps={{
162+
onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void,
163+
onDragOver: onDragOver as (e: React.DragEvent<HTMLInputElement>) => void,
164+
className: showClearButton ? 'pr-[60px]' : undefined,
165+
}}
166+
isLoading={isLoading}
167+
error={error instanceof Error ? error.message : null}
168+
/>
169+
{showClearButton && (
170+
<Button
171+
type='button'
172+
variant='ghost'
173+
className='-translate-y-1/2 absolute top-1/2 right-[28px] z-10 h-6 w-6 p-0'
174+
onClick={handleClear}
175+
>
176+
<X className='h-4 w-4 opacity-50 hover:opacity-100' />
177+
</Button>
178+
)}
179+
</div>
152180
)}
153181
</SubBlockInputController>
154182
</div>

0 commit comments

Comments
 (0)