@@ -40,9 +40,17 @@ type Stage =
4040 | { step : 'operator' ; column : string }
4141 | { step : 'value' ; column : string ; ref : Filter }
4242
43- // Unified filter field: chips live inline in an input-styled container; typing
44- // suggests column filters or sends the text to AI. Backspace on an empty input
45- // removes the last chip.
43+ function splitParts ( value : string ) {
44+ return value
45+ . split ( ',' )
46+ . map ( part => part . trim ( ) )
47+ . filter ( part => part !== '' )
48+ }
49+
50+ function operatorMatches ( filter : Filter , text : string ) {
51+ return filter . label . toLowerCase ( ) . includes ( text ) || filter . operator . toLowerCase ( ) . includes ( text )
52+ }
53+
4654export function FilterSearchBar ( { table, schema } : { table : string ; schema : string } ) {
4755 const isOnline = useSubscription ( appStore , { selector : state => state . isOnline } )
4856 const { connectionResource } = useRouteContext ( )
@@ -52,25 +60,21 @@ export function FilterSearchBar({ table, schema }: { table: string; schema: stri
5260 const query = useSubscription ( store , { selector : state => state . prompt } )
5361 const [ isFocused , setIsFocused ] = useState ( false )
5462 const [ stage , setStage ] = useState < Stage > ( { step : 'idle' } )
55- // cmdk keeps its highlight by item value; when the stage swaps the whole item
56- // list the old value no longer exists and nothing is highlighted — so we
57- // control it and point it at the first item on every transition
5863 const [ highlighted , setHighlighted ] = useState ( '' )
5964 const [ freeAiUsage , setFreeAiUsage ] = useState < {
6065 remaining : number
6166 max : number
6267 } | null > ( null )
6368
64- const setQuery = ( value : string ) => {
69+ const setPrompt = ( value : string ) =>
6570 store . set ( state => ( { ...state , prompt : value } ) satisfies typeof state )
6671
72+ const setQuery = ( value : string ) => {
73+ setPrompt ( value )
74+
6775 const trimmed = value . trim ( ) . toLowerCase ( )
6876 if ( stage . step === 'operator' ) {
69- const first = SQL_FILTERS_LIST . find (
70- filter =>
71- filter . label . toLowerCase ( ) . includes ( trimmed ) ||
72- filter . operator . toLowerCase ( ) . includes ( trimmed ) ,
73- )
77+ const first = SQL_FILTERS_LIST . find ( filter => operatorMatches ( filter , trimmed ) )
7478 setHighlighted ( first ? `operator:${ first . operator . toLowerCase ( ) } ` : '' )
7579 } else if ( stage . step === 'value' ) {
7680 setHighlighted ( 'apply-value' )
@@ -82,6 +86,12 @@ export function FilterSearchBar({ table, schema }: { table: string; schema: stri
8286 const setFilters = ( updater : ( filters : ActiveFilter [ ] ) => ActiveFilter [ ] ) =>
8387 store . set ( state => ( { ...state , filters : updater ( state . filters ) } ) satisfies typeof state )
8488
89+ const resetStage = ( ) => {
90+ setStage ( { step : 'idle' } )
91+ setHighlighted ( '' )
92+ setPrompt ( '' )
93+ }
94+
8595 const { columns } = useTableColumnsContext ( )
8696 const { data : enums } = useQuery ( resourceEnumsQueryOptions ( { connectionResource } ) )
8797
@@ -104,7 +114,6 @@ export function FilterSearchBar({ table, schema }: { table: string; schema: stri
104114 values : filter . values ,
105115 } ) satisfies Omit < ActiveFilter , 'ref' > & { ref ?: ActiveFilter [ 'ref' ] } ,
106116 )
107- // For future updates if we'll have new filters
108117 . filter ( f => ! ! f . ref ) as ActiveFilter [ ] ,
109118 } ) satisfies typeof state ,
110119 )
@@ -166,7 +175,7 @@ export function FilterSearchBar({ table, schema }: { table: string; schema: stri
166175
167176 const pickColumn = ( columnId : string ) => {
168177 setStage ( { step : 'operator' , column : columnId } )
169- store . set ( state => ( { ... state , prompt : '' } ) satisfies typeof state )
178+ setPrompt ( '' )
170179 setHighlighted ( `operator:${ SQL_FILTERS_LIST [ 0 ] ! . operator . toLowerCase ( ) } ` )
171180 inputRef . current ?. focus ( )
172181 }
@@ -175,45 +184,29 @@ export function FilterSearchBar({ table, schema }: { table: string; schema: stri
175184 if ( stage . step !== 'operator' ) return
176185 if ( ref . hasValue === false ) {
177186 setFilters ( current => [ ...current , { column : stage . column , ref, values : [ ] } ] )
178- setStage ( { step : 'idle' } )
179- setHighlighted ( '' )
187+ resetStage ( )
180188 } else {
181189 setStage ( { step : 'value' , column : stage . column , ref } )
182190 setHighlighted ( 'apply-value' )
191+ setPrompt ( '' )
183192 }
184- store . set ( state => ( { ...state , prompt : '' } ) satisfies typeof state )
185193 inputRef . current ?. focus ( )
186194 }
187195
188196 const applyValue = ( ) => {
189197 if ( stage . step !== 'value' ) return
190- const values = stage . ref . isArray
191- ? query
192- . split ( ',' )
193- . map ( value => value . trim ( ) )
194- . filter ( value => value !== '' )
195- : [ query ]
198+ const values = stage . ref . isArray ? splitParts ( query ) : [ query ]
196199 setFilters ( current => [ ...current , { column : stage . column , ref : stage . ref , values } ] )
197- setStage ( { step : 'idle' } )
198- setHighlighted ( '' )
199- store . set ( state => ( { ...state , prompt : '' } ) satisfies typeof state )
200+ resetStage ( )
200201 inputRef . current ?. focus ( )
201202 }
202203
203- // Suggested values for the value stage — enum members, or true/false for
204- // boolean columns
205204 const stageColumn =
206205 stage . step === 'value' ? columns ?. find ( column => column . id === stage . column ) : undefined
207206 const suggestedValues =
208207 stageColumn ?. availableValues ??
209208 ( stageColumn ?. uiType === 'boolean' ? [ 'true' , 'false' ] : undefined )
210- const committedParts =
211- stage . step === 'value' && stage . ref . isArray
212- ? query
213- . split ( ',' )
214- . map ( part => part . trim ( ) )
215- . filter ( part => part !== '' )
216- : [ ]
209+ const committedParts = stage . step === 'value' && stage . ref . isArray ? splitParts ( query ) : [ ]
217210 const valueFilterText = (
218211 stage . step === 'value' && stage . ref . isArray ? ( query . split ( ',' ) . at ( - 1 ) ?? '' ) : query
219212 ) . trim ( )
@@ -224,29 +217,21 @@ export function FilterSearchBar({ table, schema }: { table: string; schema: stri
224217 const pickSuggestedValue = ( value : string ) => {
225218 if ( stage . step !== 'value' ) return
226219 if ( stage . ref . isArray ) {
227- // Multi-value operators collect picks into the comma list (click again to
228- // remove); Enter on Apply commits the whole list
229220 const committed = valueFilterText ? committedParts . slice ( 0 , - 1 ) : committedParts
230221 const next = committed . includes ( value )
231222 ? committed . filter ( part => part !== value )
232223 : [ ...committed , value ]
233224 setQuery ( next . join ( ', ' ) )
234225 } else {
235226 setFilters ( current => [ ...current , { column : stage . column , ref : stage . ref , values : [ value ] } ] )
236- setStage ( { step : 'idle' } )
237- setHighlighted ( '' )
238- store . set ( state => ( { ...state , prompt : '' } ) satisfies typeof state )
227+ resetStage ( )
239228 }
240229 inputRef . current ?. focus ( )
241230 }
242231
243232 const matchingOperators = SQL_FILTERS_GROUPED . map ( group => ( {
244233 ...group ,
245- filters : group . filters . filter (
246- filter =>
247- filter . label . toLowerCase ( ) . includes ( trimmedQuery . toLowerCase ( ) ) ||
248- filter . operator . toLowerCase ( ) . includes ( trimmedQuery . toLowerCase ( ) ) ,
249- ) ,
234+ filters : group . filters . filter ( filter => operatorMatches ( filter , trimmedQuery . toLowerCase ( ) ) ) ,
250235 } ) ) . filter ( group => group . filters . length > 0 )
251236
252237 const placeholder =
@@ -374,7 +359,6 @@ export function FilterSearchBar({ table, schema }: { table: string; schema: stri
374359 absolute bottom-full left-0 z-30 mb-2 w-full overflow-hidden
375360 rounded-xl bg-popover p-1 shadow-lg ring-1 ring-foreground/4
376361 "
377- // Keep the input focused while clicking suggestions
378362 onMouseDown = { e => e . preventDefault ( ) }
379363 >
380364 < CommandList className = "max-h-64" >
0 commit comments