Skip to content

Commit 389eea4

Browse files
feat(file): pair the folder scope picker with a typed path field (#7457)
1 parent 2276031 commit 389eea4

11 files changed

Lines changed: 322 additions & 109 deletions

File tree

apps/docs/content/docs/integrations/file.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ With the File block, you can:
2222
- **Compress and decompress**: Bundle files into a .zip archive or extract an archive into the workspace
2323
- **Manage sharing**: Enable or disable a public share link for a file, with public, password, email, or SSO access modes
2424

25-
In Sim, the File block allows your agents to search, read, and extract text from workspace files, fetch and parse files from URLs, write or append content to files, bundle files into or out of .zip archives, and control public sharing access for a file—all programmatically as steps in a workflow. Folder selection is optional and supports multiple folders; when no folder is selected, search spans the workspace and file pickers are unscoped. Selected folders are expanded when the workflow runs, so newly added files are included automatically. This makes it possible to explore workspace content, move file content into and out of a workflow, package outputs for download or transfer, and expose files to external users through a managed share link.
25+
In Sim, the File block allows your agents to search, read, and extract text from workspace files, fetch and parse files from URLs, write or append content to files, bundle files into or out of .zip archives, and control public sharing access for a file—all programmatically as steps in a workflow. Folder selection is optional and supports multiple folders. Pick them from the workspace, or switch the field to advanced mode and type comma-separated paths, including a value from an earlier block. When no folder is selected, search spans the workspace and file pickers are unscoped. Selected folders are expanded when the workflow runs, so newly added files are included automatically. This makes it possible to explore workspace content, move file content into and out of a workflow, package outputs for download or transfer, and expose files to external users through a managed share link.
2626
{/* MANUAL-CONTENT-END */}
2727

2828

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { isFileInFolderScope } from '@/lib/workspace-files/folder-path-selection
2222
import { findSelectedWorkspaceFile } from '@/lib/workspace-files/selection'
2323
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
2424
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
25+
import { useActiveCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value'
2526
import { useResourceFolders } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-resource-folders'
2627
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
2728
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
@@ -56,7 +57,7 @@ interface FileUploadProps {
5657
* A sibling folder field that narrows what this picker offers, and the switch
5758
* saying whether that scope descends. See `SubBlockConfig.folderScope`.
5859
*/
59-
folderScope?: { fieldId: string; manualFieldId?: string; recursiveFieldId?: string }
60+
folderScope?: { fieldId: string; recursiveFieldId?: string }
6061
/**
6162
* Controlled value. When `onValueChange` is provided the component reads from
6263
* this prop and writes through `onValueChange` instead of the subblock store,
@@ -353,20 +354,19 @@ export function FileUpload({
353354
* a picker with no folder scope; its own value is never a folder path, so the
354355
* scope reads as absent.
355356
*/
356-
const [folderScopeValue] = useSubBlockValue<unknown>(blockId, folderScope?.fieldId ?? subBlockId)
357+
const folderScopeValue = useActiveCanonicalSubBlockValue<unknown>(
358+
blockId,
359+
folderScope?.fieldId ?? subBlockId
360+
)
357361
/*
358-
* Through `readFolderPaths` rather than a string check so current arrays and
359-
* legacy serialized arrays resolve to the same canonical scopes.
362+
* Through `readFolderPaths` rather than a string check so a picked array, a
363+
* legacy serialized array, and a typed comma-separated list all resolve to
364+
* the same canonical scopes.
360365
*/
361-
const [manualFolderScopeValue] = useSubBlockValue<unknown>(
362-
blockId,
363-
folderScope?.manualFieldId ?? folderScope?.fieldId ?? subBlockId
366+
const folderScopePaths = useMemo(
367+
() => (folderScope ? readFolderPaths(folderScopeValue) : []),
368+
[folderScope, folderScopeValue]
364369
)
365-
const folderScopePaths = useMemo(() => {
366-
if (!folderScope) return []
367-
const selectedPaths = readFolderPaths(folderScopeValue)
368-
return selectedPaths.length > 0 ? selectedPaths : readFolderPaths(manualFolderScopeValue)
369-
}, [folderScope, folderScopeValue, manualFolderScopeValue])
370370

371371
const [folderScopeRecursive] = useSubBlockValue<unknown>(
372372
blockId,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,28 @@ import { isEqual } from 'es-toolkit'
33
import { useStoreWithEqualityFn } from 'zustand/traditional'
44
import {
55
buildCanonicalIndexForSurface,
6+
type CanonicalIndex,
7+
type CanonicalModeOverrides,
8+
resolveActiveDependencyValue,
69
resolveDependencyValue,
710
} from '@/lib/workflows/subblocks/visibility'
811
import { getBlock } from '@/blocks/registry'
912
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1013
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1114
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
1215

13-
/**
14-
* Read a sub-block value by either its raw subBlockId or its canonicalParamId.
15-
*
16-
* `useSubBlockValue` only looks up the raw subBlockId. For fields that use
17-
* `canonicalParamId` to unify basic/advanced inputs (e.g. `tableSelector` vs
18-
* `manualTableId` both mapping to `tableId`), this hook resolves to whichever
19-
* member of the canonical group currently holds the value.
20-
*/
21-
export function useCanonicalSubBlockValue<T = unknown>(
16+
type CanonicalResolver = (
17+
key: string,
18+
values: Record<string, unknown>,
19+
canonicalIndex: CanonicalIndex,
20+
overrides?: CanonicalModeOverrides
21+
) => unknown
22+
23+
/** Subscribes to one key of a block's values, read through the given canonical resolver. */
24+
function useResolvedSubBlockValue<T>(
2225
blockId: string,
23-
canonicalOrSubBlockId: string
26+
canonicalOrSubBlockId: string,
27+
resolve: CanonicalResolver
2428
): T | null {
2529
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
2630
const blockState = useWorkflowStore((state) => state.blocks[blockId])
@@ -38,16 +42,55 @@ export function useCanonicalSubBlockValue<T = unknown>(
3842
(state) => {
3943
if (!activeWorkflowId) return null
4044
const blockValues = state.workflowValues[activeWorkflowId]?.[blockId] || {}
41-
const resolved = resolveDependencyValue(
45+
const resolved = resolve(
4246
canonicalOrSubBlockId,
4347
blockValues,
4448
canonicalIndex,
4549
canonicalModeOverrides
4650
)
4751
return (resolved ?? null) as T | null
4852
},
49-
[activeWorkflowId, blockId, canonicalOrSubBlockId, canonicalIndex, canonicalModeOverrides]
53+
[
54+
activeWorkflowId,
55+
blockId,
56+
canonicalOrSubBlockId,
57+
canonicalIndex,
58+
canonicalModeOverrides,
59+
resolve,
60+
]
5061
),
5162
(a, b) => isEqual(a, b)
5263
)
5364
}
65+
66+
/**
67+
* Read a sub-block value by either its raw subBlockId or its canonicalParamId.
68+
*
69+
* `useSubBlockValue` only looks up the raw subBlockId. For fields that use
70+
* `canonicalParamId` to unify basic/advanced inputs (e.g. `tableSelector` vs
71+
* `manualTableId` both mapping to `tableId`), this hook resolves to whichever
72+
* member of the canonical group currently holds the value.
73+
*/
74+
export function useCanonicalSubBlockValue<T = unknown>(
75+
blockId: string,
76+
canonicalOrSubBlockId: string
77+
): T | null {
78+
return useResolvedSubBlockValue<T>(blockId, canonicalOrSubBlockId, resolveDependencyValue)
79+
}
80+
81+
/**
82+
* Like {@link useCanonicalSubBlockValue}, but strict: a pair answers with its
83+
* ACTIVE member only, honoring the user's basic/advanced toggle, so a dormant
84+
* half's stale value never leaks.
85+
*
86+
* This is the reading for a control that narrows itself by a sibling field,
87+
* such as the file picker's folder scope. The serializer publishes only the
88+
* active half, so a picker that fell back to the other one would offer a set
89+
* the operation then ignores.
90+
*/
91+
export function useActiveCanonicalSubBlockValue<T = unknown>(
92+
blockId: string,
93+
canonicalOrSubBlockId: string
94+
): T | null {
95+
return useResolvedSubBlockValue<T>(blockId, canonicalOrSubBlockId, resolveActiveDependencyValue)
96+
}

apps/sim/blocks/blocks.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ describe.concurrent('Blocks Module', () => {
196196
recursiveFieldId: 'folderIncludeSubfolders',
197197
})
198198
expect(block?.subBlocks.find((subBlock) => subBlock.id === 'folderSelection')?.mode).toBe(
199-
'both'
199+
'basic'
200200
)
201201
expect(block?.tools.config?.tool({ operation: 'file_read' })).toBe('file_read')
202202
expect(block?.tools.config?.tool({ operation: 'file_get_content' })).toBe('file_get_content')

0 commit comments

Comments
 (0)