diff --git a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx index c6991ac7d..add2f28d2 100644 --- a/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx +++ b/packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx @@ -45,6 +45,8 @@ import { ColumnHeaderGroup, IrisGridContextMenuData, PartitionConfig, + IrisGridRenderer, + MouseHandlersProp, } from '@deephaven/iris-grid'; import { type RowDataMap, @@ -151,7 +153,10 @@ export interface OwnProps extends DashboardPanelProps { /** Load a plugin defined by the table */ loadPlugin: (pluginName: string) => TablePluginComponent; - theme?: IrisGridThemeType; + theme?: Partial & Record; + + mouseHandlers?: MouseHandlersProp; + renderer?: IrisGridRenderer; } interface StateProps { @@ -993,6 +998,8 @@ export class IrisGridPanel extends PureComponent< rollupConfig, aggregationSettings, sorts, + // TODO: + // DH-20403: IrisGrid should persist user column widths when the model initializes with a partial column list userColumnWidths, userRowHeights, showSearchBar, @@ -1103,8 +1110,10 @@ export class IrisGridPanel extends PureComponent< inputFilters, links, metadata, + mouseHandlers, panelState, user, + renderer, settings, theme, } = this.props; @@ -1206,11 +1215,13 @@ export class IrisGridPanel extends PureComponent< isSelectingPartition={isSelectingPartition} isStuckToBottom={isStuckToBottom} isStuckToRight={isStuckToRight} + mouseHandlers={mouseHandlers} movedColumns={movedColumns} movedRows={movedRows} partitions={partitions} partitionConfig={partitionConfig} quickFilters={quickFilters} + renderer={renderer} reverse={reverse} rollupConfig={rollupConfig} settings={settings} diff --git a/packages/grid/src/ExpandableColumnGridModel.test.ts b/packages/grid/src/ExpandableColumnGridModel.test.ts new file mode 100644 index 000000000..d52d22562 --- /dev/null +++ b/packages/grid/src/ExpandableColumnGridModel.test.ts @@ -0,0 +1,29 @@ +import { isExpandableColumnGridModel } from './ExpandableColumnGridModel'; +import type GridModel from './GridModel'; +import type ExpandableColumnGridModel from './ExpandableColumnGridModel'; + +describe('ExpandableColumnGridModel', () => { + describe('isExpandableColumnGridModel', () => { + it('should return true for model with hasExpandableColumns property', () => { + const model = { + hasExpandableColumns: true, + } as ExpandableColumnGridModel; + + expect(isExpandableColumnGridModel(model)).toBe(true); + }); + + it('should return true when hasExpandableColumns is false', () => { + const model = { + hasExpandableColumns: false, + } as ExpandableColumnGridModel; + + expect(isExpandableColumnGridModel(model)).toBe(true); + }); + + it('should return false for model without hasExpandableColumns property', () => { + const model = {} as GridModel; + + expect(isExpandableColumnGridModel(model)).toBe(false); + }); + }); +}); diff --git a/packages/grid/src/ExpandableColumnGridModel.ts b/packages/grid/src/ExpandableColumnGridModel.ts new file mode 100644 index 000000000..440e70ae2 --- /dev/null +++ b/packages/grid/src/ExpandableColumnGridModel.ts @@ -0,0 +1,64 @@ +import GridModel from './GridModel'; +import { ModelIndex } from './GridMetrics'; + +export function isExpandableColumnGridModel( + model: GridModel +): model is ExpandableColumnGridModel { + return ( + (model as ExpandableColumnGridModel)?.hasExpandableColumns !== undefined + ); +} + +/** + * Expandable grid model. Allows for a grid with columns that can expand (e.g. Pivot Table) + */ +export interface ExpandableColumnGridModel extends GridModel { + /** Whether the grid has columns that can be expanded */ + hasExpandableColumns: boolean; + + /** Whether the grid can expand all columns */ + isExpandAllColumnsAvailable: boolean; + + /** + * @param column Column to check + * @returns True if the column is expandable + */ + isColumnExpandable: (column: ModelIndex) => boolean; + + /** + * @param column Column to check + * @returns True if the column is currently expanded + */ + isColumnExpanded: (column: ModelIndex) => boolean; + + /** + * Change the expanded status of an expandable column + * @param column Column to expand + * @param isExpanded True to expand the column, false to collapse + * @param expandDescendants True to expand nested columns, false otherwise + */ + setColumnExpanded: ( + column: ModelIndex, + isExpanded: boolean, + expandDescendants?: boolean + ) => void; + + /** + * Expand all columns + */ + expandAllColumns: () => void; + + /** + * Collapse all columns + */ + collapseAllColumns: () => void; + + /** + * Get the depth of a column (ie. How indented the column should be) + * @param column Column to check + * @returns Depth of the column + */ + depthForColumn: (column: ModelIndex) => number; +} + +export default ExpandableColumnGridModel; diff --git a/packages/grid/src/index.ts b/packages/grid/src/index.ts index f145c88e7..29a33f63b 100644 --- a/packages/grid/src/index.ts +++ b/packages/grid/src/index.ts @@ -2,6 +2,7 @@ export * from './ColumnHeaderGroup'; export * from './EditableGridModel'; export * from './DeletableGridModel'; export * from './ExpandableGridModel'; +export * from './ExpandableColumnGridModel'; export { default as Grid } from './Grid'; export * from './Grid'; export * from './GridMetricCalculator'; diff --git a/packages/iris-grid/src/ColumnHeaderGroup.ts b/packages/iris-grid/src/ColumnHeaderGroup.ts index 33b4fa78d..a83bc9f45 100644 --- a/packages/iris-grid/src/ColumnHeaderGroup.ts +++ b/packages/iris-grid/src/ColumnHeaderGroup.ts @@ -14,6 +14,15 @@ export function isColumnHeaderGroup(x: unknown): x is ColumnHeaderGroup { return x instanceof ColumnHeaderGroup; } +export type ColumnHeaderGroupConfig = { + name: string; + children: string[]; + color?: string | null; + depth: number; + childIndexes: ModelIndex[]; + parent?: string; +}; + export default class ColumnHeaderGroup implements IColumnHeaderGroup { static NEW_GROUP_PREFIX = ':newGroup'; @@ -36,14 +45,7 @@ export default class ColumnHeaderGroup implements IColumnHeaderGroup { depth, childIndexes, parent, - }: { - name: string; - children: string[]; - color?: string | null; - depth: number; - childIndexes: ModelIndex[]; - parent?: string; - }) { + }: ColumnHeaderGroupConfig) { this.name = name; this.children = children; this.color = color ?? undefined; diff --git a/packages/iris-grid/src/IrisGrid.test.tsx b/packages/iris-grid/src/IrisGrid.test.tsx index 24d58e721..0392812bf 100644 --- a/packages/iris-grid/src/IrisGrid.test.tsx +++ b/packages/iris-grid/src/IrisGrid.test.tsx @@ -4,8 +4,13 @@ import dh from '@deephaven/jsapi-shim'; import { DateUtils, Settings } from '@deephaven/jsapi-utils'; import { TestUtils } from '@deephaven/utils'; import { TypeValue } from '@deephaven/filters'; +import { + ExpandableColumnGridModel, + isExpandableColumnGridModel, +} from '@deephaven/grid'; import IrisGrid from './IrisGrid'; import IrisGridTestUtils from './IrisGridTestUtils'; +import IrisGridProxyModel from './IrisGridProxyModel'; class MockPath2D { // eslint-disable-next-line class-methods-use-this @@ -14,6 +19,13 @@ class MockPath2D { window.Path2D = MockPath2D as unknown as new () => Path2D; +jest.mock('@deephaven/grid', () => ({ + ...jest.requireActual('@deephaven/grid'), + isExpandableColumnGridModel: jest.fn(), +})); + +const { asMock } = TestUtils; + const VIEW_SIZE = 5000; const DEFAULT_SETTINGS: Settings = { @@ -66,10 +78,12 @@ function createNodeMock(element: ReactElement) { function makeComponent( model = irisGridTestUtils.makeModel(), - settings = DEFAULT_SETTINGS + settings = DEFAULT_SETTINGS, + props = {} ) { const testRenderer = TestRenderer.create( - , + // eslint-disable-next-line react/jsx-props-no-spreading + , { createNodeMock, } @@ -222,3 +236,96 @@ it('should set gotoValueSelectedColumnName to empty string if no columns are giv expect(component.state.gotoValueSelectedColumnName).toEqual(''); }); + +describe('rebuildFilters', () => { + it('updates state if filters not empty', () => { + const component = makeComponent(undefined, undefined, { + quickFilters: [ + [ + '2', + { + columnType: IrisGridTestUtils.DEFAULT_TYPE, + filterList: [ + { + operator: 'eq', + text: 'null', + value: null, + startColumnIndex: 0, + }, + ], + }, + ], + ], + }); + jest.spyOn(component, 'setState'); + expect(component.setState).not.toBeCalled(); + component.rebuildFilters(); + expect(component.setState).toBeCalled(); + }); + + it('does not update state for empty filters', () => { + const component = makeComponent(); + jest.spyOn(component, 'setState'); + component.rebuildFilters(); + expect(component.setState).not.toBeCalled(); + }); +}); + +describe('column expand/collapse', () => { + let model: IrisGridProxyModel & ExpandableColumnGridModel; + let component: IrisGrid; + + beforeEach(() => { + model = irisGridTestUtils.makeModel() as IrisGridProxyModel & + ExpandableColumnGridModel; + component = makeComponent(model); + model.setColumnExpanded = jest.fn(); + model.isColumnExpanded = jest.fn(() => false); + model.expandAllColumns = jest.fn(); + model.collapseAllColumns = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls setColumnExpanded if model supports expandable columns', () => { + asMock(isExpandableColumnGridModel).mockReturnValue(true); + model.hasExpandableColumns = true; + component.toggleExpandColumn(0); + expect(model.setColumnExpanded).toHaveBeenCalled(); + }); + + it('ignores setColumnExpanded and expand/collapse all if model does not support expandable columns', () => { + asMock(isExpandableColumnGridModel).mockReturnValue(false); + component.toggleExpandColumn(0); + expect(model.setColumnExpanded).not.toHaveBeenCalled(); + + component.expandAllColumns(); + expect(model.expandAllColumns).not.toHaveBeenCalled(); + + component.collapseAllColumns(); + expect(model.collapseAllColumns).not.toHaveBeenCalled(); + }); + + it('calls expandAllColumns if model supports expandable columns and expand all', () => { + asMock(isExpandableColumnGridModel).mockReturnValue(true); + model.isExpandAllColumnsAvailable = true; + component.expandAllColumns(); + expect(model.expandAllColumns).toHaveBeenCalled(); + + component.collapseAllColumns(); + expect(model.collapseAllColumns).toHaveBeenCalled(); + }); + + it('ignores expandAllColumns if model does not support expand all', () => { + asMock(isExpandableColumnGridModel).mockReturnValue(true); + model.isExpandAllColumnsAvailable = false; + + component.expandAllColumns(); + expect(model.expandAllColumns).not.toHaveBeenCalled(); + + component.collapseAllColumns(); + expect(model.collapseAllColumns).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/iris-grid/src/IrisGrid.tsx b/packages/iris-grid/src/IrisGrid.tsx index fc50e4d38..e107b8ec0 100644 --- a/packages/iris-grid/src/IrisGrid.tsx +++ b/packages/iris-grid/src/IrisGrid.tsx @@ -34,7 +34,6 @@ import { GridMouseHandler, GridRange, GridRangeIndex, - GridThemeType, GridUtils, KeyHandler, ModelIndex, @@ -46,6 +45,7 @@ import { BoundedAxisRange, isExpandableGridModel, isDeletableGridModel, + isExpandableColumnGridModel, } from '@deephaven/grid'; import { dhEye, @@ -124,7 +124,9 @@ import { PendingMouseHandler, } from './mousehandlers'; import ToastBottomBar from './ToastBottomBar'; -import IrisGridMetricCalculator from './IrisGridMetricCalculator'; +import IrisGridMetricCalculator, { + IrisGridMetricState, +} from './IrisGridMetricCalculator'; import IrisGridModelUpdater from './IrisGridModelUpdater'; import IrisGridRenderer from './IrisGridRenderer'; import { createDefaultIrisGridTheme, IrisGridThemeType } from './IrisGridTheme'; @@ -270,6 +272,11 @@ export interface IrisGridContextMenuData { modelColumn: GridRangeIndex; } +export type MouseHandlersProp = readonly ( + | GridMouseHandler + | ((irisGrid: IrisGrid) => GridMouseHandler) +)[]; + export interface IrisGridProps { children: React.ReactNode; advancedFilters: ReadonlyAdvancedFilterMap; @@ -344,7 +351,7 @@ export interface IrisGridProps { frozenColumns: readonly ColumnName[]; // Theme override for IrisGridTheme - theme: GridThemeType; + theme: Partial & Record; canToggleSearch: boolean; @@ -352,7 +359,7 @@ export interface IrisGridProps { // Optional key and mouse handlers keyHandlers: readonly KeyHandler[]; - mouseHandlers: readonly GridMouseHandler[]; + mouseHandlers: MouseHandlersProp; // Pass in a custom renderer to the grid for advanced use cases renderer?: IrisGridRenderer; @@ -730,13 +737,18 @@ class IrisGrid extends Component { columnHeaderGroups, } = props; + const { mouseHandlers: mouseHandlersProp } = props; + const { dh } = model; const keyHandlers: KeyHandler[] = [ new CopyCellKeyHandler(this), new ReverseKeyHandler(this), new ClearFilterKeyHandler(this), ]; - const mouseHandlers: GridMouseHandler[] = [ + const mouseHandlers: ( + | GridMouseHandler + | ((irisGrid: IrisGrid) => GridMouseHandler) + )[] = [ new IrisGridCellOverflowMouseHandler(this), new IrisGridRowTreeMouseHandler(this), new IrisGridTokenMouseHandler(this), @@ -748,6 +760,7 @@ class IrisGrid extends Component { new IrisGridDataSelectMouseHandler(this), new PendingMouseHandler(this), new IrisGridPartitionedTableMouseHandler(this), + ...mouseHandlersProp, ]; if (canCopy) { keyHandlers.push(new CopyKeyHandler(this)); @@ -1065,7 +1078,7 @@ class IrisGrid extends Component { keyHandlers: readonly KeyHandler[]; - mouseHandlers: readonly GridMouseHandler[]; + mouseHandlers: MouseHandlersProp; get gridWrapper(): HTMLDivElement | null { return this.grid?.canvasWrapper.current ?? null; @@ -1102,6 +1115,7 @@ class IrisGrid extends Component { getCachedOptionItems = memoize( ( isChartBuilderAvailable: boolean, + isOrganizeColumnsAvailable: boolean, isCustomColumnsAvailable: boolean, isFormatColumnsAvailable: boolean, isRollupAvailable: boolean, @@ -1126,11 +1140,13 @@ class IrisGrid extends Component { icon: dhGraphLineUp, }); } - optionItems.push({ - type: OptionType.VISIBILITY_ORDERING_BUILDER, - title: 'Organize Columns', - icon: dhEye, - }); + if (isOrganizeColumnsAvailable) { + optionItems.push({ + type: OptionType.VISIBILITY_ORDERING_BUILDER, + title: 'Organize Columns', + icon: dhEye, + }); + } if (isFormatColumnsAvailable) { optionItems.push({ type: OptionType.CONDITIONAL_FORMATTING, @@ -1453,10 +1469,32 @@ class IrisGrid extends Component { return this.getCachedKeyHandlers(keyHandlers); } + getMetricState(): IrisGridMetricState | undefined { + const gridMetricState = this.grid?.getMetricState(); + if (gridMetricState == null) { + return undefined; + } + const { isFilterBarShown, advancedFilters, quickFilters, sorts, reverse } = + this.state; + const { model } = this.props; + + return { + ...gridMetricState, + model, + theme: this.getTheme(), + isFilterBarShown, + advancedFilters, + quickFilters, + sorts, + reverse, + }; + } + getCachedMouseHandlers = memoize( - ( - mouseHandlers: readonly GridMouseHandler[] - ): readonly GridMouseHandler[] => [...mouseHandlers, ...this.mouseHandlers] + (mouseHandlers: MouseHandlersProp): readonly GridMouseHandler[] => + [...mouseHandlers, ...this.mouseHandlers].map(handler => + typeof handler === 'function' ? handler(this) : handler + ) ); getCachedRenderer = memoize( @@ -1814,6 +1852,12 @@ class IrisGrid extends Component { rebuildFilters(): void { const { model } = this.props; const { advancedFilters, quickFilters } = this.state; + + if (advancedFilters.size === 0 && quickFilters.size === 0) { + log.debug('No filters to rebuild'); + return; + } + const { columns, formatter } = model; log.debug('Rebuilding filters'); @@ -2401,7 +2445,7 @@ class IrisGrid extends Component { if (column < left) { this.grid?.setViewState({ left: column }, true); } else if (rightVisible < column) { - const metricState = this.grid?.getMetricState(); + const metricState = this.getMetricState(); assertNotNull(metricState); const newLeft = metricCalculator.getLastLeft( metricState, @@ -2483,6 +2527,36 @@ class IrisGrid extends Component { }); } + toggleExpandColumn(modelIndex: ModelIndex): void { + log.debug2('Toggle expand column', modelIndex); + const { model } = this.props; + if (isExpandableColumnGridModel(model) && model.hasExpandableColumns) { + model.setColumnExpanded(modelIndex, !model.isColumnExpanded(modelIndex)); + } + } + + expandAllColumns(): void { + log.debug2('Expand all columns'); + const { model } = this.props; + if ( + isExpandableColumnGridModel(model) && + model.isExpandAllColumnsAvailable + ) { + model.expandAllColumns(); + } + } + + collapseAllColumns(): void { + log.debug2('Collapse all columns'); + const { model } = this.props; + if ( + isExpandableColumnGridModel(model) && + model.isExpandAllColumnsAvailable + ) { + model.collapseAllColumns(); + } + } + handleColumnVisibilityChanged( modelIndexes: readonly ModelIndex[], isVisible: boolean @@ -2513,12 +2587,9 @@ class IrisGrid extends Component { } handleColumnVisibilityReset(): void { - const { metricCalculator, metrics } = this.state; + const { metricCalculator } = this.state; const { model } = this.props; - assertNotNull(metrics); - for (let i = 0; i < metrics.columnCount; i += 1) { - metricCalculator.resetColumnWidth(i); - } + metricCalculator.resetAllColumnWidths(); this.handleMovedColumnsChanged(model.initialMovedColumns); this.handleHeaderGroupsChanged(model.initialColumnHeaderGroups); this.setState({ @@ -2653,6 +2724,16 @@ class IrisGrid extends Component { this.grid?.forceUpdate(); } + /** + * Updates grid metrics after model columns have changed + * to keep Grid and IrisGrid metrics in sync since metrics are stored in both places. + */ + updateMetrics(): void { + this.setState({ + metrics: this.grid?.updateMetrics(), + }); + } + toggleSort(columnIndex: VisibleIndex, addToExisting: boolean): void { log.info('Toggling sort for column', columnIndex); @@ -3409,11 +3490,14 @@ class IrisGrid extends Component { } handleCustomColumnsChanged(): void { - log.debug('custom columns changed'); + log.debug('Model columns changed'); const { isReady } = this.state; if (isReady) { + this.updateMetrics(); + + // Make sure stopLoading() is called after the updateMetrics call, + // otherwise IrisGridModelUpdater queues an extra setViewport based on old metrics. this.stopLoading(); - this.grid?.forceUpdate(); } else { this.loadTableState(); } @@ -4631,6 +4715,7 @@ class IrisGrid extends Component { const optionItems = this.getCachedOptionItems( onCreateChart !== undefined && model.isChartBuilderAvailable, + model.isOrganizeColumnsAvailable, model.isCustomColumnsAvailable, model.isFormatColumnsAvailable, model.isRollupAvailable, diff --git a/packages/iris-grid/src/IrisGridMetricCalculator.test.ts b/packages/iris-grid/src/IrisGridMetricCalculator.test.ts new file mode 100644 index 000000000..c547b770d --- /dev/null +++ b/packages/iris-grid/src/IrisGridMetricCalculator.test.ts @@ -0,0 +1,109 @@ +import { GridMetricCalculator } from '@deephaven/grid'; +import { dh } from '@deephaven/jsapi-types'; +import { TestUtils } from '@deephaven/utils'; +import { + IrisGridMetricCalculator, + type IrisGridMetricState, +} from './IrisGridMetricCalculator'; +import IrisGridModel from './IrisGridModel'; + +const { createMockProxy } = TestUtils; + +function makeColumns(count = 5): dh.Column[] { + return Array.from({ length: count }, (_, i) => + createMockProxy({ + name: `Column${i + 1}`, + type: 'java.lang.String', + }) + ); +} + +function makeGridMetricState(model: IrisGridModel): IrisGridMetricState { + return createMockProxy({ + model, + draggingColumn: undefined, + }); +} + +// Spy on GridMetricCalculator.getMetrics +jest.spyOn(GridMetricCalculator.prototype, 'getMetrics'); + +// Spy on GridMetricCalculator.setColumnWidth +jest.spyOn(GridMetricCalculator.prototype, 'setColumnWidth'); + +describe('IrisGridMetricCalculator', () => { + let calculator: IrisGridMetricCalculator; + let model; + let state: IrisGridMetricState; + let columns: dh.Column[]; + + beforeEach(() => { + columns = makeColumns(); + model = createMockProxy({ + get columns() { + return columns; + }, + getColumnIndexByName: name => { + const index = columns.findIndex(col => col.name === name); + return index !== -1 ? index : undefined; + }, + }); + calculator = new IrisGridMetricCalculator(); + state = makeGridMetricState(model); + }); + + it('preserves column width based on column name instead of index', () => { + expect(calculator.getUserColumnWidths().size).toBe(0); + + expect(model.getColumnIndexByName('Column1')).toBe(0); + + // setColumnWidth requires getMetrics call + calculator.getMetrics(state); + calculator.setColumnWidth(model.getColumnIndexByName('Column1'), 100); + calculator.setColumnWidth(model.getColumnIndexByName('Column2'), 200); + calculator.setColumnWidth(model.getColumnIndexByName('Column3'), 300); + + // Calling getMetrics to update user column widths + calculator.getMetrics(state); + expect(calculator.getUserColumnWidths().size).toBe(3); + + // Delete Column2 + columns = columns.filter(col => col.name !== 'Column2'); + + expect(state.model.columns[1].name).toBe('Column3'); + + calculator.getMetrics(state); + expect([...calculator.getUserColumnWidths().entries()]).toEqual([ + [0, 100], + [1, 300], + ]); + + // Restore Column2 + columns = [ + columns[0], + createMockProxy({ + name: 'Column2', + type: 'java.lang.String', + }), + ...columns.slice(1), + ]; + + calculator.getMetrics(state); + expect([...calculator.getUserColumnWidths().entries()]).toEqual([ + [0, 100], + [1, 200], + [2, 300], + ]); + }); + + it('setColumnWidth updates user column width', () => { + calculator.getMetrics(state); + calculator.setColumnWidth(model.getColumnIndexByName('Column1'), 100); + + expect(calculator.getUserColumnWidths().size).toBe(1); + expect(calculator.getUserColumnWidths().get(0)).toBe(100); + + calculator.setColumnWidth(model.getColumnIndexByName('Column1'), 150); + expect(calculator.getUserColumnWidths().get(0)).toBe(150); + }); +}); diff --git a/packages/iris-grid/src/IrisGridMetricCalculator.ts b/packages/iris-grid/src/IrisGridMetricCalculator.ts index 1c08c6b99..8c7b1c632 100644 --- a/packages/iris-grid/src/IrisGridMetricCalculator.ts +++ b/packages/iris-grid/src/IrisGridMetricCalculator.ts @@ -1,31 +1,101 @@ -import { GridMetricCalculator, ModelSizeMap } from '@deephaven/grid'; +import deepEqual from 'fast-deep-equal'; +import memoizeOne from 'memoize-one'; +import { + GridMetricCalculator, + GridMetrics, + ModelIndex, + ModelSizeMap, + trimMap, +} from '@deephaven/grid'; import type { GridMetricState } from '@deephaven/grid'; import type { dh } from '@deephaven/jsapi-types'; +import { assertNotNull } from '@deephaven/utils'; import type IrisGridModel from './IrisGridModel'; import { IrisGridThemeType } from './IrisGridTheme'; +import { + ColumnName, + ReadonlyAdvancedFilterMap, + ReadonlyQuickFilterMap, +} from './CommonTypes'; export interface IrisGridMetricState extends GridMetricState { model: IrisGridModel; theme: IrisGridThemeType; isFilterBarShown: boolean; - advancedFilters: Map< - string, - { options: unknown; filter: dh.FilterCondition | null } - >; - quickFilters: Map< - string, - { text: string; filter: dh.FilterCondition | null } - >; - sorts: dh.Sort[]; + advancedFilters: ReadonlyAdvancedFilterMap; + quickFilters: ReadonlyQuickFilterMap; + sorts: readonly dh.Sort[]; reverse: boolean; } -/** - * Class to calculate all the metrics for a grid. - * Call getMetrics() with the state to get metrics - */ export class IrisGridMetricCalculator extends GridMetricCalculator { + // Column widths by name to keep track of columns going in and out of viewport + userColumnWidthsByName: Map = new Map(); + + // Cached model column names to detect when the column width map update is necessary + private cachedModelColumnNames: readonly ColumnName[] | undefined; + + private getCachedCurrentModelColumnNames = memoizeOne( + (columns: readonly dh.Column[]) => columns.map(col => col.name) + ); + + private updateCalculatedColumnWidths(model: IrisGridModel): void { + assertNotNull(this.cachedModelColumnNames); + const calculatedColumnWidthsByName = new Map(); + this.cachedModelColumnNames.forEach((name, index) => { + const prevColumnWidth = this.calculatedColumnWidths.get(index); + if (prevColumnWidth != null) { + calculatedColumnWidthsByName.set(name, prevColumnWidth); + } + }); + this.resetCalculatedColumnWidths(); + calculatedColumnWidthsByName.forEach((width, name) => { + const index = model.getColumnIndexByName(name); + if (index != null) { + this.calculatedColumnWidths.set(index, width); + } + }); + trimMap(this.calculatedColumnWidths); + } + + /** + * Updates the user column widths based on the current model state + * @param model The current IrisGridModel + */ + private updateUserColumnWidths(model: IrisGridModel): void { + this.userColumnWidths = new Map(); + this.userColumnWidthsByName.forEach((width, name) => { + const modelIndex = model.getColumnIndexByName(name); + if (modelIndex != null) { + super.setColumnWidth(modelIndex, width); + } + }); + } + + /** + * Updates the user and calculated column widths if the model columns have changed + * @param model The current IrisGridModel + */ + private updateColumnWidthsIfNecessary(model: IrisGridModel): void { + // Comparing model.columns references wouldn't work here because + // the reference can change in the model without the actual column definitions changing + if ( + this.cachedModelColumnNames != null && + !deepEqual( + this.getCachedCurrentModelColumnNames(model.columns), + this.cachedModelColumnNames + ) + ) { + // Preserve column widths when possible to minimize visual shifts in the grid layout + this.updateCalculatedColumnWidths(model); + this.updateUserColumnWidths(model); + } + this.cachedModelColumnNames = model.columns.map(col => col.name); + } + getGridY(state: IrisGridMetricState): number { + // The state here seems to be a GridMetricState with stateOverrides passed from IrisGrid in the props, + // not guaranteed to be IrisGridMetricState let gridY = super.getGridY(state); const { isFilterBarShown, @@ -50,7 +120,67 @@ export class IrisGridMetricCalculator extends GridMetricCalculator { return gridY; } + /** + * Gets the metrics for the current state. This method has to be called before setColumnSize or resetColumnSize. + * @param state The current IrisGridMetricState + * @returns The metrics for the current state + */ + getMetrics(state: IrisGridMetricState): GridMetrics { + const { model } = state; + // Update column widths if columns in the cached model don't match the current model passed in the state + this.updateColumnWidthsIfNecessary(model); + + return super.getMetrics(state); + } + + /** + * Sets the width for a specific column by index + * @param column The index of the column to set + * @param size The new width for the column + */ + setColumnWidth(column: number, size: number): void { + super.setColumnWidth(column, size); + assertNotNull( + this.cachedModelColumnNames, + 'setColumnWidth should be called after getMetrics' + ); + const name = this.cachedModelColumnNames[column]; + if (name != null) { + this.userColumnWidthsByName.set(name, size); + trimMap(this.userColumnWidthsByName); + } + } + + /** + * Resets the width for a specific column by index + * @param column The index of the column to reset + */ + resetColumnWidth(column: number): void { + super.resetColumnWidth(column); + assertNotNull( + this.cachedModelColumnNames, + 'resetColumnWidth should be called after getMetrics' + ); + const name = this.cachedModelColumnNames[column]; + if (name != null) { + this.userColumnWidthsByName.delete(name); + } + } + + /** + * Resets all user column widths + */ + resetAllColumnWidths(): void { + this.userColumnWidths = new Map(); + this.userColumnWidthsByName = new Map(); + } + + /** + * Gets the user column widths + * @returns A map of user column widths + */ getUserColumnWidths(): ModelSizeMap { + // This might return stale data if getMetrics hasn't been called return this.userColumnWidths; } } diff --git a/packages/iris-grid/src/IrisGridModel.ts b/packages/iris-grid/src/IrisGridModel.ts index c1ab70f26..d0ec480dc 100644 --- a/packages/iris-grid/src/IrisGridModel.ts +++ b/packages/iris-grid/src/IrisGridModel.ts @@ -407,6 +407,13 @@ abstract class IrisGridModel< return false; } + /** + * @returns True if this model supports column groups and moved columns + */ + get isOrganizeColumnsAvailable(): boolean { + return false; + } + /** * @returns True if this model supports customColumns */ diff --git a/packages/iris-grid/src/IrisGridTableModel.ts b/packages/iris-grid/src/IrisGridTableModel.ts index 112bc59d7..1c00fb419 100644 --- a/packages/iris-grid/src/IrisGridTableModel.ts +++ b/packages/iris-grid/src/IrisGridTableModel.ts @@ -67,6 +67,10 @@ class IrisGridTableModel return this.table.selectDistinct != null; } + get isOrganizeColumnsAvailable(): boolean { + return true; + } + get isCustomColumnsAvailable(): boolean { return this.table.applyCustomColumns != null; } diff --git a/packages/iris-grid/src/IrisGridTreeTableModel.ts b/packages/iris-grid/src/IrisGridTreeTableModel.ts index 7178589ce..324dfb9b2 100644 --- a/packages/iris-grid/src/IrisGridTreeTableModel.ts +++ b/packages/iris-grid/src/IrisGridTreeTableModel.ts @@ -2,6 +2,7 @@ import memoize from 'memoize-one'; import { BoundedAxisRange, + ExpandableGridModel, GridCell, GridRange, ModelIndex, @@ -50,10 +51,10 @@ function isLayoutTreeTable(table: DhType.TreeTable): table is LayoutTreeTable { return (table as LayoutTreeTable).layoutHints !== undefined; } -class IrisGridTreeTableModel extends IrisGridTableModelTemplate< - DhType.TreeTable, - UITreeRow -> { +class IrisGridTreeTableModel + extends IrisGridTableModelTemplate + implements ExpandableGridModel +{ /** We keep a virtual column at the front that tracks the "group" that is expanded */ private virtualColumns: DisplayColumn[]; diff --git a/packages/iris-grid/src/IrisGridUtils.ts b/packages/iris-grid/src/IrisGridUtils.ts index b89a94053..7788946c7 100644 --- a/packages/iris-grid/src/IrisGridUtils.ts +++ b/packages/iris-grid/src/IrisGridUtils.ts @@ -41,7 +41,9 @@ import { FormattingRule as SidebarFormattingRule } from './sidebar/conditional-f import IrisGridModel from './IrisGridModel'; import type AdvancedSettingsType from './sidebar/AdvancedSettingsType'; import AdvancedSettings from './sidebar/AdvancedSettings'; -import ColumnHeaderGroup from './ColumnHeaderGroup'; +import ColumnHeaderGroup, { + ColumnHeaderGroupConfig, +} from './ColumnHeaderGroup'; import { isPartitionedGridModelProvider, PartitionConfig, @@ -1011,18 +1013,23 @@ class IrisGridUtils { * * @returns Object containing groups array, max depth, map of name to parent group, and map of name to group */ - static parseColumnHeaderGroups( + static parseColumnHeaderGroups< + T extends ColumnHeaderGroup, + C extends ColumnHeaderGroupConfig, + >( model: IrisGridModel, - groupsParam: readonly (DhType.ColumnGroup | ColumnHeaderGroup)[] + groupsParam: readonly (DhType.ColumnGroup | T)[], + makeColumnHeaderGroup: (args: C) => T = (args: C) => + new ColumnHeaderGroup(args) as T ): { - groups: ColumnHeaderGroup[]; + groups: T[]; maxDepth: number; - parentMap: Map; - groupMap: Map; + parentMap: Map; + groupMap: Map; } { let maxDepth = 1; - const parentMap: Map = new Map(); - const groupMap: Map = new Map(); + const parentMap: Map = new Map(); + const groupMap: Map = new Map(); // Remove any empty groups before parsing const groups = groupsParam?.filter( @@ -1036,9 +1043,7 @@ class IrisGridUtils { const originalGroupMap = new Map(groups.map(group => [group.name, group])); const seenChildren = new Set(); - const addGroup = ( - group: DhType.ColumnGroup | ColumnHeaderGroup - ): ColumnHeaderGroup => { + const addGroup = (group: DhType.ColumnGroup | T): T => { const { name } = group; assertNotNull(name, 'Column header group has no name'); @@ -1053,7 +1058,7 @@ class IrisGridUtils { return existingGroup; } - const childIndexes: ColumnHeaderGroup['childIndexes'] = []; + const childIndexes: T['childIndexes'] = []; let depth = 1; if (group.children == null) { @@ -1084,13 +1089,15 @@ class IrisGridUtils { } }); - const columnHeaderGroup = new ColumnHeaderGroup({ - ...group, + const headerGroupConfig = { + ...(group as C), name, children: group.children, depth, childIndexes: childIndexes.flat(), - }); + }; + + const columnHeaderGroup = makeColumnHeaderGroup(headerGroupConfig); groupMap.set(name, columnHeaderGroup); group.children?.forEach(childName => diff --git a/packages/iris-grid/src/index.ts b/packages/iris-grid/src/index.ts index d5dd276e4..d3c077be9 100644 --- a/packages/iris-grid/src/index.ts +++ b/packages/iris-grid/src/index.ts @@ -6,6 +6,7 @@ export * from './sidebar'; export * from './AdvancedFilterCreator'; export * from './CommonTypes'; export * from './mousehandlers'; +export * from './ColumnHeaderGroup'; export { default as ColumnHeaderGroup } from './ColumnHeaderGroup'; export * from './PartitionedGridModel'; export * from './IrisGrid'; @@ -30,3 +31,4 @@ export * from './IrisGridUtils'; export * from './IrisGridMetricCalculator'; export * from './IrisGridRenderer'; export * from './IrisGridCacheUtils'; +export { default as IrisGridCellRendererUtils } from './IrisGridCellRendererUtils'; diff --git a/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.test.tsx b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.test.tsx new file mode 100644 index 000000000..e737c4aec --- /dev/null +++ b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.test.tsx @@ -0,0 +1,116 @@ +import { TestUtils } from '@deephaven/utils'; +import { + ExpandableColumnGridModel, + GridMetrics, + type GridPoint, + type ModelIndex, +} from '@deephaven/grid'; +import { dh } from '@deephaven/jsapi-types'; +import { ContextActionUtils } from '@deephaven/components'; +import IrisGridContextMenuHandler from './IrisGridContextMenuHandler'; +import IrisGrid, { IrisGridProps, IrisGridState } from '../IrisGrid'; +import IrisGridModel from '../IrisGridModel'; +import { IrisGridThemeType } from '../IrisGridTheme'; + +const { createMockProxy } = TestUtils; + +function makeColumns(count = 5): dh.Column[] { + return Array.from({ length: count }, (_, i) => + createMockProxy({ + name: `Column${i + 1}`, + type: 'java.lang.String', + }) + ); +} + +function makeMockModel({ + columns = makeColumns(), + hasExpandableColumns = true, + isExpandAllColumnsAvailable = true, +}: { + columns?: readonly dh.Column[]; + hasExpandableColumns?: boolean; + isExpandAllColumnsAvailable?: boolean; +} = {}): IrisGridModel & ExpandableColumnGridModel { + return createMockProxy({ + hasExpandableColumns, + isExpandAllColumnsAvailable, + columns, + isColumnExpandable: jest.fn(() => true), + isColumnExpanded: jest.fn(() => false), + }); +} + +function makeMockIrisGrid({ + model = makeMockModel(), + theme = createMockProxy({}), +}: { + model?: IrisGridModel & ExpandableColumnGridModel; + theme?: IrisGridThemeType; +} = {}): IrisGrid { + return createMockProxy({ + props: createMockProxy({ + model, + theme, + }), + state: createMockProxy({ + metrics: createMockProxy({ + userColumnWidths: new Map( + model.columns.map((_col, index) => [index, 100] as [number, number]) + ), + }), + advancedFilters: new Map(), + quickFilters: new Map(), + }), + getTheme: jest.fn().mockReturnValue(theme), + }); +} + +describe('getHeaderActions', () => { + const mockDh = createMockProxy(); + const mockGridPoint: GridPoint = { + column: 0 as ModelIndex, + row: null, + x: 0, + y: 0, + columnHeaderDepth: 0, + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it.each([ + [true, true, true], + [true, false, false], + [false, true, false], + [false, false, false], + ])( + 'shows correct actions when hasExpandableColumns=%s, isExpandAllColumnsAvailable=%s', + ( + hasExpandableColumns, + isExpandAllColumnsAvailable, + shouldShowExpandCollapseAll + ) => { + const model = makeMockModel({ + hasExpandableColumns, + isExpandAllColumnsAvailable, + }); + const handler = new IrisGridContextMenuHandler( + makeMockIrisGrid({ model }), + mockDh + ); + const menuItems = ContextActionUtils.getMenuItems( + handler.getHeaderActions(0, mockGridPoint), + false + ); + + expect(menuItems.some(a => a.title === 'Expand All Columns')).toBe( + shouldShowExpandCollapseAll + ); + expect(menuItems.some(a => a.title === 'Collapse All Columns')).toBe( + shouldShowExpandCollapseAll + ); + } + ); +}); diff --git a/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx index cf73e66a9..d76e86d55 100644 --- a/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx +++ b/packages/iris-grid/src/mousehandlers/IrisGridContextMenuHandler.tsx @@ -26,6 +26,7 @@ import { GridSelectionMouseHandler, isDeletableGridModel, isEditableGridModel, + isExpandableColumnGridModel, isExpandableGridModel, ModelIndex, } from '@deephaven/grid'; @@ -239,6 +240,29 @@ class IrisGridContextMenuHandler extends GridMouseHandler { }, disabled: !isColumnHidden, }); + + if (isExpandableColumnGridModel(model) && model.hasExpandableColumns) { + if (model.isExpandAllColumnsAvailable) { + actions.push({ + title: 'Expand All Columns', + group: IrisGridContextMenuHandler.GROUP_EXPAND_COLLAPSE, + order: 30, + action: () => { + this.irisGrid.expandAllColumns(); + }, + }); + + actions.push({ + title: 'Collapse All Columns', + group: IrisGridContextMenuHandler.GROUP_EXPAND_COLLAPSE, + order: 40, + action: () => { + this.irisGrid.collapseAllColumns(); + }, + }); + } + } + actions.push({ title: 'Quick Filters', icon: vsRemove, @@ -249,6 +273,7 @@ class IrisGridContextMenuHandler extends GridMouseHandler { action: () => { this.irisGrid.toggleFilterBar(visibleIndex); }, + disabled: !model.isFilterable(modelIndex), }); actions.push({ title: 'Advanced Filters', diff --git a/packages/utils/src/TypeUtils.ts b/packages/utils/src/TypeUtils.ts index 75a7fc267..56bd7de5f 100644 --- a/packages/utils/src/TypeUtils.ts +++ b/packages/utils/src/TypeUtils.ts @@ -105,3 +105,16 @@ export type UndoPartial = T extends Partial ? U : never; * type A = typeof x[keyof typeof x]; // 1 | 2 | 3 */ export type ValueOf = T[keyof T]; + +/** + * Extracts the tail of a tuple type after the first element. + * + * e.g. Given + * declare const x: [{ a: 1 }, { b: 2 }, { c: 3 }]; + * + * The tail type can be extracted like this: + * type A = Tail; // [{ b: 2 }, { c: 3 }] + */ +export type Tail = T extends [unknown, ...infer U] + ? U + : []; diff --git a/tests/table-multiselect.spec.ts b/tests/table-multiselect.spec.ts index f3b90991e..9ed721de7 100644 --- a/tests/table-multiselect.spec.ts +++ b/tests/table-multiselect.spec.ts @@ -55,7 +55,7 @@ async function filterAndScreenshot( await page.getByRole('button', { name: 'Filter by Values' }).hover(); await expectContextMenus(page, 2); await page.getByRole('button', { name: filterType, exact: true }).click(); - await waitForLoadingDone(page, '.iris-grid-loading-status-bar'); + await waitForLoadingDone(page, '.iris-panel-scrim-background'); await expect(page.locator('.iris-grid-column')).toHaveScreenshot( screenshotName ); diff --git a/tests/table-operations.spec.ts-snapshots/custom-column-3-chromium-linux.png b/tests/table-operations.spec.ts-snapshots/custom-column-3-chromium-linux.png index 9e26b8cb6..d2adc1ee9 100644 Binary files a/tests/table-operations.spec.ts-snapshots/custom-column-3-chromium-linux.png and b/tests/table-operations.spec.ts-snapshots/custom-column-3-chromium-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/custom-column-3-firefox-linux.png b/tests/table-operations.spec.ts-snapshots/custom-column-3-firefox-linux.png index 2ae567f07..b8734fabe 100644 Binary files a/tests/table-operations.spec.ts-snapshots/custom-column-3-firefox-linux.png and b/tests/table-operations.spec.ts-snapshots/custom-column-3-firefox-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-chromium-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-chromium-linux.png index 62e6ccb06..71a65e166 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-chromium-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-chromium-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-firefox-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-firefox-linux.png index a12b482bd..243ed0bb0 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-firefox-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-firefox-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-webkit-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-webkit-linux.png index 2dc3fada6..94dfb3ca3 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-webkit-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-10-webkit-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-chromium-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-chromium-linux.png index fbf0410db..5ab08debd 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-chromium-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-chromium-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-firefox-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-firefox-linux.png index 3ee466061..4b69cbd8f 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-firefox-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-firefox-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-webkit-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-webkit-linux.png index 307d20d32..9d5b4e503 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-webkit-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-11-webkit-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-chromium-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-chromium-linux.png index 5c103a5da..102d953e6 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-chromium-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-chromium-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-firefox-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-firefox-linux.png index a58d42460..3ae8e4e6a 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-firefox-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-firefox-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-webkit-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-webkit-linux.png index b20651d22..0b54f6d27 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-webkit-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-7-webkit-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-chromium-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-chromium-linux.png index c5375146d..81b0b5075 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-chromium-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-chromium-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-firefox-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-firefox-linux.png index cf3923fa9..488da9dae 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-firefox-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-firefox-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-webkit-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-webkit-linux.png index 87ab27f1e..498940ba0 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-webkit-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-8-webkit-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-chromium-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-chromium-linux.png index e59749490..2cb66a9a1 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-chromium-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-chromium-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-firefox-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-firefox-linux.png index 2a9b9db16..3fadc5250 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-firefox-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-firefox-linux.png differ diff --git a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-webkit-linux.png b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-webkit-linux.png index 52efb32cc..6881e1793 100644 Binary files a/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-webkit-linux.png and b/tests/table-operations.spec.ts-snapshots/rollup-rows-and-aggregrate-columns-9-webkit-linux.png differ diff --git a/tests/utils.ts b/tests/utils.ts index ea520c7ae..2426813a9 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -94,6 +94,7 @@ export async function openTable( await expect( page.locator('.iris-grid .iris-grid-loading-status') ).toHaveCount(0); + await expect(page.locator('.iris-panel-scrim-background')).toHaveCount(0); await expect(page.locator('.grid-wrapper')).toHaveCount(panelCount + 1); } }