2026-11-12 09:59:00 -08:00
// CHAKRA COMPONENTS
import {
Button ,
Stack ,
Flex ,
Field ,
NativeSelect ,
Input ,
HoverCard ,
Text ,
NumberInput ,
Box ,
useSlotRecipe ,
Combobox ,
Portal ,
useFilter ,
useListCollection
} from "@chakra-ui/react" ;
// ICONS
import {
Plus ,
CircleMinus ,
Files ,
ClipboardPaste ,
Trash
} from 'lucide-react' ;
// OTHER
import { useState } from 'react' ;
import useDataFilterStore from '../../store/useDataFilterStore.jsx' ;
// ============================================================================
// 1. MAIN QUERY BUILDER (The Engine Wrapper)
// ============================================================================
export default function MyQueryBuilder ({ url , setUrl , allColumns , getColumns , pullTable , isLoading , isReadyRef }) {
// STYLING
const dataExplorerRecipe = useSlotRecipe ({ key : "dataExplorer" });
const dataExplorerStyle = dataExplorerRecipe ();
// THE SELECT DROPDOWN MENU
const { contains } = useFilter ({ sensitivity : "base" })
const { collection , filter } = useListCollection ({
initialItems : tables ,
filter : contains ,
})
// STATE MANAGEMENT
const [ value , setValue ] = useState ([]);
const addNewGroup = useDataFilterStore ( state => state . addNewGroup );
const filterGroups = useDataFilterStore ( state => state . filterGroups );
console . log ( "filter groups" , filterGroups );
return (
< Flex direction = "column" gap = { 2 } w = "100%" bg = "transparent" css = { dataExplorerStyle . query_builder }>
{ /* Top Controls: Source & Execute */ }
< Flex gap = { 2 } align = "flex-end" >
< Combobox.Root
collection = { collection }
onInputValueChange = {( e ) => filter ( e . inputValue )}
value = { value }
onValueChange = {( e ) => {
setValue ( e . value );
const selectedView = e . value ;
setUrl ( prev => ({ ... prev , source : selectedView }));
// need to get all columns as default in param building
const endpoint = "/api/v3/view_dash_event_search_columns" ;
getColumns ( endpoint );
}}
width = "320px"
>
< Combobox.Label > Select Table </ Combobox.Label >
< Combobox.Control >
< Combobox.Input placeholder = "Type to search" />
< Combobox.IndicatorGroup >
< Combobox.ClearTrigger />
< Combobox.Trigger />
</ Combobox.IndicatorGroup >
</ Combobox.Control >
< Portal >
< Combobox.Positioner >
< Combobox.Content >
< Combobox.Empty > No items found </ Combobox.Empty >
{ collection . items . map (( item ) => (
< Combobox.Item item = { item } key = { item . value }>
{ item . label }
< Combobox.ItemIndicator />
</ Combobox.Item >
))}
</ Combobox.Content >
</ Combobox.Positioner >
</ Portal >
</ Combobox.Root >
< Button
loading = { isLoading }
onClick = {() => {
isReadyRef . current = true ;
pullTable ();
}}
>
Execute Search
</ Button >
</ Flex >
{ /* Filter Builder Engine */ }
{ url . source && allColumns ? . types && (
< Stack w = "100%" gap = "2" >
< Text fontSize = "sm" fontWeight = "bold" color = "text" > Filters </ Text >
< Box w = "100%" maxH = "400px" overflow = "auto" >
< Stack w = "100%" gap = "2" pr = "4" >
{ filterGroups . map (( group ) => (
< FilterGroup key = { group . id } groupID = { group . id } filters = { group . filters } columns = { allColumns . types } />
))}
< Button onClick = {() => addNewGroup ()} variant = "outline" borderColor = "widgetBadgeBg" color = "text" _hover = {{ bg : "widgetBadgeBg" }}>
New Filter Group < Plus />
</ Button >
</ Stack >
</ Box >
</ Stack >
)}
</ Flex >
);
}
// LIST OF TABLES USER CAN EXPLORE
const tables = [
{ label : "Events Table" , value : "ViewDashEventSearch" },
];
// ============================================================================
// 2. FILTERS GROUPS (The AND logic groups)
// ============================================================================
function FilterGroup ({ groupID , filters , columns }) {
const removeGroup = useDataFilterStore ( state => state . removeGroup );
const addFilter = useDataFilterStore ( state => state . addFilter );
console . log ( "in filter group. GroupID: " , groupID , "filters: " , filters );
return (
< Stack bg = "widgetItemBg" borderWidth = "1px" borderColor = "widgetBadgeBg" borderRadius = "lg" p = "3" spacing = "3" >
< Flex justify = "space-between" align = "center" >
< Input placeholder = "Group Name (Optional)" maxW = "200px" size = "sm" variant = "flushed" color = "text" />
< Button onClick = {() => removeGroup ( groupID )} variant = "ghost" size = "sm" color = "red.400" >
< Trash />
</ Button >
</ Flex >
< Stack spacing = "2" >
{ filters ? . map (( filter ) => (
2026-11-12 10:23:44 -08:00
< FilterRow key = { filter . id } groupID = { groupID } filterID = { filter . id } filter = { filter } columns = { columns } />
2026-11-12 09:59:00 -08:00
))}
< Button onClick = {() => addFilter ( groupID )} variant = "outline" size = "sm" color = "text" borderColor = "widgetBadgeBg" _hover = {{ bg : "widgetBadgeBg" }}>
Add Filter < Plus />
</ Button >
</ Stack >
</ Stack >
);
};
// ============================================================================
// 3. FILTER ROW (The individual criteria inputs)
// ============================================================================
function FilterRow ({ groupID , filterID , filter , columns }) {
// STATE MANAGEMENT
const removeFilter = useDataFilterStore ( state => state . removeFilter );
const updateFilterColumn = useDataFilterStore ( state => state . updateFilterColumn );
const updateFilterOp = useDataFilterStore ( state => state . updateFilterOps );
const updateFilterValue = useDataFilterStore ( state => state . updateFilterValue );
const copyFilter = useDataFilterStore ( state => state . copyFilter );
const pasteFilter = useDataFilterStore ( state => state . pasteFilter );
// CONSTANTS
const types = {
AutoField : [ "=" , "<" , ">" , "<=" , ">=" ],
IntegerField : [ "=" , "<" , ">" , "<=" , ">=" ],
CharField : [ "=" , "contains" ],
DateTimeField : [ "range" ],
};
return (
< Flex gap = "2" align = "center" >
{ /* Remove button */ }
< HoverCard.Root positioning = {{ placement : "top" }}>
< HoverCard.Trigger asChild >
< Button onClick = {() => removeFilter ( groupID , filterID )} variant = "ghost" size = "sm" color = "red.400" >
< CircleMinus />
</ Button >
</ HoverCard.Trigger >
< HoverCard.Positioner >
< HoverCard.Content p = "2" bg = "white" >< Text fontSize = "sm" > Remove Filter </ Text ></ HoverCard.Content >
</ HoverCard.Positioner >
</ HoverCard.Root >
{ /* Copy Button */ }
< HoverCard.Root positioning = {{ placement : "top" }}>
< HoverCard.Trigger asChild >
< Button onClick = {() => copyFilter ( filter )} variant = "ghost" size = "sm" color = "text" >
< Files />
</ Button >
</ HoverCard.Trigger >
< HoverCard.Positioner >
< HoverCard.Content p = "2" bg = "white" >< Text fontSize = "sm" > Copy Filter </ Text ></ HoverCard.Content >
</ HoverCard.Positioner >
</ HoverCard.Root >
{ /* paste button */ }
< HoverCard.Root positioning = {{ placement : "top" }}>
< HoverCard.Trigger asChild >
< Button
2026-11-12 10:23:44 -08:00
onClick = {() => pasteFilter ( groupID , filterID )}
2026-11-12 09:59:00 -08:00
variant = "ghost"
size = "sm"
color = "text"
>
< ClipboardPaste />
</ Button >
</ HoverCard.Trigger >
< HoverCard.Positioner >
< HoverCard.Content p = "2" bg = "white" >< Text fontSize = "sm" > Paste Filter </ Text ></ HoverCard.Content >
</ HoverCard.Positioner >
</ HoverCard.Root >
{ /* Column selector */ }
< Field.Root w = { filter ? . type === "DateTimeField" ? "30%" : "30%" }>
< NativeSelect.Root >
< NativeSelect.Field
bg = "widgetItemBg"
color = "text"
value = { filter . col ?? "" }
onChange = {( e ) => updateFilterColumn ( groupID , filterID , e . target . value , columns [ e . target . value ], null , null )} // e.target.value is the new column
>
< option value = "" disabled hidden > Select Column </ option >
{ Object . keys ( columns ). map (( colName ) => (
< option key = { colName } value = { colName }>{ colName }</ option >
))}
</ NativeSelect.Field >
< NativeSelect.Indicator />
</ NativeSelect.Root >
</ Field.Root >
{ /* Operator selector or start date for range */ }
< Field.Root w = { filter ? . type === "DateTimeField" ? "20%" : "20%" }>
{ filter ? . type === "DateTimeField" ? (
< Input type = "date" bg = "widgetItemBg" color = "text" value = { filter . value ? . start || "" } onChange = {( e ) => updateFilterValue ( groupID , filterID , e . target . value , "s" )} />
) : (
< NativeSelect.Root >
< NativeSelect.Field bg = "widgetItemBg" color = "text" value = { filter . op ?? "" } onChange = {( e ) => updateFilterOp ( groupID , filterID , e . target . value )}>
< option value = "" disabled hidden > Operator </ option >
{ filter ? . type && types [ filter ? . type ]. map (( op ) => < option key = { op } value = { op }>{ op }</ option >)} { /* maps over the 'filter.type' corresponding ops from the types dictionary */ }
</ NativeSelect.Field >
< NativeSelect.Indicator />
</ NativeSelect.Root >
)}
</ Field.Root >
{ /* Value input or end date for range */ }
< Field.Root w = { filter . type === "DateTimeField" ? "20%" : "50%" }>
{ filter ? . type === "DateTimeField" ? (
< Input type = "date" bg = "widgetItemBg" color = "text" value = { filter . value ? . end || "" } onChange = {( e ) => updateFilterValue ( groupID , filterID , e . target . value , "e" )} />
) : filter ? . type === "IntegerField" || filter ? . type === "AutoField" ? (
< NumberInput.Root value = { filter . value ?? "" } onValueChange = {( e ) => updateFilterValue ( groupID , filterID , e . value )}>
< NumberInput.Input placeholder = "Value" bg = "widgetItemBg" color = "text" />
</ NumberInput.Root >
) : (
< Input placeholder = "Value" bg = "widgetItemBg" color = "text" value = { filter . value ?? "" } onChange = {( e ) => updateFilterValue ( groupID , filterID , e . target . value )} />
)}
</ Field.Root >
</ Flex >
);
};