@@ -1141,6 +1141,136 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
11411141 { max : 50 }
11421142 ) ;
11431143
1144+ /**
1145+ * Render an advanced filter menu element for a column
1146+ * @param columnIndex The visible column index (used as key and for handlers)
1147+ * @param modelColumn The model column index (used for filter/sort lookup)
1148+ * @param style CSS positioning for the menu container
1149+ * @param isShown Whether the menu is currently shown
1150+ * @returns The advanced filter menu element, or null if column not found
1151+ */
1152+ renderAdvancedFilterMenu (
1153+ columnIndex : VisibleIndex ,
1154+ modelColumn : ModelIndex ,
1155+ style : CSSProperties ,
1156+ isShown : boolean
1157+ ) : ReactElement | null {
1158+ const { model } = this . props ;
1159+ const { advancedFilters, formatter } = this . state ;
1160+
1161+ const column = model . columns [ modelColumn ] ;
1162+ if ( column == null ) {
1163+ log . warn (
1164+ `Column does not exist at index ${ modelColumn } for column array of length ${ model . columns . length } `
1165+ ) ;
1166+ return null ;
1167+ }
1168+
1169+ const advancedFilter = advancedFilters . get ( modelColumn ) ;
1170+ const { options : advancedFilterOptions } = advancedFilter || { } ;
1171+ const sort = TableUtils . getSortForColumn ( model . sort , column . name ) ;
1172+ const sortDirection = sort ? sort . direction : null ;
1173+
1174+ if ( ! isSortDirection ( sortDirection ) ) {
1175+ throw new Error ( `Invalid sort direction: ${ sortDirection } ` ) ;
1176+ }
1177+
1178+ return (
1179+ < div
1180+ key = { columnIndex }
1181+ className = "advanced-filter-menu-container"
1182+ style = { style }
1183+ >
1184+ < Popper
1185+ className = "advanced-filter-menu-popper"
1186+ onEntered = { this . getAdvancedMenuOpenedHandler ( columnIndex ) }
1187+ onExited = { ( ) => {
1188+ this . handleAdvancedMenuClosed ( columnIndex ) ;
1189+ } }
1190+ isShown = { isShown }
1191+ interactive
1192+ closeOnBlur
1193+ options = { {
1194+ positionFixed : true ,
1195+ } }
1196+ >
1197+ { this . getCachedAdvancedFilterMenuActions (
1198+ model ,
1199+ column ,
1200+ advancedFilterOptions ,
1201+ sortDirection ,
1202+ formatter
1203+ ) }
1204+ </ Popper >
1205+ </ div >
1206+ ) ;
1207+ }
1208+
1209+ /**
1210+ * Renders the advanced filter button for a column in the filter bar.
1211+ * @param columnIndex The visible column index
1212+ * @param modelColumn The model column index
1213+ * @param buttonCoordinates The x,y coordinates for the button
1214+ * @returns The filter button element or null if not visible
1215+ */
1216+ renderAdvancedFilterButton (
1217+ columnIndex : VisibleIndex ,
1218+ modelColumn : ModelIndex ,
1219+ buttonCoordinates : { x : number ; y : number }
1220+ ) : ReactElement | null {
1221+ const { advancedFilters, hoverAdvancedFilter, focusedFilterBarColumn } =
1222+ this . state ;
1223+ const advancedFilter = advancedFilters . get ( modelColumn ) ;
1224+ const isFilterSet = advancedFilter != null ;
1225+ const isFilterVisible =
1226+ columnIndex === hoverAdvancedFilter ||
1227+ columnIndex === focusedFilterBarColumn ||
1228+ isFilterSet ;
1229+
1230+ if ( ! isFilterVisible ) {
1231+ return null ;
1232+ }
1233+
1234+ const { x, y } = buttonCoordinates ;
1235+ const style : CSSProperties = {
1236+ position : 'absolute' ,
1237+ top : y ,
1238+ left : x ,
1239+ } ;
1240+
1241+ return (
1242+ < div
1243+ className = "advanced-filter-button-container"
1244+ key = { columnIndex }
1245+ style = { style }
1246+ >
1247+ < Button
1248+ kind = "ghost"
1249+ className = { classNames ( 'btn-link-icon advanced-filter-button' , {
1250+ 'filter-set' : isFilterSet ,
1251+ } ) }
1252+ onClick = { ( ) => {
1253+ this . setState ( { shownAdvancedFilter : columnIndex } ) ;
1254+ } }
1255+ onContextMenu = { event => {
1256+ this . grid ?. handleContextMenu ( event ) ;
1257+ } }
1258+ onMouseEnter = { ( ) => {
1259+ this . setState ( { hoverAdvancedFilter : columnIndex } ) ;
1260+ } }
1261+ onMouseLeave = { ( ) => {
1262+ this . setState ( { hoverAdvancedFilter : null } ) ;
1263+ } }
1264+ >
1265+ < div className = "fa-layers" >
1266+ < FontAwesomeIcon icon = { dhFilterFilled } className = "filter-solid" />
1267+ < FontAwesomeIcon icon = { vsFilter } className = "filter-light" />
1268+ </ div >
1269+ </ Button >
1270+ </ div >
1271+ ) ;
1272+ }
1273+
11441274 getCachedOptionItems = memoize (
11451275 (
11461276 isChartBuilderAvailable : boolean ,
@@ -2506,16 +2636,19 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
25062636 }
25072637
25082638 focusFilterBar ( column : VisibleIndex ) : void {
2509- const { movedColumns } = this . state ;
25102639 const { model } = this . props ;
25112640 const { columnCount } = model ;
2512- const modelColumn = GridUtils . getModelIndex ( column , movedColumns ) ;
2641+ const modelColumn = this . getModelColumn ( column ) ;
25132642
2514- if (
2643+ // Negative indexes are valid as long as they have a model column
2644+ const isOutOfBounds = column >= 0 && columnCount <= column ;
2645+ const isInvalid =
25152646 column == null ||
2516- columnCount <= column ||
2517- ! model . isFilterable ( modelColumn )
2518- ) {
2647+ isOutOfBounds ||
2648+ modelColumn == null ||
2649+ ! model . isFilterable ( modelColumn ) ;
2650+
2651+ if ( isInvalid ) {
25192652 this . setState ( { focusedFilterBarColumn : null } ) ;
25202653 return ;
25212654 }
@@ -4636,7 +4769,6 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
46364769 loadingCancelShown,
46374770 loadingBlocksGrid,
46384771 shownColumnTooltip,
4639- hoverAdvancedFilter,
46404772 shownAdvancedFilter,
46414773 hoverSelectColumn,
46424774 quickFilters,
@@ -4793,7 +4925,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
47934925 if ( metrics && isFilterBarShown ) {
47944926 const metricState = this . getMetricState ( ) ;
47954927
4796- // Advanced Filter buttons
4928+ // Advanced Filter buttons for visible columns
47974929 const { visibleColumns } = metrics ;
47984930
47994931 for ( let i = 0 ; i < visibleColumns . length ; i += 1 ) {
@@ -4811,66 +4943,37 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
48114943 )
48124944 : null ;
48134945 if ( buttonCoordinates != null ) {
4814- const { x, y } = buttonCoordinates ;
4815- const style : CSSProperties = {
4816- position : 'absolute' ,
4817- top : y ,
4818- left : x ,
4819- } ;
4820- const advancedFilter = advancedFilters . get ( modelColumn ) ;
4821- const isFilterSet = advancedFilter != null ;
4822- const isFilterVisible =
4823- columnIndex === hoverAdvancedFilter ||
4824- columnIndex === focusedFilterBarColumn ||
4825- isFilterSet ;
4826- const element = (
4827- < div
4828- className = { classNames ( 'advanced-filter-button-container' , {
4829- hidden : ! isFilterVisible ,
4830- } ) }
4831- key = { columnIndex }
4832- style = { style }
4833- >
4834- { isFilterVisible && (
4835- < Button
4836- kind = "ghost"
4837- className = { classNames (
4838- 'btn-link-icon advanced-filter-button' ,
4839- {
4840- 'filter-set' : isFilterSet ,
4841- }
4842- ) }
4843- onClick = { ( ) => {
4844- this . setState ( { shownAdvancedFilter : columnIndex } ) ;
4845- } }
4846- onContextMenu = { event => {
4847- this . grid ?. handleContextMenu ( event ) ;
4848- } }
4849- onMouseEnter = { ( ) => {
4850- this . setState ( { hoverAdvancedFilter : columnIndex } ) ;
4851- } }
4852- onMouseLeave = { ( ) => {
4853- this . setState ( { hoverAdvancedFilter : null } ) ;
4854- } }
4855- >
4856- < div className = "fa-layers" >
4857- < FontAwesomeIcon
4858- icon = { dhFilterFilled }
4859- className = "filter-solid"
4860- />
4861- < FontAwesomeIcon
4862- icon = { vsFilter }
4863- className = "filter-light"
4864- />
4865- </ div >
4866- </ Button >
4867- ) }
4868- </ div >
4946+ filterBar . push (
4947+ this . renderAdvancedFilterButton (
4948+ columnIndex ,
4949+ modelColumn ,
4950+ buttonCoordinates
4951+ )
48694952 ) ;
4870- filterBar . push ( element ) ;
48714953 }
48724954 }
48734955 }
4956+
4957+ // Advanced filter buttons for columns at negative indexes
4958+ // Models can expose columns at negative indexes (e.g., model.columns[-1])
4959+ for ( let i = - 1 ; model . columns [ i ] != null ; i -= 1 ) {
4960+ if ( ! model . isFilterable ( i ) ) {
4961+ // eslint-disable-next-line no-continue
4962+ continue ;
4963+ }
4964+ const buttonCoordinates = metricState
4965+ ? metricCalculator . getAdvancedFilterButtonCoordinates (
4966+ i ,
4967+ metricState ,
4968+ metrics
4969+ )
4970+ : null ;
4971+ if ( buttonCoordinates != null ) {
4972+ filterBar . push (
4973+ this . renderAdvancedFilterButton ( i , i , buttonCoordinates )
4974+ ) ;
4975+ }
4976+ }
48744977 }
48754978 const advancedFilterMenus = [ ] ;
48764979 if ( metrics ) {
@@ -4905,53 +5008,51 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
49055008 } ;
49065009 const modelColumn = this . getModelColumn ( columnIndex ) ;
49075010 if ( modelColumn != null ) {
4908- const column = model . columns [ modelColumn ] ;
4909- if ( column == null ) {
4910- // Grid metrics is likely out of sync with model
4911- log . warn (
4912- `Column does not exist at index ${ modelColumn } for column array of length ${ model . columns . length } `
4913- ) ;
4914- // eslint-disable-next-line no-continue
4915- continue ;
4916- }
4917- const advancedFilter = advancedFilters . get ( modelColumn ) ;
4918- const { options : advancedFilterOptions } = advancedFilter || { } ;
4919- const sort = TableUtils . getSortForColumn ( model . sort , column . name ) ;
4920-
4921- const sortDirection = sort ? sort . direction : null ;
4922- if ( ! isSortDirection ( sortDirection ) ) {
4923- throw new Error ( `Invalid sort direction: ${ sortDirection } ` ) ;
5011+ const element = this . renderAdvancedFilterMenu (
5012+ columnIndex ,
5013+ modelColumn ,
5014+ style ,
5015+ shownAdvancedFilter === columnIndex
5016+ ) ;
5017+ if ( element != null ) {
5018+ advancedFilterMenus . push ( element ) ;
49245019 }
5020+ }
5021+ }
5022+ }
49255023
4926- const element = (
4927- < div
4928- key = { columnIndex }
4929- className = "advanced-filter-menu-container"
4930- style = { style }
4931- >
4932- < Popper
4933- className = "advanced-filter-menu-popper"
4934- onEntered = { this . getAdvancedMenuOpenedHandler ( columnIndex ) }
4935- onExited = { ( ) => {
4936- this . handleAdvancedMenuClosed ( columnIndex ) ;
4937- } }
4938- isShown = { shownAdvancedFilter === columnIndex }
4939- interactive
4940- closeOnBlur
4941- options = { {
4942- positionFixed : true ,
4943- } }
4944- >
4945- { this . getCachedAdvancedFilterMenuActions (
4946- model ,
4947- column ,
4948- advancedFilterOptions ,
4949- sortDirection ,
4950- formatter
4951- ) }
4952- </ Popper >
4953- </ div >
4954- ) ;
5024+ // Handle advanced filter for column indexes not in visibleColumns
5025+ // Models can expose columns at negative indexes (e.g., model.columns[-1])
5026+ // We always render Poppers so they can transition from isShown=false to isShown=true
5027+ const metricState = this . getMetricState ( ) ;
5028+ for ( let i = - 1 ; model . columns [ i ] != null ; i -= 1 ) {
5029+ if ( ! model . isFilterable ( i ) ) {
5030+ // eslint-disable-next-line no-continue
5031+ continue ;
5032+ }
5033+ const filterCoords =
5034+ metricState != null
5035+ ? metricCalculator . getFilterInputCoordinates (
5036+ i ,
5037+ metricState ,
5038+ metrics
5039+ )
5040+ : null ;
5041+ if ( filterCoords != null ) {
5042+ const style : CSSProperties = {
5043+ position : 'absolute' ,
5044+ top : filterCoords . y ,
5045+ left : filterCoords . x + filterCoords . width - 20 ,
5046+ width : 20 ,
5047+ height : filterCoords . height ,
5048+ } ;
5049+ const element = this . renderAdvancedFilterMenu (
5050+ i ,
5051+ i , // modelColumn is same as columnIndex for negative indexes
5052+ style ,
5053+ shownAdvancedFilter === i
5054+ ) ;
5055+ if ( element != null ) {
49555056 advancedFilterMenus . push ( element ) ;
49565057 }
49575058 }
0 commit comments