Skip to content

Commit ec1192b

Browse files
authored
feat: DH-20363: Pivot filter support (#2602)
Added support for filtering on pivot columns (e.g., ColumnBy) that use negative column indices, and makes filter input positioning extensible for plugins. Added two new methods to `IrisGridMetricCalculator`: `getFilterInputCoordinates`: Returns positioning for the filter input field `getAdvancedFilterButtonCoordinates`: Returns positioning for the advanced filter button Both methods return null for negative indices by default, allowing plugins to override for custom column types Added `showAdvancedFilterButton` prop to `FilterInputField` to hide the advanced filter button for columns that don't support advanced filters.
1 parent ff7b424 commit ec1192b

6 files changed

Lines changed: 669 additions & 109 deletions

File tree

packages/grid/src/GridMetricCalculator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ export class GridMetricCalculator {
616616
visibleRows,
617617
visibleColumns,
618618

619-
// Map of the height/width of visible rows/columns
619+
// Map of the height/width of columns in the viewport (excluding floating columns)
620620
visibleRowHeights,
621621
visibleColumnWidths,
622622

@@ -632,7 +632,7 @@ export class GridMetricCalculator {
632632
allRows,
633633
allColumns,
634634

635-
// Map of the height/width of visible rows/columns
635+
// Map of the height/width of all rendered columns (visible + floating + dragging)
636636
allRowHeights,
637637
allColumnWidths,
638638

packages/iris-grid/src/FilterInputField.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface FilterInputFieldProps {
1515
className: string;
1616
style: React.CSSProperties;
1717
value: string;
18+
showAdvancedFilterButton: boolean;
1819
isAdvancedFilterSet: boolean;
1920
onAdvancedFiltersTriggered: React.MouseEventHandler<HTMLButtonElement>;
2021
onChange: (value: string) => void;
@@ -39,6 +40,7 @@ class FilterInputField extends PureComponent<
3940
style: {},
4041
className: '',
4142
value: '',
43+
showAdvancedFilterButton: false,
4244
isAdvancedFilterSet: false,
4345
onAdvancedFiltersTriggered: (): void => undefined,
4446
onChange: (): void => undefined,
@@ -207,6 +209,7 @@ class FilterInputField extends PureComponent<
207209
const {
208210
className,
209211
style,
212+
showAdvancedFilterButton,
210213
isAdvancedFilterSet,
211214
onAdvancedFiltersTriggered,
212215
} = this.props;
@@ -234,21 +237,26 @@ class FilterInputField extends PureComponent<
234237
autoCapitalize="off"
235238
spellCheck="false"
236239
/>
237-
<div className="advanced-filter-button-container">
238-
<Button
239-
kind="ghost"
240-
className={classNames('btn-link-icon advanced-filter-button', {
241-
'filter-set': isAdvancedFilterSet,
242-
})}
243-
onClick={onAdvancedFiltersTriggered}
244-
onContextMenu={this.handleContextMenu}
245-
>
246-
<div className="fa-layers ">
247-
<FontAwesomeIcon icon={dhFilterFilled} className="filter-solid" />
248-
<FontAwesomeIcon icon={vsFilter} className="filter-light" />
249-
</div>
250-
</Button>
251-
</div>
240+
{showAdvancedFilterButton && (
241+
<div className="advanced-filter-button-container">
242+
<Button
243+
kind="ghost"
244+
className={classNames('btn-link-icon advanced-filter-button', {
245+
'filter-set': isAdvancedFilterSet,
246+
})}
247+
onClick={onAdvancedFiltersTriggered}
248+
onContextMenu={this.handleContextMenu}
249+
>
250+
<div className="fa-layers ">
251+
<FontAwesomeIcon
252+
icon={dhFilterFilled}
253+
className="filter-solid"
254+
/>
255+
<FontAwesomeIcon icon={vsFilter} className="filter-light" />
256+
</div>
257+
</Button>
258+
</div>
259+
)}
252260
</div>
253261
);
254262
}

packages/iris-grid/src/IrisGrid.test.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,14 +359,18 @@ describe('handleResizeAllColumns', () => {
359359
});
360360
jest.spyOn(component, 'setState');
361361
expect(component.setState).not.toBeCalled();
362-
component.rebuildFilters();
362+
act(() => {
363+
component.rebuildFilters();
364+
});
363365
expect(component.setState).toBeCalled();
364366
});
365367

