Skip to content

Commit 6b185be

Browse files
authored
v0.3.32: loop block max increase, url-encoded API calls, subflow logs, new supabase tools
2 parents 1619d63 + 214a035 commit 6b185be

File tree

69 files changed

+1327
-618
lines changed

Some content is hidden

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

69 files changed

+1327
-618
lines changed

apps/docs/content/docs/tools/supabase.mdx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Get a single row from a Supabase table based on filter criteria
142142
| Parameter | Type | Description |
143143
| --------- | ---- | ----------- |
144144
| `message` | string | Operation status message |
145-
| `results` | object | The row data if found, null if not found |
145+
| `results` | array | Array containing the row data if found, empty array if not found |
146146

147147
### `supabase_update`
148148

@@ -185,6 +185,26 @@ Delete rows from a Supabase table based on filter criteria
185185
| `message` | string | Operation status message |
186186
| `results` | array | Array of deleted records |
187187

188+
### `supabase_upsert`
189+
190+
Insert or update data in a Supabase table (upsert operation)
191+
192+
#### Input
193+
194+
| Parameter | Type | Required | Description |
195+
| --------- | ---- | -------- | ----------- |
196+
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
197+
| `table` | string | Yes | The name of the Supabase table to upsert data into |
198+
| `data` | any | Yes | The data to upsert \(insert or update\) |
199+
| `apiKey` | string | Yes | Your Supabase service role secret key |
200+
201+
#### Output
202+
203+
| Parameter | Type | Description |
204+
| --------- | ---- | ----------- |
205+
| `message` | string | Operation status message |
206+
| `results` | array | Array of upserted records |
207+
188208

189209

190210
## Notes

