Skip to content

Commit f62568e

Browse files
authored
fix(variables, webhook): fix variable tag dropdown for escaped < and allow empty webhook payload (#1851)
* Fix << tags * Allow empty webhook body * Variable highlighting in loop conditions
1 parent a73e2aa commit f62568e

File tree

10 files changed

+291
-38
lines changed

10 files changed

+291
-38
lines changed

apps/sim/app/api/auth/oauth/credentials/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { checkHybridAuth } from '@/lib/auth/hybrid'
88
import { createLogger } from '@/lib/logs/console/logger'
9-
import type { OAuthService } from '@/lib/oauth/oauth'
109
import { evaluateScopeCoverage, parseProvider } from '@/lib/oauth/oauth'
1110
import { getUserEntityPermissions } from '@/lib/permissions/utils'
1211
import { generateRequestId } from '@/lib/utils'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/components/iteration-badges/iteration-badges.tsx

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
1212
import 'prismjs/components/prism-javascript'
1313
import 'prismjs/themes/prism.css'
1414

15+
import {
16+
isLikelyReferenceSegment,
17+
SYSTEM_REFERENCE_PREFIXES,
18+
splitReferenceSegment,
19+
} from '@/lib/workflows/references'
1520
import type { LoopType, ParallelType } from '@/lib/workflows/types'
21+
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
22+
import { normalizeBlockName } from '@/stores/workflows/utils'
1623

1724
type IterationType = 'loop' | 'parallel'
1825

@@ -130,6 +137,88 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
130137
collaborativeUpdateIterationCount,
131138
collaborativeUpdateIterationCollection,
132139
} = useCollaborativeWorkflow()
140+
const accessiblePrefixes = useAccessibleReferencePrefixes(nodeId)
141+
142+
const shouldHighlightReference = useCallback(
143+
(part: string): boolean => {
144+
if (!part.startsWith('<') || !part.endsWith('>')) {
145+
return false
146+
}
147+
148+
if (!isLikelyReferenceSegment(part)) {
149+
return false
150+
}
151+
152+
const split = splitReferenceSegment(part)
153+
if (!split) {
154+
return false
155+
}
156+
157+
const reference = split.reference
158+
159+
if (!accessiblePrefixes) {
160+
return true
161+
}
162+
163+
const inner = reference.slice(1, -1)
164+
const [prefix] = inner.split('.')
165+
const normalizedPrefix = normalizeBlockName(prefix)
166+
167+
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
168+
return true
169+
}
170+
171+
return accessiblePrefixes.has(normalizedPrefix)
172+
},
173+
[accessiblePrefixes]
174+
)
175+
176+
const highlightWithReferences = useCallback(
177+
(code: string): string => {
178+
const placeholders: Array<{
179+
placeholder: string
180+
original: string
181+
type: 'var' | 'env'
182+
}> = []
183+
184+
let processedCode = code
185+
186+
processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
187+
const placeholder = `__ENV_VAR_${placeholders.length}__`
188+
placeholders.push({ placeholder, original: match, type: 'env' })
189+
return placeholder
190+
})
191+
192+
processedCode = processedCode.replace(/<[^>]+>/g, (match) => {
193+
if (shouldHighlightReference(match)) {
194+
const placeholder = `__VAR_REF_${placeholders.length}__`
195+
placeholders.push({ placeholder, original: match, type: 'var' })
196+
return placeholder
197+
}
198+
return match
199+
})
200+
201+
let highlightedCode = highlight(processedCode, languages.javascript, 'javascript')
202+
203+
placeholders.forEach(({ placeholder, original, type }) => {
204+
if (type === 'env') {
205+
highlightedCode = highlightedCode.replace(
206+
placeholder,
207+
`<span class="text-blue-500">${original}</span>`
208+
)
209+
} else {
210+
const escaped = original.replace(/</g, '&lt;').replace(/>/g, '&gt;')
211+
highlightedCode = highlightedCode.replace(
212+
placeholder,
213+
`<span class="text-blue-500">${escaped}</span>`
214+
)
215+
}
216+
})
217+
218+
return highlightedCode
219+
},
220+
[shouldHighlightReference]
221+
)
133222

