Skip to content

Commit ea3bab1

Browse files
fix(inputs): canonical params + manual validations + params resolution cleanups (#3141)
* fix(onedrive): canonical param required validation * fix onedrive * cleanup canonical tool param resolution code * fix type * fix jira type checks * remove manual validations
1 parent 552dc56 commit ea3bab1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1401
-990
lines changed

.claude/commands/add-integration.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,15 @@ export const {Service}Block: BlockConfig = {
206206
}
207207
```
208208

209-
**Critical:**
210-
- `canonicalParamId` must NOT match any other subblock's `id`, must be unique per block, and should only be used to link basic/advanced alternatives for the same parameter.
211-
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent.
212-
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
209+
**Critical Canonical Param Rules:**
210+
- `canonicalParamId` must NOT match any subblock's `id` in the block
211+
- `canonicalParamId` must be unique per operation/condition context
212+
- Only use `canonicalParamId` to link basic/advanced alternatives for the same logical parameter
213+
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent
214+
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions
215+
- **Required consistency:** If one subblock in a canonical group has `required: true`, ALL subblocks in that group must have `required: true` (prevents bypassing validation by switching modes)
216+
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs (e.g., `fileSelector`, `manualFileId`)
217+
- **Params function:** Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
213218

214219
## Step 4: Add Icon
215220

.claude/rules/sim-integrations.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,36 @@ dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
157157
- `'both'` - Show in both modes (default)
158158
- `'trigger'` - Only when block is used as trigger
159159

160+
### `canonicalParamId` - Link basic/advanced alternatives
161+
162+
Use to map multiple UI inputs to a single logical parameter:
163+
164+
```typescript
165+
// Basic mode: Visual selector
166+
{
167+
id: 'fileSelector',
168+
type: 'file-selector',
169+
mode: 'basic',
170+
canonicalParamId: 'fileId',
171+
required: true,
172+
},
173+
// Advanced mode: Manual input
174+
{
175+
id: 'manualFileId',
176+
type: 'short-input',
177+
mode: 'advanced',
178+
canonicalParamId: 'fileId',
179+
required: true,
180+
},
181+
```
182+
183+
**Critical Rules:**
184+
- `canonicalParamId` must NOT match any subblock's `id`
185+
- `canonicalParamId` must be unique per operation/condition context
186+
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
187+
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
188+
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
189+
160190
**Register in `blocks/registry.ts`:**
161191

162192
```typescript

.cursor/rules/sim-integrations.mdc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,36 @@ dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
155155
- `'both'` - Show in both modes (default)
156156
- `'trigger'` - Only when block is used as trigger
157157

158+
### `canonicalParamId` - Link basic/advanced alternatives
159+
160+
Use to map multiple UI inputs to a single logical parameter:
161+
162+
```typescript
163+
// Basic mode: Visual selector
164+
{
165+
id: 'fileSelector',
166+
type: 'file-selector',
167+
mode: 'basic',
168+
canonicalParamId: 'fileId',
169+
required: true,
170+
},
171+
// Advanced mode: Manual input
172+
{
173+
id: 'manualFileId',
174+
type: 'short-input',
175+
mode: 'advanced',
176+
canonicalParamId: 'fileId',
177+
required: true,
178+
},
179+
```
180+
181+
**Critical Rules:**
182+
- `canonicalParamId` must NOT match any subblock's `id`
183+
- `canonicalParamId` must be unique per operation/condition context
184+
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
185+
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
186+
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
187+
158188
**Register in `blocks/registry.ts`:**
159189

160190
```typescript

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

0 commit comments

Comments
 (0)