Skip to content

Commit 174c6a1

Browse files
committed
Fix Advanced Filters in Pivots
1 parent c46fbaf commit 174c6a1

1 file changed

Lines changed: 210 additions & 109 deletions

File tree

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 210 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)