apps/sim/app/api/tools/drive/file/route.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
4545
// Fetch the file from Google Drive API
4646
logger.info(`[${requestId}] Fetching file ${fileId} from Google Drive API`)
4747
const response = await fetch(
48-
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks`,
48+
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks,shortcutDetails&supportsAllDrives=true`,
4949
{
5050
headers: {
5151
Authorization: `Bearer ${accessToken}`,
@@ -77,6 +77,34 @@ export async function GET(request: NextRequest) {
7777
'application/vnd.google-apps.presentation': 'application/pdf', // Google Slides to PDF
7878
}
7979

80+
// Resolve shortcuts transparently for UI stability
81+
if (
82+
file.mimeType === 'application/vnd.google-apps.shortcut' &&
83+
file.shortcutDetails?.targetId
84+
) {
85+
const targetId = file.shortcutDetails.targetId
86+
const shortcutResp = await fetch(
87+
`https://www.googleapis.com/drive/v3/files/${targetId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks&supportsAllDrives=true`,
88+
{
89+
headers: { Authorization: `Bearer ${accessToken}` },
90+
}
91+
)
92+
if (shortcutResp.ok) {
93+
const targetFile = await shortcutResp.json()
94+
file.id = targetFile.id
95+
file.name = targetFile.name
96+
file.mimeType = targetFile.mimeType
97+
file.iconLink = targetFile.iconLink
98+
file.webViewLink = targetFile.webViewLink
99+
file.thumbnailLink = targetFile.thumbnailLink
100+
file.createdTime = targetFile.createdTime
101+
file.modifiedTime = targetFile.modifiedTime
102+
file.size = targetFile.size
103+
file.owners = targetFile.owners
104+
file.exportLinks = targetFile.exportLinks
105+
}
106+
}
107+
80108
// If the file is a Google Docs, Sheets, or Slides file, we need to provide the export link
81109
if (file.mimeType.startsWith('application/vnd.google-apps.')) {
82110
const format = exportFormats[file.mimeType] || 'application/pdf'

apps/sim/app/api/tools/drive/files/route.ts

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { eq } from 'drizzle-orm'
21
import { type NextRequest, NextResponse } from 'next/server'
32
import { getSession } from '@/lib/auth'
3+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
44
import { createLogger } from '@/lib/logs/console/logger'
55
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
6-
import { db } from '@/db'
7-
import { account } from '@/db/schema'
86

97
export const dynamic = 'force-dynamic'
108

@@ -32,64 +30,48 @@ export async function GET(request: NextRequest) {
3230
const credentialId = searchParams.get('credentialId')
3331
const mimeType = searchParams.get('mimeType')
3432
const query = searchParams.get('query') || ''
33+
const folderId = searchParams.get('folderId') || searchParams.get('parentId') || ''
34+
const workflowId = searchParams.get('workflowId') || undefined
3535

3636
if (!credentialId) {
3737
logger.warn(`[${requestId}] Missing credential ID`)
3838
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
3939
}
4040

41-
// Get the credential from the database
42-
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
43-
44-
if (!credentials.length) {
45-
logger.warn(`[${requestId}] Credential not found`, { credentialId })
46-
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
47-
}
48-
49-
const credential = credentials[0]
50-
51-
// Check if the credential belongs to the user
52-
if (credential.userId !== session.user.id) {
53-
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
54-
credentialUserId: credential.userId,
55-
requestUserId: session.user.id,
56-
})
57-
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
41+
// Authorize use of the credential (supports collaborator credentials via workflow)
42+
const authz = await authorizeCredentialUse(request, { credentialId: credentialId!, workflowId })
43+
if (!authz.ok || !authz.credentialOwnerUserId) {
44+
logger.warn(`[${requestId}] Unauthorized credential access attempt`, authz)
45+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
5846
}
5947

6048
// Refresh access token if needed using the utility function
61-
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
49+
const accessToken = await refreshAccessTokenIfNeeded(
50+
credentialId!,
51+
authz.credentialOwnerUserId,
52+
requestId
53+
)
6254

6355
if (!accessToken) {
6456
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
6557
}
6658

67-
// Build the query parameters for Google Drive API
68-
let queryParams = 'trashed=false'
69-
70-
// Add mimeType filter if provided
59+
// Build Drive 'q' expression safely
60+
const qParts: string[] = ['trashed = false']
61+
if (folderId) {
62+
qParts.push(`'${folderId.replace(/'/g, "\\'")}' in parents`)
63+
}
7164
if (mimeType) {
72-
// For Google Drive API, we need to use 'q' parameter for mimeType filtering
73-
// Instead of using the mimeType parameter directly, we'll add it to the query
74-
if (queryParams.includes('q=')) {
75-
queryParams += ` and mimeType='${mimeType}'`
76-
} else {
77-
queryParams += `&q=mimeType='${mimeType}'`
78-
}
65+
qParts.push(`mimeType = '${mimeType.replace(/'/g, "\\'")}'`)
7966
}
80-
81-
// Add search query if provided
8267
if (query) {
83-
if (queryParams.includes('q=')) {
84-
queryParams += ` and name contains '${query}'`
85-
} else {
86-
queryParams += `&q=name contains '${query}'`
87-
}
68+
qParts.push(`name contains '${query.replace(/'/g, "\\'")}'`)
8869
}
70+
const q = encodeURIComponent(qParts.join(' and '))
8971

90-
// Fetch files from Google Drive API
72+
// Fetch files from Google Drive API with shared drives support
9173
const response = await fetch(
92-
`https://www.googleapis.com/drive/v3/files?${queryParams}&fields=files(id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners)`,
74+
`https://www.googleapis.com/drive/v3/files?q=${q}&supportsAllDrives=true&includeItemsFromAllDrives=true&spaces=drive&fields=files(id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,parents)`,
9375
{
9476
headers: {
9577
Authorization: `Bearer ${accessToken}`,

apps/sim/app/api/webhooks/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export async function POST(request: NextRequest) {
329329
logger.info(`[${requestId}] Gmail provider detected. Setting up Gmail webhook configuration.`)
330330
try {
331331
const { configureGmailPolling } = await import('@/lib/webhooks/utils')
332-
// Use workflow owner for OAuth lookups to support collaborator-saved credentials
332+
// Pass workflow owner for backward-compat fallback (utils prefers credentialId if present)
333333
const success = await configureGmailPolling(workflowRecord.userId, savedWebhook, requestId)
334334

335335
if (!success) {
@@ -364,7 +364,7 @@ export async function POST(request: NextRequest) {
364364
)
365365
try {
366366
const { configureOutlookPolling } = await import('@/lib/webhooks/utils')
367-
// Use workflow owner for OAuth lookups to support collaborator-saved credentials
367+
// Pass workflow owner for backward-compat fallback (utils prefers credentialId if present)
368368
const success = await configureOutlookPolling(
369369
workflowRecord.userId,
370370
savedWebhook,

apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,21 @@ function transformBlockData(data: any, blockType: string, isInput: boolean) {
8282
interface CollapsibleInputOutputProps {
8383
span: TraceSpan
8484
spanId: string
85+
depth: number
8586
}
8687

87-
function CollapsibleInputOutput({ span, spanId }: CollapsibleInputOutputProps) {
88+
function CollapsibleInputOutput({ span, spanId, depth }: CollapsibleInputOutputProps) {
8889
const [inputExpanded, setInputExpanded] = useState(false)
8990
const [outputExpanded, setOutputExpanded] = useState(false)
9091

92+
// Calculate the left margin based on depth to match the parent span's indentation
93+
const leftMargin = depth * 16 + 8 + 24 // Base depth indentation + icon width + extra padding
94+
9195
return (
92-
<div className='mt-2 mr-4 mb-4 ml-8 space-y-3 overflow-hidden'>
96+
<div
97+
className='mt-2 mr-4 mb-4 space-y-3 overflow-hidden'
98+
style={{ marginLeft: `${leftMargin}px` }}
99+
>
93100
{/* Input Data - Collapsible */}
94101
{span.input && (
95102
<div>
@@ -162,26 +169,30 @@ function BlockDataDisplay({
162169
if (value === undefined) return <span className='text-muted-foreground italic'>undefined</span>
163170

164171
if (typeof value === 'string') {
165-
return <span className='break-all text-green-700 dark:text-green-400'>"{value}"</span>
172+
return <span className='break-all text-emerald-700 dark:text-emerald-400'>"{value}"</span>
166173
}
167174

168175
if (typeof value === 'number') {
169-
return <span className='text-blue-700 dark:text-blue-400'>{value}</span>
176+
return <span className='font-mono text-blue-700 dark:text-blue-400'>{value}</span>
170177
}
171178

172179
if (typeof value === 'boolean') {
173-
return <span className='text-purple-700 dark:text-purple-400'>{value.toString()}</span>
180+
return (
181+
<span className='font-mono text-amber-700 dark:text-amber-400'>{value.toString()}</span>
182+
)
174183
}
175184

176185
if (Array.isArray(value)) {
177186
if (value.length === 0) return <span className='text-muted-foreground'>[]</span>
178187
return (
179-
<div className='space-y-1'>
188+
<div className='space-y-0.5'>
180189
<span className='text-muted-foreground'>[</span>
181-
<div className='ml-4 space-y-1'>
190+
<div className='ml-2 space-y-0.5'>
182191
{value.map((item, index) => (
183-
<div key={index} className='flex min-w-0 gap-2'>
184-
<span className='flex-shrink-0 text-muted-foreground text-xs'>{index}:</span>
192+
<div key={index} className='flex min-w-0 gap-1.5'>
193+
<span className='flex-shrink-0 font-mono text-slate-600 text-xs dark:text-slate-400'>
194+
{index}:
195+
</span>
185196
<div className='min-w-0 flex-1 overflow-hidden'>{renderValue(item)}</div>
186197
</div>
187198
))}
@@ -196,10 +207,10 @@ function BlockDataDisplay({
196207
if (entries.length === 0) return <span className='text-muted-foreground'>{'{}'}</span>
197208

198209
return (
199-
<div className='space-y-1'>
210+
<div className='space-y-0.5'>
200211
{entries.map(([objKey, objValue]) => (
201-
<div key={objKey} className='flex min-w-0 gap-2'>
202-
<span className='flex-shrink-0 font-medium text-orange-700 dark:text-orange-400'>
212+
<div key={objKey} className='flex min-w-0 gap-1.5'>
213+
<span className='flex-shrink-0 font-medium text-indigo-700 dark:text-indigo-400'>
203214
{objKey}:
204215
</span>
205216
<div className='min-w-0 flex-1 overflow-hidden'>{renderValue(objValue, objKey)}</div>
@@ -227,12 +238,12 @@ function BlockDataDisplay({
227238
{transformedData &&
228239
Object.keys(transformedData).filter((key) => key !== 'error' && key !== 'success')
229240
.length > 0 && (
230-
<div className='space-y-1'>
241+
<div className='space-y-0.5'>
231242
{Object.entries(transformedData)
232243
.filter(([key]) => key !== 'error' && key !== 'success')
233244
.map(([key, value]) => (
234-
<div key={key} className='flex gap-2'>
235-
<span className='font-medium text-orange-700 dark:text-orange-400'>{key}:</span>
245+
<div key={key} className='flex gap-1.5'>
246+
<span className='font-medium text-indigo-700 dark:text-indigo-400'>{key}:</span>
236247
{renderValue(value, key)}
237248
</div>
238249
))}
@@ -592,7 +603,9 @@ function TraceSpanItem({
592603
{expanded && (
593604
<div>
594605
{/* Block Input/Output Data - Collapsible */}
595-
{(span.input || span.output) && <CollapsibleInputOutput span={span} spanId={spanId} />}
606+
{(span.input || span.output) && (
607+
<CollapsibleInputOutput span={span} spanId={spanId} depth={depth} />
608+
)}
596609

597610
{/* Children and tool calls */}
598611
{/* Render child spans */}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
type SlackChannelInfo,
77
SlackChannelSelector,
88
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/components/slack-channel-selector'
9+
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-depends-on-gate'
910
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
1011
import type { SubBlockConfig } from '@/blocks/types'
11-
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1212

1313
interface ChannelSelectorInputProps {
1414
blockId: string
@@ -29,8 +29,6 @@ export function ChannelSelectorInput({
2929
isPreview = false,
3030
previewValue,
3131
}: ChannelSelectorInputProps) {
32-
const { getValue } = useSubBlockStore()
33-
3432
// Use the proper hook to get the current value and setter (same as file-selector)
3533
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
3634
// Reactive upstream fields
@@ -43,6 +41,8 @@ export function ChannelSelectorInput({
4341
// Get provider-specific values
4442
const provider = subBlock.provider || 'slack'
4543
const isSlack = provider === 'slack'
44+
// Central dependsOn gating
45+
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
4646

4747
// Get the credential for the provider - use provided credential or fall back to reactive values
4848
let credential: string
@@ -89,15 +89,10 @@ export function ChannelSelectorInput({
8989
}}
9090
credential={credential}
9191
label={subBlock.placeholder || 'Select Slack channel'}
92-
disabled={disabled || !credential}
92+
disabled={finalDisabled}
9393
/>
9494
</div>
9595
</TooltipTrigger>
96-
{!credential && (
97-
<TooltipContent side='top'>
98-
<p>Please select a Slack account or enter a bot token first</p>
99-
</TooltipContent>
100-
)}
10196
</Tooltip>
10297
</TooltipProvider>
10398
)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
2626
import type { SubBlockConfig } from '@/blocks/types'
2727
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
2828
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
29-
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
3029

3130
const logger = createLogger('CredentialSelector')
3231

@@ -217,17 +216,6 @@ export function CredentialSelector({
217216
setSelectedId(credentialId)
218217
if (!isPreview) {
219218
setStoreValue(credentialId)
220-
// If credential changed, clear other sub-block fields for a clean state
221-
if (previousId && previousId !== credentialId) {
222-
const wfId = (activeWorkflowId as string) || ''
223-
const workflowValues = useSubBlockStore.getState().workflowValues[wfId] || {}
224-
const blockValues = workflowValues[blockId] || {}
225-
Object.keys(blockValues).forEach((key) => {
226-
if (key !== subBlock.id) {
227-
collaborativeSetSubblockValue(blockId, key, '')
228-
}
229-
})
230-
}
231219
}
232220
setOpen(false)
233221
}

0 commit comments

Comments
 (0)