Skip to content

Commit 44393cc

Browse files
committed
Expandable columns for Pivots
1 parent e3860a7 commit 44393cc

File tree

10 files changed

+278
-35
lines changed

10 files changed

+278
-35
lines changed

packages/grid/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './ColumnHeaderGroup';
22
export * from './EditableGridModel';
33
export * from './DeletableGridModel';
44
export * from './ExpandableGridModel';
5+
export * from './ExpandableColumnGridModel';
56
export { default as Grid } from './Grid';
67
export * from './Grid';
78
export * from './GridMetricCalculator';

packages/iris-grid/src/ColumnHeaderGroup.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export function isColumnHeaderGroup(x: unknown): x is ColumnHeaderGroup {
1414
return x instanceof ColumnHeaderGroup;
1515
}
1616

17+
export type ColumnHeaderGroupConfig = {
18+
name: string;
19+
children: string[];
20+
color?: string | null;
21+
depth: number;
22+
childIndexes: ModelIndex[];
23+
parent?: string;
24+
};
25+
1726
export default class ColumnHeaderGroup implements IColumnHeaderGroup {
1827
static NEW_GROUP_PREFIX = ':newGroup';
1928

@@ -36,14 +45,7 @@ export default class ColumnHeaderGroup implements IColumnHeaderGroup {
3645
depth,
3746
childIndexes,
3847
parent,
39-
}: {
40-
name: string;
41-
children: string[];
42-
color?: string | null;
43-
depth: number;
44-
childIndexes: ModelIndex[];
45-
parent?: string;
46-
}) {
48+
}: ColumnHeaderGroupConfig) {
4749
this.name = name;
4850
this.children = children;
4951
this.color = color ?? undefined;

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

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import dh from '@deephaven/jsapi-shim';
44
import { DateUtils, Settings } from '@deephaven/jsapi-utils';
55
import { TestUtils } from '@deephaven/utils';
66
import { TypeValue } from '@deephaven/filters';
7+
import {
8+
ExpandableColumnGridModel,
9+
isExpandableColumnGridModel,
10+
} from '@deephaven/grid';
711
import IrisGrid from './IrisGrid';
812
import IrisGridTestUtils from './IrisGridTestUtils';
13+
import IrisGridProxyModel from './IrisGridProxyModel';
914

1015
class MockPath2D {
1116
// eslint-disable-next-line class-methods-use-this
@@ -14,6 +19,13 @@ class MockPath2D {
1419

1520
window.Path2D = MockPath2D as unknown as new () => Path2D;
1621

22+
jest.mock('@deephaven/grid', () => ({
23+
...jest.requireActual('@deephaven/grid'),
24+
isExpandableColumnGridModel: jest.fn(),
25+
}));
26+
27+
const { asMock } = TestUtils;
28+
1729
const VIEW_SIZE = 5000;
1830

1931
const DEFAULT_SETTINGS: Settings = {
@@ -66,10 +78,12 @@ function createNodeMock(element: ReactElement) {
6678

6779
function makeComponent(
6880
model = irisGridTestUtils.makeModel(),
69-
settings = DEFAULT_SETTINGS
81+
settings = DEFAULT_SETTINGS,
82+
props = {}
7083
) {
7184
const testRenderer = TestRenderer.create(
72-
<IrisGrid model={model} settings={settings} />,
85+
// eslint-disable-next-line react/jsx-props-no-spreading
86+
<IrisGrid model={model} settings={settings} {...props} />,
7387
{
7488
createNodeMock,
7589
}
@@ -222,3 +236,96 @@ it('should set gotoValueSelectedColumnName to empty string if no columns are giv
222236

223237
expect(component.state.gotoValueSelectedColumnName).toEqual('');
224238
});
239+
240+
describe('rebuildFilters', () => {
241+
it('updates state if filters not empty', () => {
242+
const component = makeComponent(undefined, undefined, {
243+
quickFilters: [
244+
[
245+
'2',
246+
{
247+
columnType: IrisGridTestUtils.DEFAULT_TYPE,
248+
filterList: [
249+
{
250+
operator: 'eq',
251+
text: 'null',
252+
value: null,
253+
startColumnIndex: 0,
254+
},
255+
],
256+
},
257+
],
258+
],
259+
});
260+
jest.spyOn(component, 'setState');
261+
expect(component.setState).not.toBeCalled();
262+
component.rebuildFilters();
263+
expect(component.setState).toBeCalled();
264+
});
265+
266+
it('does not update state for empty filters', () => {
267+
const component = makeComponent();
268+
jest.spyOn(component, 'setState');
269+
component.rebuildFilters();
270+
expect(component.setState).not.toBeCalled();
271+
});
272+
});
273+
274+
describe('column expand/collapse', () => {
275+
let model: IrisGridProxyModel & ExpandableColumnGridModel;
276+
let component: IrisGrid;
277+
278+
beforeEach(() => {
279+
model = irisGridTestUtils.makeModel() as IrisGridProxyModel &
280+
ExpandableColumnGridModel;
281+
component = makeComponent(model);
282+
model.setColumnExpanded = jest.fn();
283+
model.isColumnExpanded = jest.fn(() => false);
284+
model.expandAllColumns = jest.fn();
285+
model.collapseAllColumns = jest.fn();
286+
});
287+
288+
afterEach(() => {
289+
jest.clearAllMocks();
290+
});
291+
292+
it('calls setColumnExpanded if model supports expandable columns', () => {
293+
asMock(isExpandableColumnGridModel).mockReturnValue(true);
294+
model.hasExpandableColumns = true;
295+
component.toggleExpandColumn(0);
296+
expect(model.setColumnExpanded).toHaveBeenCalled();
297+
});
298+
299+
it('ignores setColumnExpanded and expand/collapse all if model does not support expandable columns', () => {
300+
asMock(isExpandableColumnGridModel).mockReturnValue(false);
301+
component.toggleExpandColumn(0);
302+
expect(model.setColumnExpanded).not.toHaveBeenCalled();
303+
304+
component.expandAllColumns();
305+
expect(model.expandAllColumns).not.toHaveBeenCalled();
306+
307+
component.collapseAllColumns();
308+
expect(model.collapseAllColumns).not.toHaveBeenCalled();
309+
});
310+
311+
it('calls expandAllColumns if model supports expandable columns and expand all', () => {
312+
asMock(isExpandableColumnGridModel).mockReturnValue(true);
313+
model.isExpandAllColumnsAvailable = true;
314+
component.expandAllColumns();
315+
expect(model.expandAllColumns).toHaveBeenCalled();
316+
317+
component.collapseAllColumns();
318+
expect(model.collapseAllColumns).toHaveBeenCalled();
319+
});
320+
321+
it('ignores expandAllColumns if model does not support expand all', () => {
322+
asMock(isExpandableColumnGridModel).mockReturnValue(true);
323+
model.isExpandAllColumnsAvailable = false;
324+
325+
component.expandAllColumns();
326+
expect(model.expandAllColumns).not.toHaveBeenCalled();
327+
328+
component.collapseAllColumns();
329+
expect(model.collapseAllColumns).not.toHaveBeenCalled();
330+
});
331+
});

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
BoundedAxisRange,
4747
isExpandableGridModel,
4848
isDeletableGridModel,
49+
isExpandableColumnGridModel,
4950
} from '@deephaven/grid';
5051
import {
5152
dhEye,
@@ -387,6 +388,9 @@ export interface IrisGridState {
387388
// Current ongoing copy operation
388389
copyOperation: CopyOperation | null;
389390

391+
// Map of column indexes to their current names
392+
columnNameMap: Map<ModelIndex, ColumnName>;
393+
390394
// The filter that is currently being applied. Reset after update is received
391395
loadingText: string | null;
392396
loadingScrimProgress: number | null;
@@ -791,6 +795,9 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
791795
focusedFilterBarColumn: null,
792796
metricCalculator,
793797
metrics: undefined,
798+
columnNameMap: new Map(
799+
model.columns.map((col, index) => [index, col.name])
800+
),
794801

795802
partitionConfig:
796803
partitionConfig ??
@@ -1102,6 +1109,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
11021109
getCachedOptionItems = memoize(
11031110
(
11041111
isChartBuilderAvailable: boolean,
1112+
isOrganizeColumnsAvailable: boolean,
11051113
isCustomColumnsAvailable: boolean,
11061114
isFormatColumnsAvailable: boolean,
11071115
isRollupAvailable: boolean,
@@ -1126,11 +1134,13 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
11261134
icon: dhGraphLineUp,
11271135
});
11281136
}
1129-
optionItems.push({
1130-
type: OptionType.VISIBILITY_ORDERING_BUILDER,
1131-
title: 'Organize Columns',
1132-
icon: dhEye,
1133-
});
1137+
if (isOrganizeColumnsAvailable) {
1138+
optionItems.push({
1139+
type: OptionType.VISIBILITY_ORDERING_BUILDER,
1140+
title: 'Organize Columns',
1141+
icon: dhEye,
1142+
});
1143+
}
11341144
if (isFormatColumnsAvailable) {
11351145
optionItems.push({
11361146
type: OptionType.CONDITIONAL_FORMATTING,
@@ -1814,6 +1824,12 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
18141824
rebuildFilters(): void {
18151825
const { model } = this.props;
18161826
const { advancedFilters, quickFilters } = this.state;
1827+
1828+
if (advancedFilters.size === 0 && quickFilters.size === 0) {
1829+
log.debug('No filters to rebuild');
1830+
return;
1831+
}
1832+
18171833
const { columns, formatter } = model;
18181834

18191835
log.debug('Rebuilding filters');
@@ -2483,6 +2499,36 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
24832499
});
24842500
}
24852501

2502+
toggleExpandColumn(modelIndex: ModelIndex): void {
2503+
log.debug2('Toggle expand column', modelIndex);
2504+
const { model } = this.props;
2505+
if (isExpandableColumnGridModel(model) && model.hasExpandableColumns) {
2506+
model.setColumnExpanded(modelIndex, !model.isColumnExpanded(modelIndex));
2507+
}
2508+
}
2509+
2510+
expandAllColumns(): void {
2511+
log.debug2('Expand all columns');
2512+
const { model } = this.props;
2513+
if (
2514+
isExpandableColumnGridModel(model) &&
2515+
model.isExpandAllColumnsAvailable
2516+
) {
2517+
model.expandAllColumns();
2518+
}
2519+
}
2520+
2521+
collapseAllColumns(): void {
2522+
log.debug2('Collapse all columns');
2523+
const { model } = this.props;
2524+
if (
2525+
isExpandableColumnGridModel(model) &&
2526+
model.isExpandAllColumnsAvailable
2527+
) {
2528+
model.collapseAllColumns();
2529+
}
2530+
}
2531+
24862532
handleColumnVisibilityChanged(
24872533
modelIndexes: readonly ModelIndex[],
24882534
isVisible: boolean
@@ -2653,6 +2699,42 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
26532699
this.grid?.forceUpdate();
26542700
}
26552701

2702+
/**
2703+
* Update the user column widths when indexes change after expand/collapse.
2704+
*/
2705+
updateUserColumnWidths(): void {
2706+
const { model } = this.props;
2707+
const { metricCalculator, columnNameMap: prevColumnNameMap } = this.state;
2708+
const columnNameMap = new Map(
2709+
model.columns.map((col, index) => [index, col.name])
2710+
);
2711+
2712+
// Map userColumnWidths to column names
2713+
const userColumnWidths = metricCalculator.getUserColumnWidths();
2714+
const namedUserColumnWidths = new Map<string, number>();
2715+
userColumnWidths.forEach((width, modelIndex) => {
2716+
const name = prevColumnNameMap.get(modelIndex);
2717+
if (width != null && name != null) {
2718+
namedUserColumnWidths.set(name, width);
2719+
}
2720+
metricCalculator.resetColumnWidth(modelIndex);
2721+
});
2722+
2723+
// Restore the column widths with new indexes
2724+
namedUserColumnWidths.forEach((width, name) => {
2725+
const modelIndex = model.getColumnIndexByName(name);
2726+
if (modelIndex != null) {
2727+
metricCalculator.setColumnWidth(modelIndex, width);
2728+
}
2729+
});
2730+
2731+
// We store metrics in both Grid and IrisGrid state, keep them in sync
2732+
this.setState({
2733+
metrics: this.grid?.updateMetrics(),
2734+
columnNameMap,
2735+
});
2736+
}
2737+
26562738
toggleSort(columnIndex: VisibleIndex, addToExisting: boolean): void {
26572739
log.info('Toggling sort for column', columnIndex);
26582740

@@ -3412,8 +3494,11 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
34123494
log.debug('custom columns changed');
34133495
const { isReady } = this.state;
34143496
if (isReady) {
3497+
this.updateUserColumnWidths();
3498+
3499+
// Make sure stopLoading() is called after the updateMetrics call in updateUserColumnWidths,
3500+
// otherwise IrisGridModelUpdater queues an extra setViewport based on old metrics.
34153501
this.stopLoading();
3416-
this.grid?.forceUpdate();
34173502
} else {
34183503
this.loadTableState();
34193504
}
@@ -4631,6 +4716,7 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
46314716

46324717
const optionItems = this.getCachedOptionItems(
46334718
onCreateChart !== undefined && model.isChartBuilderAvailable,
4719+
model.isOrganizeColumnsAvailable,
46344720
model.isCustomColumnsAvailable,
46354721
model.isFormatColumnsAvailable,
46364722
model.isRollupAvailable,

packages/iris-grid/src/IrisGridModel.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,13 @@ abstract class IrisGridModel<
407407
return false;
408408
}
409409

410+
/**
411+
* @returns True if this model supports column groups and moved columns
412+
*/
413+
get isOrganizeColumnsAvailable(): boolean {
414+
return false;
415+
}
416+
410417
/**
411418
* @returns True if this model supports customColumns
412419
*/

packages/iris-grid/src/IrisGridTableModel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ class IrisGridTableModel
6767
return this.table.selectDistinct != null;
6868
}
6969

70+
get isOrganizeColumnsAvailable(): boolean {
71+
return true;
72+
}
73+
7074
get isCustomColumnsAvailable(): boolean {
7175
return this.table.applyCustomColumns != null;
7276
}

packages/iris-grid/src/IrisGridTreeTableModel.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import memoize from 'memoize-one';
33
import {
44
BoundedAxisRange,
5+
ExpandableGridModel,
56
GridCell,
67
GridRange,
78
ModelIndex,
@@ -50,10 +51,10 @@ function isLayoutTreeTable(table: DhType.TreeTable): table is LayoutTreeTable {
5051
return (table as LayoutTreeTable).layoutHints !== undefined;
5152
}
5253

53-
class IrisGridTreeTableModel extends IrisGridTableModelTemplate<
54-
DhType.TreeTable,
55-
UITreeRow
56-
> {
54+
class IrisGridTreeTableModel
55+
extends IrisGridTableModelTemplate<DhType.TreeTable, UITreeRow>
56+
implements ExpandableGridModel
57+
{
5758
/** We keep a virtual column at the front that tracks the "group" that is expanded */
5859
private virtualColumns: DisplayColumn[];
5960

0 commit comments

Comments
 (0)