Skip to content

Commit 79dd1cc

Browse files
aadamgoughAdam Gough
andauthored
fix(ux): minor ux changes (#1109)
* minor UX fixes * changed variable collapse * lint --------- Co-authored-by: Adam Gough <[email protected]>
1 parent 730164a commit 79dd1cc

File tree

4 files changed

+157
-110
lines changed

4 files changed

+157
-110
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/console.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function Console({ panelWidth }: ConsoleProps) {
2525
No console entries
2626
</div>
2727
) : (
28-
<ScrollArea className='h-full' hideScrollbar={true}>
28+
<ScrollArea className='h-full' hideScrollbar={false}>
2929
<div className='space-y-3'>
3030
{filteredEntries.map((entry) => (
3131
<ConsoleEntry key={entry.id} entry={entry} consoleWidth={panelWidth} />

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/variables/variables.tsx

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

33
import { useEffect, useRef, useState } from 'react'
4-
import { AlertTriangle, ChevronDown, Copy, MoreVertical, Plus, Trash } from 'lucide-react'
4+
import {
5+
AlertTriangle,
6+
ChevronDown,
7+
Copy,
8+
Maximize2,
9+
Minimize2,
10+
MoreVertical,
11+
Plus,
12+
Trash,
13+
} from 'lucide-react'
514
import { highlight, languages } from 'prismjs'
615
import 'prismjs/components/prism-javascript'
716
import 'prismjs/themes/prism.css'
@@ -52,6 +61,16 @@ export function Variables() {
5261
// Track which variables are currently being edited
5362
const [_activeEditors, setActiveEditors] = useState<Record<string, boolean>>({})
5463

64+
// Collapsed state per variable
65+
const [collapsedById, setCollapsedById] = useState<Record<string, boolean>>({})
66+
67+
const toggleCollapsed = (variableId: string) => {
68+
setCollapsedById((prev) => ({
69+
...prev,
70+
[variableId]: !prev[variableId],
71+
}))
72+
}
73+
5574
// Handle variable name change with validation
5675
const handleVariableNameChange = (variableId: string, newName: string) => {
5776
const validatedName = validateName(newName)
@@ -220,7 +239,7 @@ export function Variables() {
220239
</Button>
221240
</div>
222241
) : (
223-
<ScrollArea className='h-full' hideScrollbar={true}>
242+
<ScrollArea className='h-full' hideScrollbar={false}>
224243
<div className='space-y-4'>
225244
{workflowVariables.map((variable) => (
226245
<div key={variable.id} className='space-y-2'>
@@ -298,6 +317,17 @@ export function Variables() {
298317
align='end'
299318
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
300319
>
320+
<DropdownMenuItem
321+
onClick={() => toggleCollapsed(variable.id)}
322+
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
323+
>
324+
{(collapsedById[variable.id] ?? false) ? (
325+
<Maximize2 className='mr-2 h-4 w-4 text-muted-foreground' />
326+
) : (
327+
<Minimize2 className='mr-2 h-4 w-4 text-muted-foreground' />
328+
)}
329+
{(collapsedById[variable.id] ?? false) ? 'Expand' : 'Collapse'}
330+
</DropdownMenuItem>
301331
<DropdownMenuItem
302332
onClick={() => collaborativeDuplicateVariable(variable.id)}
303333
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
@@ -317,71 +347,75 @@ export function Variables() {
317347
</div>
318348

319349
{/* Value area */}
320-
<div className='relative rounded-lg bg-secondary/50'>
321-
{/* Validation indicator */}
322-
{variable.value !== '' && getValidationStatus(variable) && (
323-
<div className='absolute top-2 right-2 z-10'>
324-
<Tooltip>
325-
<TooltipTrigger asChild>
326-
<div className='cursor-help'>
327-
<AlertTriangle className='h-3 w-3 text-muted-foreground' />
328-
</div>
329-
</TooltipTrigger>
330-
<TooltipContent side='bottom' className='max-w-xs'>
331-
<p>{getValidationStatus(variable)}</p>
332-
</TooltipContent>
333-
</Tooltip>
334-
</div>
335-
)}
336-
337-
{/* Editor */}
338-
<div className='relative overflow-hidden'>
339-
<div
340-
className='relative min-h-[36px] w-full max-w-full px-3 py-2 font-normal text-sm'
341-
ref={(el) => {
342-
editorRefs.current[variable.id] = el
343-
}}
344-
style={{ maxWidth: '100%' }}
345-
>
346-
{variable.value === '' && (
347-
<div className='pointer-events-none absolute inset-0 flex select-none items-start justify-start px-3 py-2 font-[380] text-muted-foreground text-sm leading-normal'>
348-
<div style={{ lineHeight: '20px' }}>{getPlaceholder(variable.type)}</div>
349-
</div>
350-
)}
351-
<Editor
352-
key={`editor-${variable.id}-${variable.type}`}
353-
value={formatValue(variable)}
354-
onValueChange={handleEditorChange.bind(null, variable)}
355-
onBlur={() => handleEditorBlur(variable.id)}
356-
onFocus={() => handleEditorFocus(variable.id)}
357-
highlight={(code) =>
358-
// Only apply syntax highlighting for non-basic text types
359-
variable.type === 'plain' || variable.type === 'string'
360-
? code
361-
: highlight(
362-
code,
363-
languages[getEditorLanguage(variable.type)],
364-
getEditorLanguage(variable.type)
365-
)
366-
}
367-
padding={0}
368-
style={{
369-
fontFamily: 'inherit',
370-
lineHeight: '20px',
371-
width: '100%',
372-
maxWidth: '100%',
373-
whiteSpace: 'pre-wrap',
374-
wordBreak: 'break-all',
375-
overflowWrap: 'break-word',
376-
minHeight: '20px',
377-
overflow: 'hidden',
350+
{!(collapsedById[variable.id] ?? false) && (
351+
<div className='relative rounded-lg bg-secondary/50'>
352+
{/* Validation indicator */}
353+
{variable.value !== '' && getValidationStatus(variable) && (
354+
<div className='absolute top-2 right-2 z-10'>
355+
<Tooltip>
356+
<TooltipTrigger asChild>
357+
<div className='cursor-help'>
358+
<AlertTriangle className='h-3 w-3 text-muted-foreground' />
359+
</div>
360+
</TooltipTrigger>
361+
<TooltipContent side='bottom' className='max-w-xs'>
362+
<p>{getValidationStatus(variable)}</p>
363+
</TooltipContent>
364+
</Tooltip>
365+
</div>
366+
)}
367+
368+
{/* Editor */}
369+
<div className='relative overflow-hidden'>
370+
<div
371+
className='relative min-h-[36px] w-full max-w-full px-3 py-2 font-normal text-sm'
372+
ref={(el) => {
373+
editorRefs.current[variable.id] = el
378374
}}
379-
className='[&>pre]:!max-w-full [&>pre]:!overflow-hidden [&>pre]:!whitespace-pre-wrap [&>pre]:!break-all [&>pre]:!overflow-wrap-break-word [&>textarea]:!max-w-full [&>textarea]:!overflow-hidden [&>textarea]:!whitespace-pre-wrap [&>textarea]:!break-all [&>textarea]:!overflow-wrap-break-word font-[380] text-foreground text-sm leading-normal focus:outline-none'
380-
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full max-w-full whitespace-pre-wrap break-all overflow-wrap-break-word overflow-hidden font-[380] text-foreground'
381-
/>
375+
style={{ maxWidth: '100%' }}
376+
>
377+
{variable.value === '' && (
378+
<div className='pointer-events-none absolute inset-0 flex select-none items-start justify-start px-3 py-2 font-[380] text-muted-foreground text-sm leading-normal'>
379+
<div style={{ lineHeight: '20px' }}>
380+
{getPlaceholder(variable.type)}
381+
</div>
382+
</div>
383+
)}
384+
<Editor
385+
key={`editor-${variable.id}-${variable.type}`}
386+
value={formatValue(variable)}
387+
onValueChange={handleEditorChange.bind(null, variable)}
388+
onBlur={() => handleEditorBlur(variable.id)}
389+
onFocus={() => handleEditorFocus(variable.id)}
390+
highlight={(code) =>
391+
// Only apply syntax highlighting for non-basic text types
392+
variable.type === 'plain' || variable.type === 'string'
393+
? code
394+
: highlight(
395+
code,
396+
languages[getEditorLanguage(variable.type)],
397+
getEditorLanguage(variable.type)
398+
)
399+
}
400+
padding={0}
401+
style={{
402+
fontFamily: 'inherit',
403+
lineHeight: '20px',
404+
width: '100%',
405+
maxWidth: '100%',
406+
whiteSpace: 'pre-wrap',
407+
wordBreak: 'break-all',
408+
overflowWrap: 'break-word',
409+
minHeight: '20px',
410+
overflow: 'hidden',
411+
}}
412+
className='[&>pre]:!max-w-full [&>pre]:!overflow-hidden [&>pre]:!whitespace-pre-wrap [&>pre]:!break-all [&>pre]:!overflow-wrap-break-word [&>textarea]:!max-w-full [&>textarea]:!overflow-hidden [&>textarea]:!whitespace-pre-wrap [&>textarea]:!break-all [&>textarea]:!overflow-wrap-break-word font-[380] text-foreground text-sm leading-normal focus:outline-none'
413+
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full max-w-full whitespace-pre-wrap break-all overflow-wrap-break-word overflow-hidden font-[380] text-foreground'
414+
/>
415+
</div>
382416
</div>
383417
</div>
384-
</div>
418+
)}
385419
</div>
386420
))}
387421

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,11 @@ const WorkflowContent = React.memo(() => {
667667
y: position.y - containerInfo.loopPosition.y,
668668
}
669669

670+
// Capture existing child blocks before adding the new one
671+
const existingChildBlocks = Object.values(blocks).filter(
672+
(b) => b.data?.parentId === containerInfo.loopId
673+
)
674+
670675
// Add block with parent info
671676
addBlock(id, data.type, name, relativePosition, {
672677
parentId: containerInfo.loopId,
@@ -680,12 +685,35 @@ const WorkflowContent = React.memo(() => {
680685
// Auto-connect logic for blocks inside containers
681686
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
682687
if (isAutoConnectEnabled && data.type !== 'starter') {
683-
// First priority: Connect to the container's start node
684-
const containerNode = getNodes().find((n) => n.id === containerInfo.loopId)
685-
const containerType = containerNode?.type
688+
if (existingChildBlocks.length > 0) {
689+
// Connect to the nearest existing child block within the container
690+
const closestBlock = existingChildBlocks
691+
.map((b) => ({
692+
block: b,
693+
distance: Math.sqrt(
694+
(b.position.x - relativePosition.x) ** 2 +
695+
(b.position.y - relativePosition.y) ** 2
696+
),
697+
}))
698+
.sort((a, b) => a.distance - b.distance)[0]?.block
686699

687-
if (containerType === 'subflowNode') {
688-
// Connect from the container's start node to the new block
700+
if (closestBlock) {
701+
const sourceHandle = determineSourceHandle({
702+
id: closestBlock.id,
703+
type: closestBlock.type,
704+
})
705+
addEdge({
706+
id: crypto.randomUUID(),
707+
source: closestBlock.id,
708+
target: id,
709+
sourceHandle,
710+
targetHandle: 'target',
711+
type: 'workflowEdge',
712+
})
713+
}
714+
} else {
715+
// No existing children: connect from the container's start handle
716+
const containerNode = getNodes().find((n) => n.id === containerInfo.loopId)
689717
const startSourceHandle =
690718
(containerNode?.data as any)?.kind === 'loop'
691719
? 'loop-start-source'
@@ -699,45 +727,6 @@ const WorkflowContent = React.memo(() => {
699727
targetHandle: 'target',
700728
type: 'workflowEdge',
701729
})
702-
} else {
703-
// Fallback: Try to find other nodes in the container to connect to
704-
const containerNodes = getNodes().filter((n) => n.parentId === containerInfo.loopId)
705-
706-
if (containerNodes.length > 0) {
707-
// Connect to the closest node in the container
708-
const closestNode = containerNodes
709-
.map((n) => ({
710-
id: n.id,
711-
distance: Math.sqrt(
712-
(n.position.x - relativePosition.x) ** 2 +
713-
(n.position.y - relativePosition.y) ** 2
714-
),
715-
}))
716-
.sort((a, b) => a.distance - b.distance)[0]
717-
718-
if (closestNode) {
719-
// Get appropriate source handle
720-
const sourceNode = getNodes().find((n) => n.id === closestNode.id)
721-
const sourceType = sourceNode?.data?.type
722-
723-
// Default source handle
724-
let sourceHandle = 'source'
725-
726-
// For condition blocks, use the condition-true handle
727-
if (sourceType === 'condition') {
728-
sourceHandle = 'condition-true'
729-
}
730-
731-
addEdge({
732-
id: crypto.randomUUID(),
733-
source: closestNode.id,
734-
target: id,
735-
sourceHandle,
736-
targetHandle: 'target',
737-
type: 'workflowEdge',
738-
})
739-
}
740-
}
741730
}
742731
}
743732
} else {

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type React from 'react'
2-
import { useCallback, useEffect, useMemo, useState } from 'react'
2+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
33
import { ChevronRight } from 'lucide-react'
44
import { BlockPathCalculator } from '@/lib/block-path-calculator'
55
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
@@ -283,6 +283,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
283283
onClose,
284284
style,
285285
}) => {
286+
const containerRef = useRef<HTMLDivElement>(null)
286287
const [selectedIndex, setSelectedIndex] = useState(0)
287288
const [hoveredNested, setHoveredNested] = useState<{ tag: string; index: number } | null>(null)
288289
const [inSubmenu, setInSubmenu] = useState(false)
@@ -949,6 +950,28 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
949950
}
950951
}, [orderedTags.length, selectedIndex])
951952

953+
// Close on outside click/touch when dropdown is visible
954+
useEffect(() => {
955+
if (!visible) return
956+
957+
const handlePointerDown = (e: MouseEvent | TouchEvent) => {
958+
const el = containerRef.current
959+
if (!el) return
960+
const target = e.target as Node
961+
if (!el.contains(target)) {
962+
onClose?.()
963+
}
964+
}
965+
966+
// Use capture phase to detect before child handlers potentially stop propagation
967+
document.addEventListener('mousedown', handlePointerDown, true)
968+
document.addEventListener('touchstart', handlePointerDown, true)
969+
return () => {
970+
document.removeEventListener('mousedown', handlePointerDown, true)
971+
document.removeEventListener('touchstart', handlePointerDown, true)
972+
}
973+
}, [visible, onClose])
974+
952975
useEffect(() => {
953976
if (visible) {
954977
const handleKeyboardEvent = (e: KeyboardEvent) => {
@@ -1173,6 +1196,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
11731196

11741197
return (
11751198
<div
1199+
ref={containerRef}
11761200
className={cn(
11771201
'absolute z-[9999] mt-1 w-full overflow-visible rounded-md border bg-popover shadow-md',
11781202
className

0 commit comments

Comments
 (0)