From d5cfb1811b46703d84ed9b782b3bec6dd51837e4 Mon Sep 17 00:00:00 2001 From: gjmooney Date: Thu, 28 Mar 2024 09:59:36 +0100 Subject: [PATCH 1/4] Add hide column option to context menu Signed-off-by: gjmooney Signed-off-by: Greg --- js/core/gridContextMenu.ts | 21 +++++++++++++ js/core/transform.ts | 25 +++++++++++++++- js/core/transformExecutors.ts | 48 +++++++++++++++++++++++++++++- js/core/transformStateManager.ts | 27 ++++++++++++++--- js/core/view.ts | 1 + js/core/viewbasedjsonmodel.ts | 4 ++- js/feathergrid.ts | 51 ++++++++++++++++++++++++++++++++ 7 files changed, 170 insertions(+), 7 deletions(-) diff --git a/js/core/gridContextMenu.ts b/js/core/gridContextMenu.ts index 0f44268f..3156b1c6 100644 --- a/js/core/gridContextMenu.ts +++ b/js/core/gridContextMenu.ts @@ -97,6 +97,13 @@ export class FeatherGridContextMenu extends GridContextMenu { // Add menu items based on the region of the grid that was clicked on. switch (hit.region) { case 'column-header': + this._menu.addItem({ + command: FeatherGridContextMenu.CommandID.HideColumn, + args: args, + }); + this._menu.addItem({ + type: 'separator', + }); this._menu.addItem({ command: FeatherGridContextMenu.CommandID.SortAscending, args: args, @@ -141,6 +148,17 @@ export class FeatherGridContextMenu extends GridContextMenu { }); break; case 'corner-header': + this._menu.addItem({ + command: FeatherGridContextMenu.CommandID.ShowAllColumns, + args: args, + }); + this._menu.addItem({ + command: FeatherGridContextMenu.CommandID.HideAllColumns, + args: args, + }); + this._menu.addItem({ + type: 'separator', + }); this._menu.addItem({ command: FeatherGridContextMenu.CommandID.SortAscending, args: args, @@ -258,6 +276,9 @@ export namespace FeatherGridContextMenu { SaveSelectionAsCsv = 'saveSelectionAsCsv', SaveAllAsCsv = 'saveAllAsCsv', ClearSelection = 'clearSelection', + HideColumn = 'hideColumn', + HideAllColumns = 'hideAllColumns', + ShowAllColumns = 'showAllColumns', } /** diff --git a/js/core/transform.ts b/js/core/transform.ts index 23584691..4f7fbd95 100644 --- a/js/core/transform.ts +++ b/js/core/transform.ts @@ -2,7 +2,10 @@ * A declarative spec for specifying transformations. */ export namespace Transform { - export type TransformSpec = Transform.Sort | Transform.Filter; + export type TransformSpec = + | Transform.Sort + | Transform.Filter + | Transform.Hide; /** * A declarative spec for the `Sort` transformation. @@ -30,6 +33,26 @@ export namespace Transform { desc: boolean; }; + /** + * A declarative spec for the `Hide` transformation. + */ + export type Hide = { + /** + * A type alias for this trasformation. + */ + type: 'hide'; + + /** + * The column in the data schema to apply the transformation to. + */ + columnIndex: number; + + /** + * Boolean indicating if all columns should be hidden + */ + hideAll: boolean; + }; + /** * A declarative spec for the `Filter` transformation. */ diff --git a/js/core/transformExecutors.ts b/js/core/transformExecutors.ts index bd67ce23..dabe8c8e 100644 --- a/js/core/transformExecutors.ts +++ b/js/core/transformExecutors.ts @@ -1,7 +1,7 @@ import { Dict } from '@jupyter-widgets/base'; -import { Transform } from './transform'; import { DataSource } from '../datasource'; +import { Transform } from './transform'; import * as moment from 'moment'; @@ -25,6 +25,52 @@ export namespace TransformExecutor { export type IData = Readonly; } +export class HideExecutor extends TransformExecutor { + constructor(options: HideExecutor.IOptions) { + super(); + this._options = options; + } + + // input has data and schema + public apply(input: TransformExecutor.IData): TransformExecutor.IData { + const newSchema = { ...input.schema }; + + if (this._options.hideAll) { + newSchema.fields = newSchema.fields.filter( + (field) => field.name === 'key', + ); + } else { + newSchema.fields = newSchema.fields.filter( + (field) => field.name !== this._options.field, + ); + } + + return new DataSource(input.data, input.fields, newSchema); + } + + protected _options: HideExecutor.IOptions; +} + +/** + * The namespace for the `HideExecutor` class statics. + */ +export namespace HideExecutor { + /** + * An options object for initializing a Hide. + */ + export interface IOptions { + /** + * The name of the field to hide in the data source. + */ + field: string; + + /** + * Boolean indicating if all columns should be hidden + */ + hideAll: boolean; + } +} + /** * A transformation that filters a single field by the provided operator and * value. diff --git a/js/core/transformStateManager.ts b/js/core/transformStateManager.ts index 6a6272f4..5d9fde08 100644 --- a/js/core/transformStateManager.ts +++ b/js/core/transformStateManager.ts @@ -3,8 +3,9 @@ import { Transform } from './transform'; import { View } from './view'; import { - SortExecutor, FilterExecutor, + HideExecutor, + SortExecutor, TransformExecutor, } from './transformExecutors'; @@ -12,7 +13,7 @@ import { each } from '@lumino/algorithm'; import { JSONExt } from '@lumino/coreutils'; -import { Signal, ISignal } from '@lumino/signaling'; +import { ISignal, Signal } from '@lumino/signaling'; import { DataSource } from '../datasource'; /** @@ -25,6 +26,7 @@ export class TransformStateManager { this._state[transform.column] = { sort: undefined, filter: undefined, + hide: undefined, }; } @@ -41,6 +43,9 @@ export class TransformStateManager { case 'filter': this._state[transform.column]['filter'] = transform; break; + case 'hide': + this._state[transform.columnIndex]['hide'] = transform; + break; default: throw 'unreachable'; } @@ -110,6 +115,7 @@ export class TransformStateManager { private _createExecutors(data: Readonly): TransformExecutor[] { const sortExecutors: SortExecutor[] = []; const filterExecutors: FilterExecutor[] = []; + const hideExecutors: HideExecutor[] = []; Object.keys(this._state).forEach((column) => { const transform: TransformStateManager.IColumn = this._state[column]; @@ -146,10 +152,17 @@ export class TransformStateManager { }); filterExecutors.push(executor); } + if (transform.hide) { + const executor = new HideExecutor({ + field: data.schema.fields[transform.hide.columnIndex]['name'], + hideAll: transform.hide.hideAll, + }); + hideExecutors.push(executor); + } }); // Always put filters first - return [...filterExecutors, ...sortExecutors]; + return [...filterExecutors, ...sortExecutors, ...hideExecutors]; } /** @@ -171,10 +184,12 @@ export class TransformStateManager { columnState.sort = undefined; } else if (transformType === 'filter') { columnState.filter = undefined; + } else if (transformType === 'hide') { + columnState.hide = undefined; } else { throw 'unreachable'; } - if (!columnState.sort && !columnState.filter) { + if (!columnState.sort && !columnState.filter && !columnState.hide) { delete this._state[column]; } this._changed.emit({ @@ -229,6 +244,9 @@ export class TransformStateManager { if (this._state[column].filter) { transforms.push(this._state[column].filter!); } + if (this._state[column].hide) { + transforms.push(this._state[column].hide!); + } }); return transforms; } @@ -255,6 +273,7 @@ export namespace TransformStateManager { export interface IColumn { filter: Transform.Filter | undefined; sort: Transform.Sort | undefined; + hide: Transform.Hide | undefined; } export interface IState { [key: string]: IColumn; diff --git a/js/core/view.ts b/js/core/view.ts index 05b3daa3..5a16885b 100644 --- a/js/core/view.ts +++ b/js/core/view.ts @@ -42,6 +42,7 @@ export class View { if (region === 'body') { return this._data.length; } else { + // If there are no body rows, return one for the header row return this._bodyFields[0]?.rows.length ?? 1; } } diff --git a/js/core/viewbasedjsonmodel.ts b/js/core/viewbasedjsonmodel.ts index 37802479..8dcf0a1a 100644 --- a/js/core/viewbasedjsonmodel.ts +++ b/js/core/viewbasedjsonmodel.ts @@ -10,8 +10,8 @@ import { View } from './view'; import { TransformStateManager } from './transformStateManager'; -import { ArrayUtils } from '../utils'; import { DataSource } from '../datasource'; +import { ArrayUtils } from '../utils'; /** * A view based data model implementation for in-memory JSON data. @@ -76,6 +76,7 @@ export class ViewBasedJSONModel extends MutableDataModel { */ updateDataset(data: DataSource): void { this._dataset = data; + // does not happen when selecting hide column this._updatePrimaryKeyMap(); const view = new View(this._dataset); this.currentView = view; @@ -566,6 +567,7 @@ export class ViewBasedJSONModel extends MutableDataModel { } // We need to rerun the transforms, as the changed cells may change the order // or visibility of other rows + console.log('update row value'); this.currentView = this._transformState.createView(this._dataset); } diff --git a/js/feathergrid.ts b/js/feathergrid.ts index 8222d68c..d31f0306 100644 --- a/js/feathergrid.ts +++ b/js/feathergrid.ts @@ -1070,6 +1070,57 @@ export class FeatherGrid extends Widget { this.grid.selectionModel?.clear(); }, }); + commands.addCommand(FeatherGridContextMenu.CommandID.HideColumn, { + label: 'Hide Column', + mnemonic: -1, + execute: (args) => { + const cellClick: FeatherGridContextMenu.CommandArgs = + args as FeatherGridContextMenu.CommandArgs; + const colIndex = this._dataModel.getSchemaIndex( + cellClick.region, + cellClick.columnIndex, + ); + this._dataModel.addTransform({ + type: 'hide', + columnIndex: colIndex, + hideAll: false, + }); + }, + }); + commands.addCommand(FeatherGridContextMenu.CommandID.HideAllColumns, { + label: 'Hide All Columns', + mnemonic: -1, + execute: (args) => { + const cellClick: FeatherGridContextMenu.CommandArgs = + args as FeatherGridContextMenu.CommandArgs; + const colIndex = this._dataModel.getSchemaIndex( + cellClick.region, + cellClick.columnIndex, + ); + this._dataModel.addTransform({ + type: 'hide', + columnIndex: colIndex, + hideAll: true, + }); + }, + }); + commands.addCommand(FeatherGridContextMenu.CommandID.ShowAllColumns, { + label: 'Show All Columns', + mnemonic: -1, + execute: () => { + const activeTransforms: Transform.TransformSpec[] = + this._dataModel.activeTransforms; + console.log( + 'this._dataModel.activeTransforms', + this._dataModel.activeTransforms, + ); + const newTransforms = activeTransforms.filter( + (val) => val.type !== 'hide', + ); + console.log('newTransforms', newTransforms); + this._dataModel.replaceTransforms(newTransforms); + }, + }); return commands; } From 7f2638605ec31f5565f48795f49ec5efcdc8cc5b Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 12 Jun 2024 13:11:05 +0200 Subject: [PATCH 2/4] Get hide columns working Signed-off-by: Greg --- js/core/transform.ts | 7 ++++++- js/core/transformExecutors.ts | 5 +++++ js/core/transformStateManager.ts | 12 ++++++++++-- js/core/viewbasedjsonmodel.ts | 2 -- js/feathergrid.ts | 23 +++++++++++++++-------- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/js/core/transform.ts b/js/core/transform.ts index 4f7fbd95..dff659ab 100644 --- a/js/core/transform.ts +++ b/js/core/transform.ts @@ -38,10 +38,15 @@ export namespace Transform { */ export type Hide = { /** - * A type alias for this trasformation. + * A type alias for this transformation. */ type: 'hide'; + /** + * The column in the data schema to apply the transformation to. + */ + column: string; + /** * The column in the data schema to apply the transformation to. */ diff --git a/js/core/transformExecutors.ts b/js/core/transformExecutors.ts index dabe8c8e..c505a2b7 100644 --- a/js/core/transformExecutors.ts +++ b/js/core/transformExecutors.ts @@ -64,6 +64,11 @@ export namespace HideExecutor { */ field: string; + /** + * The data type of the column associated with this transform. + */ + dType: string; + /** * Boolean indicating if all columns should be hidden */ diff --git a/js/core/transformStateManager.ts b/js/core/transformStateManager.ts index 5d9fde08..df05ab90 100644 --- a/js/core/transformStateManager.ts +++ b/js/core/transformStateManager.ts @@ -44,7 +44,7 @@ export class TransformStateManager { this._state[transform.column]['filter'] = transform; break; case 'hide': - this._state[transform.columnIndex]['hide'] = transform; + this._state[transform.column]['hide'] = transform; break; default: throw 'unreachable'; @@ -153,8 +153,16 @@ export class TransformStateManager { filterExecutors.push(executor); } if (transform.hide) { + let dType = ''; + for (const field of data.schema.fields) { + if (field.name === transform.hide.column) { + dType = field.type; + } + } + const executor = new HideExecutor({ - field: data.schema.fields[transform.hide.columnIndex]['name'], + field: transform.hide.column, + dType, hideAll: transform.hide.hideAll, }); hideExecutors.push(executor); diff --git a/js/core/viewbasedjsonmodel.ts b/js/core/viewbasedjsonmodel.ts index 8dcf0a1a..5e335b36 100644 --- a/js/core/viewbasedjsonmodel.ts +++ b/js/core/viewbasedjsonmodel.ts @@ -76,7 +76,6 @@ export class ViewBasedJSONModel extends MutableDataModel { */ updateDataset(data: DataSource): void { this._dataset = data; - // does not happen when selecting hide column this._updatePrimaryKeyMap(); const view = new View(this._dataset); this.currentView = view; @@ -567,7 +566,6 @@ export class ViewBasedJSONModel extends MutableDataModel { } // We need to rerun the transforms, as the changed cells may change the order // or visibility of other rows - console.log('update row value'); this.currentView = this._transformState.createView(this._dataset); } diff --git a/js/feathergrid.ts b/js/feathergrid.ts index d31f0306..538cb12d 100644 --- a/js/feathergrid.ts +++ b/js/feathergrid.ts @@ -1074,14 +1074,20 @@ export class FeatherGrid extends Widget { label: 'Hide Column', mnemonic: -1, execute: (args) => { - const cellClick: FeatherGridContextMenu.CommandArgs = + const command: FeatherGridContextMenu.CommandArgs = args as FeatherGridContextMenu.CommandArgs; + const colIndex = this._dataModel.getSchemaIndex( - cellClick.region, - cellClick.columnIndex, + command.region, + command.columnIndex, ); + + const column = + this.dataModel.currentView.dataset.schema.fields[colIndex]; + this._dataModel.addTransform({ type: 'hide', + column: column.name, columnIndex: colIndex, hideAll: false, }); @@ -1097,8 +1103,13 @@ export class FeatherGrid extends Widget { cellClick.region, cellClick.columnIndex, ); + + const column = + this.dataModel.currentView.dataset.schema.fields[colIndex]; + this._dataModel.addTransform({ type: 'hide', + column: column.name, columnIndex: colIndex, hideAll: true, }); @@ -1110,14 +1121,10 @@ export class FeatherGrid extends Widget { execute: () => { const activeTransforms: Transform.TransformSpec[] = this._dataModel.activeTransforms; - console.log( - 'this._dataModel.activeTransforms', - this._dataModel.activeTransforms, - ); + const newTransforms = activeTransforms.filter( (val) => val.type !== 'hide', ); - console.log('newTransforms', newTransforms); this._dataModel.replaceTransforms(newTransforms); }, }); From 58284712258d9e0643c77716d0dcd2081c10242b Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 12 Jun 2024 13:42:41 +0200 Subject: [PATCH 3/4] Leave index column when hiding all columns Signed-off-by: Greg --- js/core/transformExecutors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/core/transformExecutors.ts b/js/core/transformExecutors.ts index c505a2b7..b2881329 100644 --- a/js/core/transformExecutors.ts +++ b/js/core/transformExecutors.ts @@ -35,9 +35,10 @@ export class HideExecutor extends TransformExecutor { public apply(input: TransformExecutor.IData): TransformExecutor.IData { const newSchema = { ...input.schema }; + console.log('input.schema', input.schema); if (this._options.hideAll) { newSchema.fields = newSchema.fields.filter( - (field) => field.name === 'key', + (field) => field.name === 'index_column', ); } else { newSchema.fields = newSchema.fields.filter( From fd265edbbc660cf8cc8a56bd9dc1d7a6fce7d8b4 Mon Sep 17 00:00:00 2001 From: Greg Date: Wed, 12 Jun 2024 14:12:57 +0200 Subject: [PATCH 4/4] Add hide transform to tests Signed-off-by: Greg --- js/core/transform.ts | 2 +- tests/js/transformStateManager.test.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/js/core/transform.ts b/js/core/transform.ts index dff659ab..26c16a5e 100644 --- a/js/core/transform.ts +++ b/js/core/transform.ts @@ -50,7 +50,7 @@ export namespace Transform { /** * The column in the data schema to apply the transformation to. */ - columnIndex: number; + columnIndex?: number; /** * Boolean indicating if all columns should be hidden diff --git a/tests/js/transformStateManager.test.ts b/tests/js/transformStateManager.test.ts index b2a5b244..04d67534 100644 --- a/tests/js/transformStateManager.test.ts +++ b/tests/js/transformStateManager.test.ts @@ -1,7 +1,7 @@ -import { TransformStateManager } from '../../js/core/transformStateManager'; import { Transform } from '../../js/core/transform'; -import { DataGenerator } from './testUtils'; +import { TransformStateManager } from '../../js/core/transformStateManager'; import { View } from '../../js/core/view'; +import { DataGenerator } from './testUtils'; describe('Test .add()', () => { const testCases: Transform.TransformSpec[] = [ @@ -229,10 +229,14 @@ namespace Private { return { type: 'filter', column: 'test', operator: '<', value: 0 }; } + export function simpleHide(): Transform.Hide { + return { type: 'hide', column: 'test', hideAll: false }; + } + /** * Returns a simple column of transform state. */ export function simpleState(): TransformStateManager.IColumn { - return { filter: simpleFilter(), sort: simpleSort() }; + return { filter: simpleFilter(), sort: simpleSort(), hide: simpleHide() }; } }