Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
resolveCanvasSentence,
} from '@/lib/workflows/blocks/canvas-sentence'
import { resolveSelectedTriggerId } from '@/lib/workflows/blocks/canvas-trigger-sentence'
import { resolveCanvasCodePreview } from '@/lib/workflows/blocks/code-preview'
import { calculateWorkflowBlockDimensions } from '@/lib/workflows/blocks/deterministic-dimensions'
import { getConditionRows, getRouterRows } from '@/lib/workflows/dynamic-handle-topology'
import { getDependsOnFields } from '@/lib/workflows/subblocks/dependencies'
Expand Down Expand Up @@ -288,6 +289,9 @@ const areSubBlockRowPropsEqual = (
const prevValue = subBlockId ? prevProps.allSubBlockValues?.[subBlockId]?.value : undefined
const nextValue = subBlockId ? nextProps.allSubBlockValues?.[subBlockId]?.value : undefined
const valueEqual = prevValue === nextValue || isEqual(prevValue, nextValue)
const codeLanguageEqual =
prevProps.subBlock?.type !== 'code' ||
prevProps.allSubBlockValues?.language?.value === nextProps.allSubBlockValues?.language?.value

return (
prevProps.title === nextProps.title &&
Expand All @@ -298,6 +302,7 @@ const areSubBlockRowPropsEqual = (
prevProps.workflowId === nextProps.workflowId &&
prevProps.blockId === nextProps.blockId &&
valueEqual &&
codeLanguageEqual &&
prevProps.displayAdvancedOptions === nextProps.displayAdvancedOptions &&
prevProps.canonicalIndex === nextProps.canonicalIndex &&
prevProps.canonicalModeOverrides === nextProps.canonicalModeOverrides &&
Expand Down Expand Up @@ -596,12 +601,15 @@ const SubBlockRow = memo(function SubBlockRow({
webhookUrlDisplayValue ||
selectorDisplayName
const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value)
const codePreview =
Comment thread
icecrasher321 marked this conversation as resolved.
variant === 'inline-value' ? resolveCanvasCodePreview(subBlock, rawValue, rawValues) : undefined

return (
<SubBlockRowView
title={title}
displayValue={displayValue}
isMonospace={isMonospaceField}
codePreview={codePreview}
variant={variant}
icon={icon}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
resolveCanvasSentence,
} from '@/lib/workflows/blocks/canvas-sentence'
import { resolveSelectedTriggerId } from '@/lib/workflows/blocks/canvas-trigger-sentence'
import { resolveCanvasCodePreview } from '@/lib/workflows/blocks/code-preview'
import {
getDisplayValue,
hasDisplayableRowValue,
Expand Down Expand Up @@ -562,6 +563,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
<SubBlockRowView
title={subBlock.title ?? subBlock.id}
displayValue={displayValue}
codePreview={resolveCanvasCodePreview(subBlock, rawValue, rawValues)}
variant='inline-value'
/>
)
Expand Down
9 changes: 8 additions & 1 deletion apps/sim/blocks/blocks/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { ApiBlock } from '@/blocks/blocks/api'

describe('API block redirect policy', () => {
describe('API block', () => {
it('uses a versioned safe default without changing legacy blocks', () => {
const version = ApiBlock.subBlocks.find((subBlock) => subBlock.id === 'redirectPolicyVersion')
const sendCredentials = ApiBlock.subBlocks.find(
Expand All @@ -14,4 +14,11 @@ describe('API block redirect policy', () => {
expect(sendCredentials?.mode).toBe('advanced')
expect(sendCredentials?.defaultValue).toBe(true)
})

it('marks the request body as JSON for code previews', () => {
const body = ApiBlock.subBlocks.find((subBlock) => subBlock.id === 'body')

expect(body?.type).toBe('code')
expect(body?.language).toBe('json')
})
})
1 change: 1 addition & 0 deletions apps/sim/blocks/blocks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const ApiBlock: BlockConfig<RequestResponse> = {
id: 'body',
title: 'Body',
type: 'code',
language: 'json',
placeholder: 'Enter JSON...',
wandConfig: {
enabled: true,
Expand Down
47 changes: 47 additions & 0 deletions apps/sim/lib/workflows/blocks/code-preview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import { resolveCanvasCodePreview } from '@/lib/workflows/blocks/code-preview'
import type { SubBlockConfig } from '@/blocks/types'

const CODE_SUBBLOCK: SubBlockConfig = {
id: 'code',
type: 'code',
language: 'javascript',
}

describe('resolveCanvasCodePreview', () => {
it('uses the selected language when the block has a language field', () => {
expect(
resolveCanvasCodePreview(CODE_SUBBLOCK, 'print("hello")', { language: 'python' })
).toEqual({
code: 'print("hello")',
language: 'python',
})
})

it('maps the stored shell language to the Prism bash grammar', () => {
expect(
resolveCanvasCodePreview({ ...CODE_SUBBLOCK, language: 'shell' }, 'echo hello', {})
).toEqual({
code: 'echo hello',
language: 'bash',
})
})

it('falls back to the subblock language when the selected language is empty', () => {
expect(resolveCanvasCodePreview(CODE_SUBBLOCK, 'return true', { language: '' })).toEqual({
code: 'return true',
language: 'javascript',
})
})

it('does not preview non-code, password, empty, or non-string values', () => {
expect(
resolveCanvasCodePreview({ ...CODE_SUBBLOCK, type: 'short-input' }, 'hello', {})
).toBeUndefined()
expect(
resolveCanvasCodePreview({ ...CODE_SUBBLOCK, password: true }, 'secret', {})
).toBeUndefined()
expect(resolveCanvasCodePreview(CODE_SUBBLOCK, ' ', {})).toBeUndefined()
expect(resolveCanvasCodePreview(CODE_SUBBLOCK, { source: 'code' }, {})).toBeUndefined()
})
})
38 changes: 38 additions & 0 deletions apps/sim/lib/workflows/blocks/code-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { CodePreview, CodePreviewLanguage } from '@sim/workflow-renderer'
import type { SubBlockConfig } from '@/blocks/types'

/** Maps stored editor languages to the Prism grammar used by the shared viewer. */
function resolveCodePreviewLanguage(language: unknown): CodePreviewLanguage {
switch (language) {
case 'json':
case 'python':
case 'javascript':
return language
case 'shell':
return 'bash'
default:
return 'javascript'
Comment thread
icecrasher321 marked this conversation as resolved.
}
}

/** Builds a rich preview only for safe, non-empty code fields on the canvas. */
export function resolveCanvasCodePreview(
subBlock: SubBlockConfig | undefined,
rawValue: unknown,
values: Readonly<Record<string, unknown>>
): CodePreview | undefined {
if (
subBlock?.type !== 'code' ||
subBlock.password === true ||
typeof rawValue !== 'string' ||
rawValue.trim().length === 0
) {
return undefined
}

const language =
typeof values.language === 'string' && values.language.length > 0
? values.language
: subBlock.language
return { code: rawValue, language: resolveCodePreviewLanguage(language) }
}
55 changes: 42 additions & 13 deletions packages/emcn/src/components/code/code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ function highlightOrEscape(prism: PrismModule | null, text: string, language: st
* All code editors in the app should use these values for consistency.
*/
export const CODE_LINE_HEIGHT_PX = 21
const COMPACT_CODE_LINE_HEIGHT_PX = 20

/**
* Gutter width values based on the number of digits in line numbers.
Expand Down Expand Up @@ -679,6 +680,8 @@ interface CodeRowProps {
showGutter: boolean
/** Custom styles for the gutter */
gutterStyle?: React.CSSProperties
/** Visual density for read-only code. */
density: CodeViewerDensity
/** Left offset for alignment */
leftOffset: number
/** Whether to wrap long lines */
Expand All @@ -703,6 +706,7 @@ function CodeRow({
gutterWidth,
showGutter,
gutterStyle,
density,
leftOffset,
wrapText,
showCollapseColumn,
Expand All @@ -718,7 +722,10 @@ function CodeRow({
<div className={cn('flex', wrapText && 'overflow-hidden')} data-row-index={index}>
{showGutter && (
<div
className='flex-shrink-0 select-none pr-0.5 text-right text-[var(--text-muted)] text-xs tabular-nums leading-[21px] dark:text-[var(--code-line-number)]'
className={cn(
'flex-shrink-0 select-none pr-0.5 text-right text-[var(--text-muted)] tabular-nums dark:text-[var(--code-line-number)]',
density === 'compact' ? 'text-caption leading-5' : 'text-xs leading-[21px]'
)}
style={{ width: gutterWidth, marginLeft: leftOffset, ...gutterStyle }}
>
{line.lineNumber}
Expand All @@ -740,7 +747,8 @@ function CodeRow({
)}
<pre
className={cn(
'm-0 flex-1 pr-2 pl-2 font-mono text-[var(--text-primary)] text-small leading-[21px] dark:text-[var(--code-foreground)]',
'm-0 flex-1 pr-2 pl-2 font-mono text-[var(--text-primary)] dark:text-[var(--code-foreground)]',
density === 'compact' ? 'text-caption leading-5' : 'text-small leading-[21px]',
wrapText ? 'min-w-0 whitespace-pre-wrap break-words' : 'whitespace-pre'
)}
dangerouslySetInnerHTML={{ __html: line.html || '&nbsp;' }}
Expand Down Expand Up @@ -796,6 +804,8 @@ function applySearchHighlightingToLine(
/**
* Props for the Code.Viewer component (readonly code display).
*/
type CodeViewerDensity = 'default' | 'compact'

interface CodeViewerProps {
/** Code content to display */
code: string
Expand All @@ -805,6 +815,8 @@ interface CodeViewerProps {
language?: 'javascript' | 'json' | 'python' | 'bash'
/** Additional CSS classes for the container */
className?: string
/** Visual density for read-only code. */
density?: CodeViewerDensity
/** Left padding offset (useful for terminal alignment) */
paddingLeft?: number
/** Inline styles for the gutter (e.g., to override background) */
Expand Down Expand Up @@ -891,6 +903,8 @@ type ViewerInnerProps = {
language: 'javascript' | 'json' | 'python' | 'bash'
/** Additional CSS classes for the container */
className?: string
/** Visual density for read-only code. */
density: CodeViewerDensity
/** Left padding offset in pixels */
paddingLeft: number
/** Custom styles for the gutter */
Expand Down Expand Up @@ -918,6 +932,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
showGutter,
language,
className,
density,
paddingLeft,
gutterStyle,
wrapText,
Expand Down Expand Up @@ -1010,15 +1025,16 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
const virtualizer = useVirtualizer({
count: visibleLines.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => CODE_LINE_HEIGHT_PX,
estimateSize: () => (density === 'compact' ? COMPACT_CODE_LINE_HEIGHT_PX : CODE_LINE_HEIGHT_PX),
overscan: 5,
})

/**
* Drop cached row measurements when leaving wrap mode: the measureElement
* refs detach with their wrapped heights still cached, and falling back to
* the fixed estimate is exactly correct for nowrap rows. Entering wrap needs
* no reset — refs re-attach and re-measure as rows render.
* Drop cached row measurements when leaving wrap mode or changing density:
* the measureElement refs detach with their wrapped heights still cached,
* and falling back to the current fixed estimate is exactly correct for
* nowrap rows. Entering wrap needs no reset — refs re-attach and re-measure
* as rows render.
*
* Deliberately NOT keyed on content (`visibleLines`): `measure()` wipes the
* cache without re-measuring mounted rows (ResizeObserver only fires on size
Expand All @@ -1029,7 +1045,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
*/
useEffect(() => {
if (!wrapText) virtualizer.measure()
}, [wrapText, virtualizer])
}, [density, wrapText, virtualizer])

useEffect(() => {
if (!searchQuery?.trim() || matchCount === 0 || !scrollRef.current) return
Expand Down Expand Up @@ -1107,6 +1123,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
gutterWidth={gutterWidth}
showGutter={showGutter}
gutterStyle={gutterStyle}
density={density}
leftOffset={paddingLeft}
wrapText={wrapText}
showCollapseColumn={effectiveShowCollapseColumn}
Expand All @@ -1131,6 +1148,7 @@ const ViewerInner = memo(function ViewerInner({
showGutter,
language,
className,
density,
paddingLeft,
gutterStyle,
wrapText,
Expand Down Expand Up @@ -1236,8 +1254,8 @@ const ViewerInner = memo(function ViewerInner({
<div
style={{
paddingLeft,
paddingTop: '8px',
paddingBottom: '8px',
paddingTop: density === 'compact' ? '6px' : '8px',
paddingBottom: density === 'compact' ? '6px' : '8px',
display: 'grid',
gridTemplateColumns: effectiveShowCollapseColumn
? `${gutterWidth}px ${collapseColumnWidth}px 1fr`
Expand All @@ -1252,7 +1270,10 @@ const ViewerInner = memo(function ViewerInner({
return (
<Fragment key={idx}>
<div
className='select-none pr-0.5 text-right text-[var(--text-muted)] text-xs tabular-nums leading-[21px] dark:text-[var(--code-line-number)]'
className={cn(
'select-none pr-0.5 text-right text-[var(--text-muted)] tabular-nums dark:text-[var(--code-line-number)]',
density === 'compact' ? 'text-caption leading-5' : 'text-xs leading-[21px]'
)}
style={gutterStyle}
>
{lineNumber}
Expand All @@ -1270,7 +1291,10 @@ const ViewerInner = memo(function ViewerInner({
)}
<pre
className={cn(
'm-0 min-w-0 pr-2 pl-2 font-mono text-[var(--text-primary)] text-small leading-[21px] dark:text-[var(--code-foreground)]',
'm-0 min-w-0 pr-2 pl-2 font-mono text-[var(--text-primary)] dark:text-[var(--code-foreground)]',
density === 'compact'
? 'text-caption leading-5'
: 'text-small leading-[21px]',
whitespaceClass
)}
dangerouslySetInnerHTML={{ __html: html }}
Expand All @@ -1291,7 +1315,10 @@ const ViewerInner = memo(function ViewerInner({
<pre
className={cn(
whitespaceClass,
'p-2 font-mono text-[var(--text-primary)] text-small leading-[21px] dark:text-[var(--code-foreground)]'
'font-mono text-[var(--text-primary)] dark:text-[var(--code-foreground)]',
density === 'compact'
? 'px-2 py-1.5 text-caption leading-5'
: 'p-2 text-small leading-[21px]'
)}
style={{ paddingLeft: paddingLeft > 0 ? paddingLeft : undefined }}
dangerouslySetInnerHTML={{ __html: highlightedCode }}
Expand Down Expand Up @@ -1330,6 +1357,7 @@ function Viewer({
showGutter = false,
language = 'json',
className,
density = 'default',
paddingLeft = 0,
gutterStyle,
wrapText = false,
Expand All @@ -1345,6 +1373,7 @@ function Viewer({
showGutter,
language,
className,
density,
paddingLeft,
gutterStyle,
wrapText,
Expand Down
Loading
Loading