11'use client'
22
3- import { useEffect , useMemo , useRef , useState } from 'react'
4- import { Check } from 'lucide-react'
3+ import type React from 'react'
4+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
5+ import { Check , RepeatIcon , SplitIcon } from 'lucide-react'
56import {
67 Badge ,
78 Popover ,
@@ -19,6 +20,32 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
1920import { useSubBlockStore } from '@/stores/workflows/subblock/store'
2021import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2122
23+ /**
24+ * Renders a tag icon with background color.
25+ *
26+ * @param icon - Either a letter string or a Lucide icon component
27+ * @param color - Background color for the icon container
28+ * @returns A styled icon element
29+ */
30+ const TagIcon : React . FC < {
31+ icon : string | React . ComponentType < { className ?: string } >
32+ color : string
33+ } > = ( { icon, color } ) => (
34+ < div
35+ className = 'flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
36+ style = { { background : color } }
37+ >
38+ { typeof icon === 'string' ? (
39+ < span className = '!text-white font-bold text-[10px]' > { icon } </ span >
40+ ) : (
41+ ( ( ) => {
42+ const IconComponent = icon
43+ return < IconComponent className = '!text-white size-[9px]' />
44+ } ) ( )
45+ ) }
46+ </ div >
47+ )
48+
2249/**
2350 * Props for the OutputSelect component
2451 */
@@ -71,7 +98,6 @@ export function OutputSelect({
7198 const [ highlightedIndex , setHighlightedIndex ] = useState ( - 1 )
7299 const triggerRef = useRef < HTMLDivElement > ( null )
73100 const popoverRef = useRef < HTMLDivElement > ( null )
74- const contentRef = useRef < HTMLDivElement > ( null )
75101 const blocks = useWorkflowStore ( ( state ) => state . blocks )
76102 const { isShowingDiff, isDiffReady, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore ( )
77103 const subBlockValues = useSubBlockStore ( ( state ) =>
@@ -185,8 +211,11 @@ export function OutputSelect({
185211 * @param o - The output object to check
186212 * @returns True if the output is selected, false otherwise
187213 */
188- const isSelectedValue = ( o : { id : string ; label : string } ) =>
189- selectedOutputs . includes ( o . id ) || selectedOutputs . includes ( o . label )
214+ const isSelectedValue = useCallback (
215+ ( o : { id : string ; label : string } ) =>
216+ selectedOutputs . includes ( o . id ) || selectedOutputs . includes ( o . label ) ,
217+ [ selectedOutputs ]
218+ )
190219
191220 /**
192221 * Gets display text for selected outputs
@@ -292,82 +321,94 @@ export function OutputSelect({
292321 * Handles output selection by toggling the selected state
293322 * @param value - The output label to toggle
294323 */
295- const handleOutputSelection = ( value : string ) => {
296- const emittedValue =
297- valueMode === 'label' ? value : workflowOutputs . find ( ( o ) => o . label === value ) ?. id || value
298- const index = selectedOutputs . indexOf ( emittedValue )
299-
300- const newSelectedOutputs =
301- index === - 1
302- ? [ ...new Set ( [ ...selectedOutputs , emittedValue ] ) ]
303- : selectedOutputs . filter ( ( id ) => id !== emittedValue )
304-
305- onOutputSelect ( newSelectedOutputs )
306- }
324+ const handleOutputSelection = useCallback (
325+ ( value : string ) => {
326+ const emittedValue =
327+ valueMode === 'label' ? value : workflowOutputs . find ( ( o ) => o . label === value ) ?. id || value
328+ const index = selectedOutputs . indexOf ( emittedValue )
329+
330+ const newSelectedOutputs =
331+ index === - 1
332+ ? [ ...new Set ( [ ...selectedOutputs , emittedValue ] ) ]
333+ : selectedOutputs . filter ( ( id ) => id !== emittedValue )
334+
335+ onOutputSelect ( newSelectedOutputs )
336+ } ,
337+ [ valueMode , workflowOutputs , selectedOutputs , onOutputSelect ]
338+ )
307339
308340 /**
309341 * Handles keyboard navigation within the output list
310342 * Supports ArrowUp, ArrowDown, Enter, and Escape keys
311- * @param e - Keyboard event
312343 */
313- const handleKeyDown = ( e : React . KeyboardEvent ) => {
314- if ( flattenedOutputs . length === 0 ) return
315-
316- switch ( e . key ) {
317- case 'ArrowDown' :
318- e . preventDefault ( )
319- setHighlightedIndex ( ( prev ) => {
320- const next = prev < flattenedOutputs . length - 1 ? prev + 1 : 0
321- return next
322- } )
323- break
324-
325- case 'ArrowUp' :
326- e . preventDefault ( )
327- setHighlightedIndex ( ( prev ) => {
328- const next = prev > 0 ? prev - 1 : flattenedOutputs . length - 1
329- return next
330- } )
331- break
332-
333- case 'Enter' :
334- e . preventDefault ( )
335- if ( highlightedIndex >= 0 && highlightedIndex < flattenedOutputs . length ) {
336- handleOutputSelection ( flattenedOutputs [ highlightedIndex ] . label )
337- }
338- break
344+ useEffect ( ( ) => {
345+ if ( ! open || flattenedOutputs . length === 0 ) return
346+
347+ const handleKeyboardEvent = ( e : KeyboardEvent ) => {
348+ switch ( e . key ) {
349+ case 'ArrowDown' :
350+ e . preventDefault ( )
351+ e . stopPropagation ( )
352+ setHighlightedIndex ( ( prev ) => {
353+ if ( prev === - 1 || prev >= flattenedOutputs . length - 1 ) {
354+ return 0
355+ }
356+ return prev + 1
357+ } )
358+ break
359+
360+ case 'ArrowUp' :
361+ e . preventDefault ( )
362+ e . stopPropagation ( )
363+ setHighlightedIndex ( ( prev ) => {
364+ if ( prev <= 0 ) {
365+ return flattenedOutputs . length - 1
366+ }
367+ return prev - 1
368+ } )
369+ break
370+
371+ case 'Enter' :
372+ e . preventDefault ( )
373+ e . stopPropagation ( )
374+ setHighlightedIndex ( ( currentIndex ) => {
375+ if ( currentIndex >= 0 && currentIndex < flattenedOutputs . length ) {
376+ handleOutputSelection ( flattenedOutputs [ currentIndex ] . label )
377+ }
378+ return currentIndex
379+ } )
380+ break
339381
340- case 'Escape' :
341- e . preventDefault ( )
342- setOpen ( false )
343- break
382+ case 'Escape' :
383+ e . preventDefault ( )
384+ e . stopPropagation ( )
385+ setOpen ( false )
386+ break
387+ }
344388 }
345- }
389+
390+ window . addEventListener ( 'keydown' , handleKeyboardEvent , true )
391+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyboardEvent , true )
392+ } , [ open , flattenedOutputs , handleOutputSelection ] )
346393
347394 /**
348395 * Reset highlighted index when popover opens/closes
349396 */
350397 useEffect ( ( ) => {
351398 if ( open ) {
352- // Find first selected item, or start at -1
353399 const firstSelectedIndex = flattenedOutputs . findIndex ( ( output ) => isSelectedValue ( output ) )
354400 setHighlightedIndex ( firstSelectedIndex >= 0 ? firstSelectedIndex : - 1 )
355-
356- // Focus the content for keyboard navigation
357- setTimeout ( ( ) => {
358- contentRef . current ?. focus ( )
359- } , 0 )
360401 } else {
361402 setHighlightedIndex ( - 1 )
362403 }
363- } , [ open , flattenedOutputs ] )
404+ } , [ open , flattenedOutputs , isSelectedValue ] )
364405
365406 /**
366407 * Scroll highlighted item into view
367408 */
368409 useEffect ( ( ) => {
369- if ( highlightedIndex >= 0 && contentRef . current ) {
370- const highlightedElement = contentRef . current . querySelector (
410+ if ( highlightedIndex >= 0 && popoverRef . current ) {
411+ const highlightedElement = popoverRef . current . querySelector (
371412 `[data-option-index="${ highlightedIndex } "]`
372413 )
373414 if ( highlightedElement ) {
@@ -425,18 +466,35 @@ export function OutputSelect({
425466 minWidth = { 160 }
426467 border
427468 disablePortal = { disablePopoverPortal }
428- onKeyDown = { handleKeyDown }
429- tabIndex = { 0 }
430- style = { { outline : 'none' } }
431469 >
432- < div ref = { contentRef } className = 'space-y-[2px]' >
470+ < div className = 'space-y-[2px]' >
433471 { Object . entries ( groupedOutputs ) . map ( ( [ blockName , outputs ] ) => {
434- // Calculate the starting index for this group
435472 const startIndex = flattenedOutputs . findIndex ( ( o ) => o . blockName === blockName )
436473
474+ const firstOutput = outputs [ 0 ]
475+ const blockConfig = getBlock ( firstOutput . blockType )
476+ const blockColor = getOutputColor ( firstOutput . blockId , firstOutput . blockType )
477+
478+ let blockIcon : string | React . ComponentType < { className ?: string } > = blockName
479+ . charAt ( 0 )
480+ . toUpperCase ( )
481+
482+ if ( blockConfig ?. icon ) {
483+ blockIcon = blockConfig . icon
484+ } else if ( firstOutput . blockType === 'loop' ) {
485+ blockIcon = RepeatIcon
486+ } else if ( firstOutput . blockType === 'parallel' ) {
487+ blockIcon = SplitIcon
488+ }
489+
437490 return (
438491 < div key = { blockName } >
439- < PopoverSection > { blockName } </ PopoverSection >
492+ < PopoverSection >
493+ < div className = 'flex items-center gap-1.5' >
494+ < TagIcon icon = { blockIcon } color = { blockColor } />
495+ < span > { blockName } </ span >
496+ </ div >
497+ </ PopoverSection >
440498
441499 < div className = 'flex flex-col gap-[2px]' >
442500 { outputs . map ( ( output , localIndex ) => {
@@ -451,17 +509,9 @@ export function OutputSelect({
451509 onClick = { ( ) => handleOutputSelection ( output . label ) }
452510 onMouseEnter = { ( ) => setHighlightedIndex ( globalIndex ) }
453511 >
454- < div
455- className = 'flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
456- style = { {
457- backgroundColor : getOutputColor ( output . blockId , output . blockType ) ,
458- } }
459- >
460- < span className = 'font-bold text-[10px] text-white' >
461- { blockName . charAt ( 0 ) . toUpperCase ( ) }
462- </ span >
463- </ div >
464- < span className = 'min-w-0 flex-1 truncate' > { output . path } </ span >
512+ < span className = 'min-w-0 flex-1 truncate text-[var(--text-primary)]' >
513+ { output . path }
514+ </ span >
465515 { isSelectedValue ( output ) && < Check className = 'h-3 w-3 flex-shrink-0' /> }
466516 </ PopoverItem >
467517 )
0 commit comments