1- import { X } from 'lucide-react'
2- import { Button , Combobox , type ComboboxOption , Input } from '@/components/emcn'
1+ import { useRef } from 'react'
2+ import { Plus } from 'lucide-react'
3+ import {
4+ Badge ,
5+ Button ,
6+ Combobox ,
7+ type ComboboxOption ,
8+ Input ,
9+ Label ,
10+ Trash ,
11+ } from '@/components/emcn'
312import { cn } from '@/lib/core/utils/cn'
413import type { FilterRule } from '@/lib/table/query-builder/constants'
514import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
6- import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller'
15+ import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
16+ import type { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
717import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
818
919interface FilterRuleRowProps {
@@ -17,121 +27,196 @@ interface FilterRuleRowProps {
1727 isReadOnly : boolean
1828 isPreview : boolean
1929 disabled : boolean
30+ onAdd : ( ) => void
2031 onRemove : ( id : string ) => void
2132 onUpdate : ( id : string , field : keyof FilterRule , value : string ) => void
33+ onToggleCollapse : ( id : string ) => void
34+ inputController : ReturnType < typeof useSubBlockInput >
2235}
2336
2437export function FilterRuleRow ( {
2538 blockId,
26- subBlockId,
2739 rule,
2840 index,
2941 columns,
3042 comparisonOptions,
3143 logicalOptions,
3244 isReadOnly,
33- isPreview,
34- disabled,
45+ onAdd,
3546 onRemove,
3647 onUpdate,
48+ onToggleCollapse,
49+ inputController,
3750} : FilterRuleRowProps ) {
3851 const accessiblePrefixes = useAccessibleReferencePrefixes ( blockId )
52+ const valueInputRef = useRef < HTMLInputElement > ( null )
53+ const overlayRef = useRef < HTMLDivElement > ( null )
3954
40- return (
41- < div className = 'flex items-center gap-[6px]' >
42- < Button
43- variant = 'ghost'
44- size = 'sm'
45- onClick = { ( ) => onRemove ( rule . id ) }
55+ const syncOverlayScroll = ( scrollLeft : number ) => {
56+ if ( overlayRef . current ) overlayRef . current . scrollLeft = scrollLeft
57+ }
58+
59+ const cellKey = `filter-${ rule . id } -value`
60+ const fieldState = inputController . fieldHelpers . getFieldState ( cellKey )
61+ const handlers = inputController . fieldHelpers . createFieldHandlers (
62+ cellKey ,
63+ rule . value ,
64+ ( newValue ) => onUpdate ( rule . id , 'value' , newValue )
65+ )
66+ const tagSelectHandler = inputController . fieldHelpers . createTagSelectHandler (
67+ cellKey ,
68+ rule . value ,
69+ ( newValue ) => onUpdate ( rule . id , 'value' , newValue )
70+ )
71+
72+ const getOperatorLabel = ( value : string ) => {
73+ const option = comparisonOptions . find ( ( op ) => op . value === value )
74+ return option ?. label || value
75+ }
76+
77+ const getColumnLabel = ( value : string ) => {
78+ const option = columns . find ( ( col ) => col . value === value )
79+ return option ?. label || value
80+ }
81+
82+ const renderHeader = ( ) => (
83+ < div
84+ className = 'flex cursor-pointer items-center justify-between rounded-t-[4px] bg-[var(--surface-4)] px-[10px] py-[5px]'
85+ onClick = { ( ) => onToggleCollapse ( rule . id ) }
86+ >
87+ < div className = 'flex min-w-0 flex-1 items-center gap-[8px]' >
88+ < span className = 'block truncate font-medium text-[14px] text-[var(--text-tertiary)]' >
89+ { rule . collapsed && rule . column ? getColumnLabel ( rule . column ) : `Condition ${ index + 1 } ` }
90+ </ span >
91+ { rule . collapsed && rule . column && (
92+ < Badge variant = 'type' size = 'sm' >
93+ { getOperatorLabel ( rule . operator ) }
94+ </ Badge >
95+ ) }
96+ </ div >
97+ < div className = 'flex items-center gap-[8px] pl-[8px]' onClick = { ( e ) => e . stopPropagation ( ) } >
98+ < Button variant = 'ghost' onClick = { onAdd } disabled = { isReadOnly } className = 'h-auto p-0' >
99+ < Plus className = 'h-[14px] w-[14px]' />
100+ < span className = 'sr-only' > Add Condition</ span >
101+ </ Button >
102+ < Button
103+ variant = 'ghost'
104+ onClick = { ( ) => onRemove ( rule . id ) }
105+ disabled = { isReadOnly }
106+ className = 'h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
107+ >
108+ < Trash className = 'h-[14px] w-[14px]' />
109+ < span className = 'sr-only' > Delete Condition</ span >
110+ </ Button >
111+ </ div >
112+ </ div >
113+ )
114+
115+ const renderValueInput = ( ) => (
116+ < div className = 'relative' >
117+ < Input
118+ ref = { valueInputRef }
119+ value = { rule . value }
120+ onChange = { handlers . onChange }
121+ onKeyDown = { handlers . onKeyDown }
122+ onDrop = { handlers . onDrop }
123+ onDragOver = { handlers . onDragOver }
124+ onFocus = { handlers . onFocus }
125+ onScroll = { ( e ) => syncOverlayScroll ( e . currentTarget . scrollLeft ) }
126+ onPaste = { ( ) =>
127+ setTimeout ( ( ) => {
128+ if ( valueInputRef . current ) {
129+ syncOverlayScroll ( valueInputRef . current . scrollLeft )
130+ }
131+ } , 0 )
132+ }
46133 disabled = { isReadOnly }
47- className = 'h-[24px] w-[24px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
134+ autoComplete = 'off'
135+ placeholder = 'Enter value'
136+ className = 'allow-scroll w-full overflow-auto text-transparent caret-foreground'
137+ />
138+ < div
139+ ref = { overlayRef }
140+ className = { cn (
141+ 'absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm' ,
142+ ! isReadOnly && 'pointer-events-none'
143+ ) }
48144 >
49- < X className = 'h-[12px] w-[12px]' />
50- </ Button >
145+ < div className = 'w-full whitespace-pre' style = { { minWidth : 'fit-content' } } >
146+ { formatDisplayText (
147+ rule . value ,
148+ accessiblePrefixes ? { accessiblePrefixes } : { highlightAll : true }
149+ ) }
150+ </ div >
151+ </ div >
152+ { fieldState . showTags && (
153+ < TagDropdown
154+ visible = { fieldState . showTags }
155+ onSelect = { tagSelectHandler }
156+ blockId = { blockId }
157+ activeSourceBlockId = { fieldState . activeSourceBlockId }
158+ inputValue = { rule . value }
159+ cursorPosition = { fieldState . cursorPosition }
160+ onClose = { ( ) => inputController . fieldHelpers . hideFieldDropdowns ( cellKey ) }
161+ inputRef = { valueInputRef . current ? { current : valueInputRef . current } : undefined }
162+ />
163+ ) }
164+ </ div >
165+ )
51166
52- < div className = 'w-[80px] shrink-0' >
53- { index === 0 ? (
54- < Combobox
55- size = 'sm'
56- options = { [ { value : 'where' , label : 'where' } ] }
57- value = 'where'
58- disabled
59- />
60- ) : (
167+ const renderContent = ( ) => (
168+ < div className = 'flex flex-col gap-[8px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]' >
169+ { index > 0 && (
170+ < div className = 'flex flex-col gap-[6px]' >
171+ < Label className = 'text-[13px]' > Logic</ Label >
61172 < Combobox
62- size = 'sm'
63173 options = { logicalOptions }
64174 value = { rule . logicalOperator }
65175 onChange = { ( v ) => onUpdate ( rule . id , 'logicalOperator' , v as 'and' | 'or' ) }
66176 disabled = { isReadOnly }
67177 />
68- ) }
69- </ div >
178+ </ div >
179+ ) }
70180
71- < div className = 'w-[100px] shrink-0' >
181+ < div className = 'flex flex-col gap-[6px]' >
182+ < Label className = 'text-[13px]' > Column</ Label >
72183 < Combobox
73- size = 'sm'
74184 options = { columns }
75185 value = { rule . column }
76186 onChange = { ( v ) => onUpdate ( rule . id , 'column' , v ) }
77- placeholder = 'Column'
78187 disabled = { isReadOnly }
188+ placeholder = 'Select column'
79189 />
80190 </ div >
81191
82- < div className = 'w-[110px] shrink-0' >
192+ < div className = 'flex flex-col gap-[6px]' >
193+ < Label className = 'text-[13px]' > Operator</ Label >
83194 < Combobox
84- size = 'sm'
85195 options = { comparisonOptions }
86196 value = { rule . operator }
87197 onChange = { ( v ) => onUpdate ( rule . id , 'operator' , v ) }
88198 disabled = { isReadOnly }
199+ placeholder = 'Select operator'
89200 />
90201 </ div >
91202
92- < div className = 'relative min-w-[80px] flex-1' >
93- < SubBlockInputController
94- blockId = { blockId }
95- subBlockId = { `${ subBlockId } _filter_${ rule . id } ` }
96- config = { { id : `filter_value_${ rule . id } ` , type : 'short-input' } }
97- value = { rule . value }
98- onChange = { ( newValue ) => onUpdate ( rule . id , 'value' , newValue ) }
99- isPreview = { isPreview }
100- disabled = { disabled }
101- >
102- { ( { ref, value : ctrlValue , onChange, onKeyDown, onDrop, onDragOver } ) => {
103- const formattedText = formatDisplayText ( ctrlValue , {
104- accessiblePrefixes,
105- highlightAll : ! accessiblePrefixes ,
106- } )
107-
108- return (
109- < div className = 'relative' >
110- < Input
111- ref = { ref as React . RefObject < HTMLInputElement > }
112- className = 'h-[28px] w-full overflow-auto text-[12px] text-transparent caret-foreground [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground/50 [&::-webkit-scrollbar]:hidden'
113- value = { ctrlValue }
114- onChange = { onChange as ( e : React . ChangeEvent < HTMLInputElement > ) => void }
115- onKeyDown = { onKeyDown as ( e : React . KeyboardEvent < HTMLInputElement > ) => void }
116- onDrop = { onDrop as ( e : React . DragEvent < HTMLInputElement > ) => void }
117- onDragOver = { onDragOver as ( e : React . DragEvent < HTMLInputElement > ) => void }
118- placeholder = 'Value'
119- disabled = { isReadOnly }
120- autoComplete = 'off'
121- />
122- < div
123- className = { cn (
124- 'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-[12px] text-foreground [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden' ,
125- ( isPreview || disabled ) && 'opacity-50'
126- ) }
127- >
128- < div className = 'min-w-fit whitespace-pre' > { formattedText } </ div >
129- </ div >
130- </ div >
131- )
132- } }
133- </ SubBlockInputController >
203+ < div className = 'flex flex-col gap-[6px]' >
204+ < Label className = 'text-[13px]' > Value</ Label >
205+ { renderValueInput ( ) }
134206 </ div >
135207 </ div >
136208 )
209+
210+ return (
211+ < div
212+ data-filter-id = { rule . id }
213+ className = { cn (
214+ 'rounded-[4px] border border-[var(--border-1)]' ,
215+ rule . collapsed ? 'overflow-hidden' : 'overflow-visible'
216+ ) }
217+ >
218+ { renderHeader ( ) }
219+ { ! rule . collapsed && renderContent ( ) }
220+ </ div >
221+ )
137222}
0 commit comments