Skip to content

Commit 3c8b19c

Browse files
committed
fix(dropbox): read the nested existing shared link and page list folder and search with cursors
1 parent a02c07f commit 3c8b19c

6 files changed

Lines changed: 143 additions & 18 deletions

File tree

‎apps/sim/blocks/blocks/dropbox.ts‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DropboxIcon } from '@/components/icons'
33
import { getScopesForService } from '@/lib/oauth/utils'
44
import type { BlockConfig, BlockMeta } from '@/blocks/types'
55
import { AuthMode, IntegrationType } from '@/blocks/types'
6-
import { normalizeFileInput } from '@/blocks/utils'
6+
import { normalizeFileInput, toolCallArg } from '@/blocks/utils'
77

88
/*
99
* Canonical basic/advanced pair for the upload source, shared by the card
@@ -190,6 +190,14 @@ export const DropboxBlock = {
190190
placeholder: '500',
191191
condition: { field: 'operation', value: 'dropbox_list_folder' },
192192
},
193+
{
194+
id: 'listFolderCursor',
195+
title: 'Cursor',
196+
type: 'short-input',
197+
placeholder: 'Cursor from a previous call, to fetch the next page',
198+
condition: { field: 'operation', value: 'dropbox_list_folder' },
199+
mode: 'advanced',
200+
},
193201
{
194202
id: 'path',
195203
title: 'Folder Path',
@@ -347,6 +355,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
347355
placeholder: '100',
348356
condition: { field: 'operation', value: 'dropbox_search' },
349357
},
358+
{
359+
id: 'searchCursor',
360+
title: 'Cursor',
361+
type: 'short-input',
362+
placeholder: 'Cursor from a previous call, to fetch the next page',
363+
condition: { field: 'operation', value: 'dropbox_search' },
364+
mode: 'advanced',
365+
},
350366
{
351367
id: 'path',
352368
title: 'Path',
@@ -459,8 +475,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
459475
return 'dropbox_upload'
460476
}
461477
},
462-
params: (params) => {
478+
params: (params, context) => {
463479
const result: Record<string, unknown> = {}
480+
const operation = params.operation ?? context?.operation
481+
// `cursor` is List Shared Links' field; List Folder and Search page with their own
482+
if (operation === 'dropbox_list_folder') {
483+
result.cursor = params.listFolderCursor || toolCallArg(context, params.cursor)
484+
} else if (operation === 'dropbox_search') {
485+
result.cursor = params.searchCursor || toolCallArg(context, params.cursor)
486+
}
464487
if (params.limit) result.limit = Number(params.limit)
465488
if (params.maxResults) result.maxResults = Number(params.maxResults)
466489
const normalizedFile = normalizeFileInput(params.file, { single: true })
@@ -494,6 +517,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
494517
maxResults: { type: 'number', description: 'Maximum search results' },
495518
directOnly: { type: 'boolean', description: 'Only return direct links, not parent folders' },
496519
cursor: { type: 'string', description: 'Fetch the next page of shared links (pagination)' },
520+
listFolderCursor: {
521+
type: 'string',
522+
description: 'Fetch the next page of folder entries (pagination)',
523+
},
524+
searchCursor: { type: 'string', description: 'Fetch the next page of search results' },
497525
beforeRev: { type: 'string', description: 'Fetch revisions before this one (pagination)' },
498526
rev: { type: 'string', description: 'Revision identifier to restore' },
499527
},

‎apps/sim/tools/dropbox/create_shared_link.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ export const dropboxCreateSharedLinkTool: ToolConfig<
9090
if (!response.ok) {
9191
// A link may already exist for this path - Dropbox includes its metadata in the error
9292
// when the requested settings match the existing link, so surface it as a success.
93-
// Per Stone's JSON rules, a union member whose payload is a struct (SharedLinkMetadata)
94-
// flattens that struct's fields alongside ".tag" - there is no nested "metadata" key.
95-
const existingLink = data.error?.shared_link_already_exists
96-
if (existingLink) {
93+
// SharedLinkMetadata has enumerated subtypes (file/folder), so Stone nests it under the
94+
// union tag: error.shared_link_already_exists = { ".tag": "metadata", metadata: {...} }.
95+
const existingLink = data.error?.shared_link_already_exists?.metadata
96+
if (existingLink?.url) {
9797
return {
9898
success: true,
9999
output: {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { dropboxCreateSharedLinkTool } from '@/tools/dropbox/create_shared_link'
6+
import { dropboxListFolderTool } from '@/tools/dropbox/list_folder'
7+
import { dropboxSearchTool } from '@/tools/dropbox/search'
8+
import type { ToolConfig } from '@/tools/types'
9+
10+
function resolveRequest<P>(tool: ToolConfig<P>, params: P) {
11+
const { url, body } = tool.request
12+
return {
13+
url: typeof url === 'function' ? url(params) : url,
14+
body: body?.(params),
15+
}
16+
}
17+
18+
describe('dropbox_create_shared_link', () => {
19+
it('returns the existing link that Dropbox nests under the shared_link_already_exists union', async () => {
20+
const existing = {
21+
'.tag': 'file',
22+
url: 'https://www.dropbox.com/s/abc/report.pdf?dl=0',
23+
name: 'report.pdf',
24+
path_lower: '/report.pdf',
25+
link_permissions: { can_revoke: true, resolved_visibility: { '.tag': 'public' } },
26+
}
27+
const response = Response.json(
28+
{
29+
error_summary: 'shared_link_already_exists/metadata/..',
30+
error: {
31+
'.tag': 'shared_link_already_exists',
32+
shared_link_already_exists: { '.tag': 'metadata', metadata: existing },
33+
},
34+
},
35+
{ status: 409 }
36+
)
37+
38+
const result = await dropboxCreateSharedLinkTool.transformResponse?.(response)
39+
40+
expect(result?.success).toBe(true)
41+
expect(result?.output.sharedLink?.url).toBe(existing.url)
42+
})
43+
})
44+
45+
describe('cursor pagination', () => {
46+
it('continues a folder listing from a cursor', () => {
47+
expect(
48+
resolveRequest(dropboxListFolderTool, { path: '/docs', recursive: true, cursor: ' abc ' })
49+
).toEqual({
50+
url: 'https://api.dropboxapi.com/2/files/list_folder/continue',
51+
body: { cursor: 'abc' },
52+
})
53+
expect(resolveRequest(dropboxListFolderTool, { path: '/docs' }).url).toBe(
54+
'https://api.dropboxapi.com/2/files/list_folder'
55+
)
56+
})
57+
58+
it('continues a search from a cursor', () => {
59+
expect(resolveRequest(dropboxSearchTool, { query: 'invoice', cursor: 'xyz' })).toEqual({
60+
url: 'https://api.dropboxapi.com/2/files/search/continue_v2',
61+
body: { cursor: 'xyz' },
62+
})
63+
expect(resolveRequest(dropboxSearchTool, { query: 'invoice' }).url).toBe(
64+
'https://api.dropboxapi.com/2/files/search_v2'
65+
)
66+
})
67+
})

‎apps/sim/tools/dropbox/list_folder.ts‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,22 @@ export const dropboxListFolderTool: ToolConfig<DropboxListFolderParams, DropboxL
4242
type: 'number',
4343
required: false,
4444
visibility: 'user-or-llm',
45-
description: 'Maximum number of results to return (default: 500)',
45+
description: 'Approximate maximum number of entries to return per page (1-2000)',
46+
},
47+
cursor: {
48+
type: 'string',
49+
required: false,
50+
visibility: 'user-or-llm',
51+
description:
52+
'Cursor from a previous List Folder call to fetch the next page via list_folder/continue (other options are ignored when set)',
4653
},
4754
},
4855

4956
request: {
50-
url: 'https://api.dropboxapi.com/2/files/list_folder',
57+
url: (params) =>
58+
params.cursor?.trim()
59+
? 'https://api.dropboxapi.com/2/files/list_folder/continue'
60+
: 'https://api.dropboxapi.com/2/files/list_folder',
5161
method: 'POST',
5262
headers: (params) => {
5363
if (!params.accessToken) {
@@ -58,13 +68,17 @@ export const dropboxListFolderTool: ToolConfig<DropboxListFolderParams, DropboxL
5868
'Content-Type': 'application/json',
5969
}
6070
},
61-
body: (params) => ({
62-
path: params.path.trim() === '/' ? '' : params.path.trim(),
63-
recursive: params.recursive ?? false,
64-
include_deleted: params.includeDeleted ?? false,
65-
include_media_info: params.includeMediaInfo ?? false,
66-
limit: params.limit,
67-
}),
71+
body: (params) => {
72+
const cursor = params.cursor?.trim()
73+
if (cursor) return { cursor }
74+
return {
75+
path: params.path.trim() === '/' ? '' : params.path.trim(),
76+
recursive: params.recursive ?? false,
77+
include_deleted: params.includeDeleted ?? false,
78+
include_media_info: params.includeMediaInfo ?? false,
79+
limit: params.limit,
80+
}
81+
},
6882
},
6983

7084
transformResponse: async (response) => {
@@ -105,7 +119,7 @@ export const dropboxListFolderTool: ToolConfig<DropboxListFolderParams, DropboxL
105119
},
106120
cursor: {
107121
type: 'string',
108-
description: 'Cursor for pagination',
122+
description: 'Cursor to pass back as the cursor input to fetch the next page',
109123
},
110124
hasMore: {
111125
type: 'boolean',

‎apps/sim/tools/dropbox/search.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,20 @@ export const dropboxSearchTool: ToolConfig<DropboxSearchParams, DropboxSearchRes
3737
visibility: 'user-or-llm',
3838
description: 'Maximum number of results to return (default: 100)',
3939
},
40+
cursor: {
41+
type: 'string',
42+
required: false,
43+
visibility: 'user-or-llm',
44+
description:
45+
'Cursor from a previous Search call to fetch the next page via search/continue_v2 (other options are ignored when set)',
46+
},
4047
},
4148

4249
request: {
43-
url: 'https://api.dropboxapi.com/2/files/search_v2',
50+
url: (params) =>
51+
params.cursor?.trim()
52+
? 'https://api.dropboxapi.com/2/files/search/continue_v2'
53+
: 'https://api.dropboxapi.com/2/files/search_v2',
4454
method: 'POST',
4555
headers: (params) => {
4656
if (!params.accessToken) {
@@ -52,6 +62,9 @@ export const dropboxSearchTool: ToolConfig<DropboxSearchParams, DropboxSearchRes
5262
}
5363
},
5464
body: (params) => {
65+
const cursor = params.cursor?.trim()
66+
if (cursor) return { cursor }
67+
5568
const body: Record<string, any> = {
5669
query: params.query,
5770
}
@@ -130,7 +143,8 @@ export const dropboxSearchTool: ToolConfig<DropboxSearchParams, DropboxSearchRes
130143
},
131144
cursor: {
132145
type: 'string',
133-
description: 'Cursor for pagination',
146+
description: 'Cursor to pass back as the cursor input to fetch the next page',
147+
optional: true,
134148
},
135149
},
136150
}

‎apps/sim/tools/dropbox/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export interface DropboxListFolderParams extends DropboxBaseParams {
106106
includeDeleted?: boolean
107107
includeMediaInfo?: boolean
108108
limit?: number
109+
cursor?: string
109110
}
110111

111112
export interface DropboxListFolderResponse extends ToolResponse {
@@ -192,6 +193,7 @@ export interface DropboxSearchParams extends DropboxBaseParams {
192193
path?: string
193194
fileExtensions?: string
194195
maxResults?: number
196+
cursor?: string
195197
}
196198

197199
export interface DropboxSearchResponse extends ToolResponse {

0 commit comments

Comments
 (0)