134223
// Handle type change
135224
const handleTypeChange = useCallback(
@@ -325,7 +414,7 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
325414
<Editor
326415
value={conditionString}
327416
onValueChange={handleEditorChange}
328-
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
417+
highlight={highlightWithReferences}
329418
padding={0}
330419
style={{
331420
fontFamily: 'monospace',
@@ -363,7 +452,7 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
363452
<Editor
364453
value={editorValue}
365454
onValueChange={handleEditorChange}
366-
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
455+
highlight={highlightWithReferences}
367456
padding={0}
368457
style={{
369458
fontFamily: 'monospace',

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
1313
import { CodeLanguage } from '@/lib/execution/languages'
1414
import { createLogger } from '@/lib/logs/console/logger'
1515
import { cn } from '@/lib/utils'
16-
import { isLikelyReferenceSegment, SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references'
16+
import {
17+
isLikelyReferenceSegment,
18+
SYSTEM_REFERENCE_PREFIXES,
19+
splitReferenceSegment,
20+
} from '@/lib/workflows/references'
1721
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
1822
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
1923
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
@@ -433,11 +437,18 @@ IMPORTANT FORMATTING RULES:
433437
return false
434438
}
435439

440+
const split = splitReferenceSegment(part)
441+
if (!split) {
442+
return false
443+
}
444+
445+
const reference = split.reference
446+
436447
if (!accessiblePrefixes) {
437448
return true
438449
}
439450

440-
const inner = part.slice(1, -1)
451+
const inner = reference.slice(1, -1)
441452
const [prefix] = inner.split('.')
442453
const normalizedPrefix = normalizeBlockName(prefix)
443454

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
1414
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
1515
import { createLogger } from '@/lib/logs/console/logger'
1616
import { cn } from '@/lib/utils'
17-
import { isLikelyReferenceSegment, SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references'
17+
import {
18+
isLikelyReferenceSegment,
19+
SYSTEM_REFERENCE_PREFIXES,
20+
splitReferenceSegment,
21+
} from '@/lib/workflows/references'
1822
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
1923
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
2024
import { useTagSelection } from '@/hooks/use-tag-selection'
@@ -74,11 +78,18 @@ export function ConditionInput({
7478
return false
7579
}
7680

81+
const split = splitReferenceSegment(part)
82+
if (!split) {
83+
return false
84+
}
85+
86+
const reference = split.reference
87+
7788
if (!accessiblePrefixes) {
7889
return true
7990
}
8091

81-
const inner = part.slice(1, -1)
92+
const inner = reference.slice(1, -1)
8293
const [prefix] = inner.split('.')
8394
const normalizedPrefix = normalizeBlockName(prefix)
8495

apps/sim/components/ui/formatted-text.tsx

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import type { ReactNode } from 'react'
4+
import { splitReferenceSegment } from '@/lib/workflows/references'
45
import { normalizeBlockName } from '@/stores/workflows/utils'
56

67
export interface HighlightContext {
@@ -17,16 +18,16 @@ const SYSTEM_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable'])
1718
export function formatDisplayText(text: string, context?: HighlightContext): ReactNode[] {
1819
if (!text) return []
1920

20-
const shouldHighlightPart = (part: string): boolean => {
21-
if (!part.startsWith('<') || !part.endsWith('>')) {
21+
const shouldHighlightReference = (reference: string): boolean => {
22+
if (!reference.startsWith('<') || !reference.endsWith('>')) {
2223
return false
2324
}
2425

2526
if (context?.highlightAll) {
2627
return true
2728
}
2829

29-
const inner = part.slice(1, -1)
30+
const inner = reference.slice(1, -1)
3031
const [prefix] = inner.split('.')
3132
const normalizedPrefix = normalizeBlockName(prefix)
3233

@@ -41,17 +42,52 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
4142
return false
4243
}
4344

44-
const parts = text.split(/(<[^>]+>|\{\{[^}]+\}\})/g)
45+
const nodes: ReactNode[] = []
46+
const regex = /<[^>]+>|\{\{[^}]+\}\}/g
47+
let lastIndex = 0
48+
let key = 0
4549

46-
return parts.map((part, index) => {
47-
if (shouldHighlightPart(part) || part.match(/^\{\{[^}]+\}\}$/)) {
48-
return (
49-
<span key={index} className='text-blue-500'>
50-
{part}
50+
const pushPlainText = (value: string) => {
51+
if (!value) return
52+
nodes.push(<span key={key++}>{value}</span>)
53+
}
54+
55+
let match: RegExpExecArray | null
56+
while ((match = regex.exec(text)) !== null) {
57+
const matchText = match[0]
58+
const index = match.index
59+
60+
if (index > lastIndex) {
61+
pushPlainText(text.slice(lastIndex, index))
62+
}
63+
64+
if (matchText.startsWith('{{')) {
65+
nodes.push(
66+
<span key={key++} className='text-blue-500'>
67+
{matchText}
5168
</span>
5269
)
70+
} else {
71+
const split = splitReferenceSegment(matchText)
72+
73+
if (split && shouldHighlightReference(split.reference)) {
74+
pushPlainText(split.leading)
75+
nodes.push(
76+
<span key={key++} className='text-blue-500'>
77+
{split.reference}
78+
</span>
79+
)
80+
} else {
81+
nodes.push(<span key={key++}>{matchText}</span>)
82+
}
5383
}
5484

55-
return <span key={index}>{part}</span>
56-
})
85+
lastIndex = regex.lastIndex
86+
}
87+
88+
if (lastIndex < text.length) {
89+
pushPlainText(text.slice(lastIndex))
90+
}
91+
92+
return nodes
5793
}

apps/sim/components/ui/tag-dropdown.test.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it, vi } from 'vitest'
2-
import { checkTagTrigger } from '@/components/ui/tag-dropdown'
2+
import { checkTagTrigger, getTagSearchTerm } from '@/components/ui/tag-dropdown'
33
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
44
import type { BlockState } from '@/stores/workflows/workflow/types'
55
import { generateLoopBlocks } from '@/stores/workflows/workflow/utils'
@@ -1002,14 +1002,11 @@ describe('TagDropdown Search and Filtering', () => {
10021002
{ input: 'Hello <loop.in', cursorPosition: 14, expected: 'loop.in' },
10031003
{ input: 'Hello world', cursorPosition: 11, expected: '' },
10041004
{ input: 'Hello <var> and <loo', cursorPosition: 20, expected: 'loo' },
1005+
{ input: '<block.output> < <', cursorPosition: 18, expected: '' },
10051006
]
10061007

10071008
testCases.forEach(({ input, cursorPosition, expected }) => {
1008-
const textBeforeCursor = input.slice(0, cursorPosition)
1009-
const match = textBeforeCursor.match(/<([^>]*)$/)
1010-
const searchTerm = match ? match[1].toLowerCase() : ''
1011-
1012-
expect(searchTerm).toBe(expected)
1009+
expect(getTagSearchTerm(input, cursorPosition)).toBe(expected)
10131010
})
10141011
})
10151012

apps/sim/components/ui/tag-dropdown.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ export const checkTagTrigger = (text: string, cursorPosition: number): { show: b
5959
return { show: false }
6060
}
6161

62+
export const getTagSearchTerm = (text: string, cursorPosition: number): string => {
63+
if (cursorPosition <= 0) {
64+
return ''
65+
}
66+
67+
const textBeforeCursor = text.slice(0, cursorPosition)
68+
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
69+
70+
if (lastOpenBracket === -1) {
71+
return ''
72+
}
73+
74+
const lastCloseBracket = textBeforeCursor.lastIndexOf('>')
75+
76+
if (lastCloseBracket > lastOpenBracket) {
77+
return ''
78+
}
79+
80+
return textBeforeCursor.slice(lastOpenBracket + 1).toLowerCase()
81+
}
82+
6283
const BLOCK_COLORS = {
6384
VARIABLE: '#2F8BFF',
6485
DEFAULT: '#2F55FF',
@@ -344,11 +365,10 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
344365
const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId)
345366
const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : []
346367

347-
const searchTerm = useMemo(() => {
348-
const textBeforeCursor = inputValue.slice(0, cursorPosition)
349-
const match = textBeforeCursor.match(/<([^>]*)$/)
350-
return match ? match[1].toLowerCase() : ''
351-
}, [inputValue, cursorPosition])
368+
const searchTerm = useMemo(
369+
() => getTagSearchTerm(inputValue, cursorPosition),
370+
[inputValue, cursorPosition]
371+
)
352372

353373
const {
354374
tags,

apps/sim/lib/webhooks/processor.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ export async function parseWebhookBody(
6565
const requestClone = request.clone()
6666
rawBody = await requestClone.text()
6767

68+
// Allow empty body - some webhooks send empty payloads
6869
if (!rawBody || rawBody.length === 0) {
69-
logger.warn(`[${requestId}] Rejecting request with empty body`)
70-
return new NextResponse('Empty request body', { status: 400 })
70+
logger.debug(`[${requestId}] Received request with empty body, treating as empty object`)
71+
return { body: {}, rawBody: '' }
7172
}
7273
} catch (bodyError) {
7374
logger.error(`[${requestId}] Failed to read request body`, {
@@ -96,9 +97,9 @@ export async function parseWebhookBody(
9697
logger.debug(`[${requestId}] Parsed JSON webhook payload`)
9798
}
9899

100+
// Allow empty JSON objects - some webhooks send empty payloads
99101
if (Object.keys(body).length === 0) {
100-
logger.warn(`[${requestId}] Rejecting empty JSON object`)
101-
return new NextResponse('Empty JSON payload', { status: 400 })
102+
logger.debug(`[${requestId}] Received empty JSON object`)
102103
}
103104
} catch (parseError) {
104105
logger.error(`[${requestId}] Failed to parse webhook body`, {

0 commit comments

Comments
 (0)