Skip to content

Commit 1ed3d9c

Browse files
committed
feat(file): pair the Move File operand with a workspace file picker
1 parent 389eea4 commit 1ed3d9c

2 files changed

Lines changed: 103 additions & 8 deletions

File tree

apps/sim/blocks/blocks/file-folders.test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ describe('file_v5 folder operations produce contract-valid tool input', () => {
145145
describe('move file', () => {
146146
it('sends the canonical destination the picker produced', () => {
147147
const params = paramsFor('file_move', {
148-
moveFileId: 'wf_123',
148+
moveFileInput: 'wf_123',
149149
moveTargetRef: '/Reports/Q3%20Results',
150150
})
151151

@@ -157,13 +157,66 @@ describe('file_v5 folder operations produce contract-valid tool input', () => {
157157
})
158158

159159
it('omits the destination when no folder is picked', () => {
160-
const params = paramsFor('file_move', { moveFileId: 'wf_123' })
160+
const params = paramsFor('file_move', { moveFileInput: 'wf_123' })
161161

162162
expect(params.folderPath).toBeUndefined()
163163
expect(fileManageMoveBodySchema.safeParse({ operation: 'move', ...params }).success).toBe(
164164
true
165165
)
166166
})
167+
168+
/*
169+
* The file is a basic/advanced pair like every other single-file operand,
170+
* so it can be picked as well as typed. The tool takes only an id, so a
171+
* picked file travels as the id its selection carries.
172+
*/
173+
it('pairs a workspace file picker with the typed id', () => {
174+
const picker = FileV5Block.subBlocks.find((subBlock) => subBlock.id === 'moveFile')
175+
const typed = FileV5Block.subBlocks.find((subBlock) => subBlock.id === 'moveFileId')
176+
177+
expect(picker?.type).toBe('file-upload')
178+
expect(picker?.mode).toBe('basic')
179+
expect(picker?.canonicalParamId).toBe('moveFileInput')
180+
expect(typed?.type).toBe('short-input')
181+
expect(typed?.mode).toBe('advanced')
182+
expect(typed?.canonicalParamId).toBe('moveFileInput')
183+
expect(typed?.condition).toEqual(picker?.condition)
184+
expect(typed?.required).toEqual(picker?.required)
185+
})
186+
187+
it('moves a picked file by the id it carries', () => {
188+
const params = paramsFor('file_move', {
189+
moveFileInput: { id: 'wf_abc', name: 'notes.md', key: 'workspace/ws-1/notes.md' },
190+
})
191+
192+
expect(params.fileId).toBe('wf_abc')
193+
expect(fileManageMoveBodySchema.safeParse({ operation: 'move', ...params }).success).toBe(
194+
true
195+
)
196+
})
197+
198+
it('reads one id from the serialized list a reference can produce', () => {
199+
expect(paramsFor('file_move', { moveFileInput: '["wf_abc"]' }).fileId).toBe('wf_abc')
200+
})
201+
202+
it('refuses more than one file', () => {
203+
expect(() => paramsFor('file_move', { moveFileInput: '["wf_a","wf_b"]' })).toThrow(
204+
/single file/
205+
)
206+
expect(() =>
207+
paramsFor('file_move', { moveFileInput: [{ id: 'wf_a' }, { id: 'wf_b' }] })
208+
).toThrow(/single file/)
209+
})
210+
211+
it('refuses a file object that carries no id, naming the remedy', () => {
212+
expect(() =>
213+
paramsFor('file_move', { moveFileInput: { name: 'notes.md', key: 'k' } })
214+
).toThrow(/workspace file ID/)
215+
})
216+
217+
it('refuses an empty operand', () => {
218+
expect(() => paramsFor('file_move', {})).toThrow(/File is required for move/)
219+
})
167220
})
168221

169222
describe('write takes a folder, append does not', () => {

apps/sim/blocks/blocks/file.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ const APPEND_FILE_FIELD = ['appendFile', 'appendFileName'] as const
9292
const COMPRESS_FILE_FIELD = ['compressFile', 'compressFileId'] as const
9393
const DECOMPRESS_FILE_FIELD = ['decompressFile', 'decompressFileId'] as const
9494
const SHARE_FILE_FIELD = ['shareFile', 'shareFileId'] as const
95+
const MOVE_FILE_FIELD = ['moveFile', 'moveFileId'] as const
9596
/* Text and file are mutually exclusive sources, so the clause names whichever
9697
one the card actually carries. */
9798
const WRITE_CONTENT_FIELD = ['content', 'writeFile', 'writeFileId'] as const
@@ -1129,6 +1130,7 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
11291130
- Use Write to create a new workspace file and Append to add content to an existing one. Write adds a numeric suffix when the name is taken; turn on "Overwrite Existing File" to replace the contents of the file at that exact path (folder and name) instead — a same-named file in another folder is left alone.
11301131
- Use Compress to bundle one or more files into a single .zip archive stored in the workspace. The new archive is returned in the "files" output.
11311132
- Use Decompress to extract a .zip archive back into the workspace; the extracted files are returned in the "files" output, ready to chain into Get Content or downstream blocks.
1133+
- Move File takes one workspace file, picked or given as a canonical file ID such as an earlier block's file output, and the folder to move it into.
11321134
`,
11331135
canvasPresentation: {
11341136
defaultTitle: 'File',
@@ -1178,7 +1180,7 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
11781180
file_delete_folder: [{ text: 'Delete folder', field: FOLDER_PATH_FIELD, core: true }],
11791181
file_restore_folder: [{ text: 'Restore folder', field: 'restoreFolderId', core: true }],
11801182
file_move: [
1181-
{ text: 'Move', field: 'moveFileId', core: true },
1183+
{ text: 'Move', field: MOVE_FILE_FIELD, core: true },
11821184
{ text: 'into', field: MOVE_TARGET_FIELD },
11831185
],
11841186
file_decompress: [{ text: 'Unzip', field: DECOMPRESS_FILE_FIELD, core: true }],
@@ -1873,11 +1875,24 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
18731875
type: 'switch' as SubBlockType,
18741876
condition: { field: 'operation', value: 'file_delete_folder' },
18751877
},
1878+
{
1879+
id: 'moveFile',
1880+
title: 'File',
1881+
type: 'file-upload' as SubBlockType,
1882+
canonicalParamId: 'moveFileInput',
1883+
acceptedTypes: '*',
1884+
placeholder: 'Select a workspace file',
1885+
mode: 'basic',
1886+
condition: { field: 'operation', value: 'file_move' },
1887+
required: { field: 'operation', value: 'file_move' },
1888+
},
18761889
{
18771890
id: 'moveFileId',
18781891
title: 'File ID',
18791892
type: 'short-input' as SubBlockType,
1880-
placeholder: 'Canonical workspace file ID',
1893+
canonicalParamId: 'moveFileInput',
1894+
placeholder: 'Workspace file ID',
1895+
mode: 'advanced',
18811896
condition: { field: 'operation', value: 'file_move' },
18821897
required: { field: 'operation', value: 'file_move' },
18831898
},
@@ -2045,8 +2060,35 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
20452060
}
20462061

20472062
if (operation === 'file_move') {
2063+
const moveInput = params.moveFileInput
2064+
if (!moveInput) {
2065+
throw new Error('File is required for move')
2066+
}
2067+
2068+
/*
2069+
* The tool takes an id and nothing else, so a picked file resolves
2070+
* here by the id every picker selection and in-place upload carries.
2071+
* A file object without one is refused with the remedy rather than
2072+
* forwarded as a shape the contract rejects.
2073+
*/
2074+
const fileIds = parseReadFileIds(moveInput)
2075+
if (Array.isArray(fileIds)) {
2076+
throw new Error('Move File accepts a single file at a time')
2077+
}
2078+
const file = fileIds
2079+
? null
2080+
: (normalizeFileInput(moveInput, {
2081+
single: true,
2082+
errorMessage: 'Move File accepts a single file at a time',
2083+
}) as Record<string, unknown> | undefined)
2084+
const pickedId = typeof file?.id === 'string' ? file.id.trim() : ''
2085+
const fileId = fileIds ?? pickedId
2086+
if (!fileId) {
2087+
throw new Error('Could not determine the file to move; pass its workspace file ID')
2088+
}
2089+
20482090
return {
2049-
fileId: optionalText(params.moveFileId),
2091+
fileId,
20502092
folderPath: optionalText(params.moveTargetRef),
20512093
workspaceId: params._context?.workspaceId,
20522094
}
@@ -2293,9 +2335,9 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
22932335
type: 'string',
22942336
description: 'Folder the file is moved into (move file)',
22952337
},
2296-
moveFileId: {
2297-
type: 'string',
2298-
description: 'Canonical ID of the file to move (move file)',
2338+
moveFileInput: {
2339+
type: 'json',
2340+
description: 'Selected workspace file or canonical file ID to move (move file)',
22992341
},
23002342
restoreFolderId: {
23012343
type: 'string',

0 commit comments

Comments
 (0)