366368
it('does not update state for empty filters', () => {
367369
const component = makeComponent();
368370
jest.spyOn(component, 'setState');
369-
component.rebuildFilters();
371+
act(() => {
372+
component.rebuildFilters();
373+
});
370374
expect(component.setState).not.toBeCalled();
371375
});
372376
});
@@ -430,3 +434,33 @@ describe('handleResizeAllColumns', () => {
430434
});
431435
});
432436
});
437+
438+
describe('Advanced Filter', () => {
439+
it.each([
440+
{ columnIndex: -1, expectedVisibility: false },
441+
{ columnIndex: 0, expectedVisibility: true },
442+
{ columnIndex: 1, expectedVisibility: true },
443+
])(
444+
'advanced filter button visibility is $expectedVisibility for column index $columnIndex',
445+
({ columnIndex, expectedVisibility }) => {
446+
const model = irisGridTestUtils.makeModel();
447+
const ref = React.createRef<IrisGrid>();
448+
const { container } = render(
449+
<IrisGrid ref={ref} model={model} settings={DEFAULT_SETTINGS} />
450+
);
451+
452+
act(() => {
453+
ref.current?.setState({
454+
focusedFilterBarColumn: columnIndex,
455+
isFilterBarShown: true,
456+
});
457+
});
458+
459+
const advancedFilterButtons = container.querySelectorAll(
460+
'.advanced-filter-button'
461+
);
462+
463+
expect(advancedFilterButtons.length > 0).toBe(expectedVisibility);
464+
}
465+
);
466+
});

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 128 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2513,7 +2513,6 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
25132513

25142514
if (
25152515
column == null ||
2516-
column < 0 ||
25172516
columnCount <= column ||
25182517
!model.isFilterable(modelColumn)
25192518
) {
@@ -2523,22 +2522,19 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
25232522

25242523
const { metricCalculator, metrics } = this.state;
25252524
assertNotNull(metrics);
2526-
const { left, rightVisible, lastLeft } = metrics;
2527-
if (column < left) {
2528-
this.grid?.setViewState({ left: column }, true);
2529-
} else if (rightVisible < column) {
2530-
const metricState = this.getMetricState();
2531-
assertNotNull(metricState);
2532-
const newLeft = metricCalculator.getLastLeft(
2533-
metricState,
2534-
column,
2535-
metricCalculator.getVisibleWidth(metricState)
2536-
);
2537-
this.grid?.setViewState(
2538-
{ left: Math.min(newLeft, lastLeft), leftOffset: 0 },
2539-
true
2540-
);
2525+
const metricState = this.getMetricState();
2526+
assertNotNull(metricState);
2527+
2528+
const scrollColumn = metricCalculator.getScrollLeftForColumn(
2529+
column,
2530+
metricState,
2531+
metrics
2532+
);
2533+
2534+
if (scrollColumn != null) {
2535+
this.grid?.setViewState({ left: scrollColumn, leftOffset: 0 }, true);
25412536
}
2537+
25422538
this.lastFocusedFilterBarColumn = column;
25432539
this.setState({ focusedFilterBarColumn: column, isFilterBarShown: true });
25442540
}
@@ -4515,6 +4511,99 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
45154511
this.seekRow(gotoValue, isBackwards);
45164512
}
45174513

