22
33import { useCallback , useEffect , useMemo , useState } from 'react'
44import { createLogger } from '@sim/logger'
5- import { Check , ChevronDown , Clipboard , Plus , Search , X } from 'lucide-react'
5+ import { Check , Clipboard , Plus , Search , X } from 'lucide-react'
66import { useParams } from 'next/navigation'
77import {
88 Badge ,
@@ -16,11 +16,6 @@ import {
1616 ModalContent ,
1717 ModalFooter ,
1818 ModalHeader ,
19- Popover ,
20- PopoverContent ,
21- PopoverItem ,
22- PopoverScrollArea ,
23- PopoverTrigger ,
2419 Textarea ,
2520} from '@/components/emcn'
2621import { Input , Skeleton } from '@/components/ui'
@@ -50,8 +45,8 @@ interface WorkflowTagSelectProps {
5045}
5146
5247/**
53- * A tag-input style workflow selector with dropdown .
54- * Shows selected workflows as removable tags inside the input container .
48+ * Multi-select workflow selector using Combobox .
49+ * Shows selected workflows as removable badges inside the trigger .
5550 */
5651function WorkflowTagSelect ( {
5752 workflows,
@@ -60,124 +55,68 @@ function WorkflowTagSelect({
6055 isLoading = false ,
6156 disabled = false ,
6257} : WorkflowTagSelectProps ) {
63- const [ open , setOpen ] = useState ( false )
64- const [ searchQuery , setSearchQuery ] = useState ( '' )
58+ const options : ComboboxOption [ ] = useMemo ( ( ) => {
59+ return workflows . map ( ( w ) => ( {
60+ label : w . name ,
61+ value : w . id ,
62+ } ) )
63+ } , [ workflows ] )
6564
66- const availableWorkflows = useMemo ( ( ) => {
67- return workflows . filter ( ( w ) => ! selectedIds . includes ( w . id ) )
65+ const selectedWorkflows = useMemo ( ( ) => {
66+ return workflows . filter ( ( w ) => selectedIds . includes ( w . id ) )
6867 } , [ workflows , selectedIds ] )
6968
70- const filteredWorkflows = useMemo ( ( ) => {
71- if ( ! searchQuery . trim ( ) ) return availableWorkflows
72- const query = searchQuery . toLowerCase ( )
73- return availableWorkflows . filter ( ( w ) => w . name . toLowerCase ( ) . includes ( query ) )
74- } , [ availableWorkflows , searchQuery ] )
75-
76- const handleSelect = ( id : string ) => {
77- onSelectionChange ( [ ...selectedIds , id ] )
78- setSearchQuery ( '' )
69+ const handleRemove = ( e : React . MouseEvent , id : string ) => {
70+ e . preventDefault ( )
71+ e . stopPropagation ( )
72+ onSelectionChange ( selectedIds . filter ( ( i ) => i !== id ) )
7973 }
8074
81- const handleRemove = ( id : string ) => {
82- onSelectionChange ( selectedIds . filter ( ( selectedId ) => selectedId !== id ) )
83- }
75+ const overlayContent = useMemo ( ( ) => {
76+ if ( selectedWorkflows . length === 0 ) {
77+ return null
78+ }
79+
80+ return (
81+ < div className = 'flex items-center gap-[4px] overflow-hidden' >
82+ { selectedWorkflows . slice ( 0 , 2 ) . map ( ( w ) => (
83+ < Badge
84+ key = { w . id }
85+ variant = 'outline'
86+ className = 'pointer-events-auto cursor-pointer gap-[4px] rounded-[6px] px-[8px] py-[2px] text-[11px]'
87+ onMouseDown = { ( e ) => handleRemove ( e , w . id ) }
88+ >
89+ { w . name }
90+ < X className = 'h-3 w-3' />
91+ </ Badge >
92+ ) ) }
93+ { selectedWorkflows . length > 2 && (
94+ < Badge variant = 'outline' className = 'rounded-[6px] px-[8px] py-[2px] text-[11px]' >
95+ +{ selectedWorkflows . length - 2 }
96+ </ Badge >
97+ ) }
98+ </ div >
99+ )
100+ } , [ selectedWorkflows , selectedIds ] )
84101
85102 const isEmpty = workflows . length === 0
86103
104+ if ( isLoading ) {
105+ return < Skeleton className = 'h-[34px] w-full rounded-[6px]' />
106+ }
107+
87108 return (
88- < Popover open = { open && ! disabled && ! isEmpty } onOpenChange = { setOpen } >
89- < PopoverTrigger asChild >
90- < div
91- className = { `flex h-[36px] cursor-pointer items-center gap-[6px] overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] transition-colors hover:bg-[var(--surface-7)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)] ${
92- disabled ? 'cursor-not-allowed opacity-50' : ''
93- } `}
94- onClick = { ( ) => ! disabled && ! isEmpty && setOpen ( true ) }
95- >
96- { selectedIds . length === 0 ? (
97- < span className = 'text-[var(--text-muted)] text-sm' >
98- { isEmpty ? 'No deployed workflows available' : 'Select deployed workflows...' }
99- </ span >
100- ) : (
101- < div className = 'flex min-w-0 flex-1 items-center gap-[6px] overflow-hidden' >
102- { selectedIds . map ( ( id ) => {
103- const workflow = workflows . find ( ( w ) => w . id === id )
104- return (
105- < div
106- key = { id }
107- className = 'flex flex-shrink-0 items-center gap-[4px] rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-4)] px-[6px] py-[2px] text-[12px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
108- >
109- < span className = 'max-w-[150px] truncate' > { workflow ?. name || id } </ span >
110- < button
111- type = 'button'
112- onClick = { ( e ) => {
113- e . stopPropagation ( )
114- handleRemove ( id )
115- } }
116- className = 'flex-shrink-0 text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)] focus:outline-none'
117- aria-label = { `Remove ${ workflow ?. name || id } ` }
118- >
119- < X className = 'h-[12px] w-[12px] translate-y-[0.2px]' />
120- </ button >
121- </ div >
122- )
123- } ) }
124- </ div >
125- ) }
126- < ChevronDown
127- className = { `ml-auto h-4 w-4 flex-shrink-0 opacity-50 transition-transform ${ open ? 'rotate-180' : '' } ` }
128- />
129- </ div >
130- </ PopoverTrigger >
131- < PopoverContent
132- side = 'bottom'
133- align = 'start'
134- sideOffset = { 4 }
135- className = 'w-[var(--radix-popover-trigger-width)] rounded-[6px] border border-[var(--border-1)] p-0'
136- >
137- < div className = 'flex items-center px-[10px] pt-[8px] pb-[4px]' >
138- < Search className = 'mr-[7px] ml-[1px] h-[13px] w-[13px] shrink-0 text-[var(--text-muted)]' />
139- < input
140- className = 'w-full bg-transparent font-base text-[13px] text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:outline-none'
141- placeholder = 'Search workflows...'
142- value = { searchQuery }
143- onChange = { ( e ) => setSearchQuery ( e . target . value ) }
144- onKeyDown = { ( e ) => {
145- if ( e . key === 'Escape' ) {
146- setOpen ( false )
147- setSearchQuery ( '' )
148- }
149- } }
150- />
151- </ div >
152- < PopoverScrollArea className = '!flex-none p-[4px]' style = { { maxHeight : '192px' } } >
153- { isLoading ? (
154- < div className = 'flex items-center justify-center py-[14px]' >
155- < span className = 'font-base text-[12px] text-[var(--text-muted)]' > Loading...</ span >
156- </ div >
157- ) : filteredWorkflows . length === 0 ? (
158- < div className = 'py-[14px] text-center font-base text-[12px] text-[var(--text-muted)]' >
159- { searchQuery
160- ? 'No matching workflows found'
161- : availableWorkflows . length === 0
162- ? 'All workflows have been added'
163- : 'No deployed workflows found' }
164- </ div >
165- ) : (
166- < div className = 'space-y-[2px]' >
167- { filteredWorkflows . map ( ( workflow ) => (
168- < PopoverItem
169- key = { workflow . id }
170- onClick = { ( ) => handleSelect ( workflow . id ) }
171- className = 'cursor-pointer rounded-[4px] px-[6px] py-[6px] font-medium font-sans text-sm hover:bg-[var(--border-1)]'
172- >
173- < span className = 'truncate text-[var(--text-primary)]' > { workflow . name } </ span >
174- </ PopoverItem >
175- ) ) }
176- </ div >
177- ) }
178- </ PopoverScrollArea >
179- </ PopoverContent >
180- </ Popover >
109+ < Combobox
110+ options = { options }
111+ multiSelect
112+ multiSelectValues = { selectedIds }
113+ onMultiSelectChange = { onSelectionChange }
114+ placeholder = { isEmpty ? 'No deployed workflows available' : 'Select deployed workflows...' }
115+ overlayContent = { overlayContent }
116+ searchable
117+ searchPlaceholder = 'Search workflows...'
118+ disabled = { disabled || isEmpty }
119+ />
181120 )
182121}
183122
@@ -596,7 +535,6 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro
596535}
597536
598537interface WorkflowMcpServersProps {
599- /** Key that when changed resets the component to list view */
600538 resetKey ?: number
601539}
602540
@@ -623,7 +561,6 @@ export function WorkflowMcpServers({ resetKey }: WorkflowMcpServersProps) {
623561 const [ serverToDelete , setServerToDelete ] = useState < WorkflowMcpServer | null > ( null )
624562 const [ deletingServers , setDeletingServers ] = useState < Set < string > > ( new Set ( ) )
625563
626- // Reset to list view when resetKey changes (triggered by clicking sidebar item)
627564 useEffect ( ( ) => {
628565 if ( resetKey !== undefined ) {
629566 setSelectedServerId ( null )
@@ -651,7 +588,6 @@ export function WorkflowMcpServers({ resetKey }: WorkflowMcpServersProps) {
651588 name : formData . name . trim ( ) ,
652589 } )
653590
654- // Add selected workflows as tools
655591 if ( selectedWorkflowIds . length > 0 && server ?. id ) {
656592 await Promise . all (
657593 selectedWorkflowIds . map ( ( workflowId ) =>
0 commit comments