4514+
/**
4515+
* Render the input field for the focused filter
4516+
* @param metrics Grid metrics
4517+
* @param metricCalculator Metric calculator
4518+
* @param focusedFilterBarColumn Column index for the focused filter
4519+
* @param quickFilters Quick filters map
4520+
* @param advancedFilters Advanced filters map
4521+
* @returns The filter input field element or null if not applicable
4522+
*/
4523+
getFilterBarInput(
4524+
metrics: GridMetrics | undefined,
4525+
metricCalculator: IrisGridMetricCalculator,
4526+
focusedFilterBarColumn: VisibleIndex | null,
4527+
quickFilters: ReadonlyQuickFilterMap,
4528+
advancedFilters: ReadonlyAdvancedFilterMap
4529+
): ReactElement | null {
4530+
if (metrics == null || focusedFilterBarColumn == null) {
4531+
return null;
4532+
}
4533+
4534+
const metricState = this.getMetricState();
4535+
if (metricState == null) {
4536+
return null;
4537+
}
4538+
4539+
const filterBoxCoordinates = metricCalculator.getFilterInputCoordinates(
4540+
focusedFilterBarColumn,
4541+
metricState,
4542+
metrics
4543+
);
4544+
if (filterBoxCoordinates == null) {
4545+
return null;
4546+
}
4547+
4548+
const debounceMs = Math.min(
4549+
Math.max(IrisGrid.minDebounce, Math.round(metrics.rowCount / 200)),
4550+
IrisGrid.maxDebounce
4551+
);
4552+
const {
4553+
x,
4554+
y,
4555+
width: fieldWidth,
4556+
height: fieldHeight,
4557+
} = filterBoxCoordinates;
4558+
const { width } = metrics;
4559+
const style = {
4560+
top: y,
4561+
left: x,
4562+
minWidth: Math.min(fieldWidth, width - x), // Don't cause overflow
4563+
height: fieldHeight,
4564+
};
4565+
let value = '';
4566+
let isValid = true;
4567+
const modelColumn = this.getModelColumn(focusedFilterBarColumn);
4568+
assertNotNull(modelColumn);
4569+
const quickFilter = quickFilters.get(modelColumn);
4570+
const advancedFilter = advancedFilters.get(modelColumn);
4571+
if (quickFilter != null) {
4572+
value = quickFilter.text;
4573+
isValid = quickFilter.filter != null;
4574+
}
4575+
const isBarFiltered = quickFilters.size !== 0 || advancedFilters.size !== 0;
4576+
const showAdvancedFilterButton =
4577+
metricCalculator.getAdvancedFilterButtonCoordinates(
4578+
focusedFilterBarColumn,
4579+
metricState,
4580+
metrics
4581+
) != null;
4582+
return (
4583+
<FilterInputField
4584+
ref={this.filterInputRef}
4585+
style={style}
4586+
className={classNames({
4587+
error: !isValid,
4588+
active: value !== '' || advancedFilter != null,
4589+
'iris-grid-has-filter': isBarFiltered,
4590+
})}
4591+
showAdvancedFilterButton={showAdvancedFilterButton}
4592+
isAdvancedFilterSet={advancedFilter != null}
4593+
onAdvancedFiltersTriggered={() => {
4594+
this.setState({ shownAdvancedFilter: focusedFilterBarColumn });
4595+
}}
4596+
key={focusedFilterBarColumn}
4597+
onChange={this.handleFilterBarChange}
4598+
onDone={this.handleFilterBarDone}
4599+
onTab={this.handleFilterBarTab}
4600+
onContextMenu={this.grid?.handleContextMenu}
4601+
debounceMs={debounceMs}
4602+
value={value}
4603+
/>
4604+
);
4605+
}
4606+
45184607
render(): ReactElement | null {
45194608
const {
45204609
children,
@@ -4649,66 +4738,15 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
46494738
metrics != null && metrics.width > 0 && metrics.height > 0;
46504739
const isRollup = (rollupConfig?.columns?.length ?? 0) > 0;
46514740

4652-
let focusField = null;
4653-
4654-
const debounceMs = metrics
4655-
? Math.min(
4656-
Math.max(IrisGrid.minDebounce, Math.round(metrics.rowCount / 200)),
4657-
IrisGrid.maxDebounce
4741+
const focusField = isFilterBarShown
4742+
? this.getFilterBarInput(
4743+
metrics,
4744+
metricCalculator,
4745+
focusedFilterBarColumn,
4746+
quickFilters,
4747+
advancedFilters
46584748
)
4659-
: IrisGrid.maxDebounce;
4660-
4661-
if (isFilterBarShown && focusedFilterBarColumn != null && metrics != null) {
4662-
const { gridX, gridY, allColumnXs, allColumnWidths, width } = metrics;
4663-
const columnX = allColumnXs.get(focusedFilterBarColumn);
4664-
const columnWidth = allColumnWidths.get(focusedFilterBarColumn);
4665-
if (columnX != null && columnWidth != null) {
4666-
const x = gridX + columnX;
4667-
const y = gridY - (theme.filterBarHeight ?? 0);
4668-
const fieldWidth = columnWidth + 1; // cover right border
4669-
const fieldHeight = (theme.filterBarHeight ?? 0) - 1; // remove bottom border
4670-
const style = {
4671-
top: y,
4672-
left: x,
4673-
minWidth: Math.min(fieldWidth, width - x), // Don't cause overflow
4674-
height: fieldHeight,
4675-
};
4676-
let value = '';
4677-
let isValid = true;
4678-
const modelColumn = this.getModelColumn(focusedFilterBarColumn);
4679-
assertNotNull(modelColumn);
4680-
const quickFilter = quickFilters.get(modelColumn);
4681-
const advancedFilter = advancedFilters.get(modelColumn);
4682-
if (quickFilter != null) {
4683-
value = quickFilter.text;
4684-
isValid = quickFilter.filter != null;
4685-
}
4686-
const isBarFiltered =
4687-
quickFilters.size !== 0 || advancedFilters.size !== 0;
4688-
focusField = (
4689-
<FilterInputField
4690-
ref={this.filterInputRef}
4691-
style={style}
4692-
className={classNames({
4693-
error: !isValid,
4694-
active: value !== '' || advancedFilter != null,
4695-
'iris-grid-has-filter': isBarFiltered,
4696-
})}
4697-
isAdvancedFilterSet={advancedFilter != null}
4698-
onAdvancedFiltersTriggered={() => {
4699-
this.setState({ shownAdvancedFilter: focusedFilterBarColumn });
4700-
}}
4701-
key={focusedFilterBarColumn}
4702-
onChange={this.handleFilterBarChange}
4703-
onDone={this.handleFilterBarDone}
4704-
onTab={this.handleFilterBarTab}
4705-
onContextMenu={this.grid?.handleContextMenu}
4706-
debounceMs={debounceMs}
4707-
value={value}
4708-
/>
4709-
);
4710-
}
4711-
}
4749+
: null;
47124750

47134751
let loadingElement = null;
47144752
if (loadingText != null) {
@@ -4753,26 +4791,27 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
47534791

47544792
const filterBar = [];
47554793
if (metrics && isFilterBarShown) {
4756-
const { gridX, gridY, visibleColumns, allColumnXs, allColumnWidths } =
4757-
metrics;
4758-
const { filterBarHeight } = theme;
4794+
const metricState = this.getMetricState();
4795+
4796+
// Advanced Filter buttons
4797+
const { visibleColumns } = metrics;
47594798

47604799
for (let i = 0; i < visibleColumns.length; i += 1) {
47614800
const columnIndex = visibleColumns[i];
4762-
4763-
const columnX = allColumnXs.get(columnIndex);
4764-
const columnWidth = allColumnWidths.get(columnIndex);
47654801
const modelColumn = this.getModelColumn(columnIndex);
4802+
47664803
if (modelColumn != null) {
47674804
const isFilterable = model.isFilterable(modelColumn);
4768-
if (
4769-
isFilterable &&
4770-
columnX != null &&
4771-
columnWidth != null &&
4772-
columnWidth > 0
4773-
) {
4774-
const x = gridX + columnX + columnWidth - 24;
4775-
const y = gridY - (filterBarHeight ?? 0) + 2; // 2 acts as top margin for the button
4805+
const buttonCoordinates =
4806+
isFilterable && metricState
4807+
? metricCalculator.getAdvancedFilterButtonCoordinates(
4808+
columnIndex,
4809+
metricState,
4810+
metrics
4811+
)
4812+
: null;
4813+
if (buttonCoordinates != null) {
4814+
const { x, y } = buttonCoordinates;
47764815
const style: CSSProperties = {
47774816
position: 'absolute',
47784817
top: y,

0 commit comments

Comments
 (0)