From e4a34a0dff7c330afaa746b3b79a4ce7e0425a7d Mon Sep 17 00:00:00 2001 From: Ghislain B <61021172+ghiscoding-SE@users.noreply.github.com> Date: Tue, 19 May 2020 10:24:04 -0400 Subject: [PATCH] fix(gridMenu): column picker list should include grouped header titles (#460) * fix(gridMenu): column picker list should include grouped header titles - add "headerColumnValueExtractor" callback in the global grid options to deal with grouped header titles - also add 2 other new options "contentMinWidth" and "useClickToRepositionMenu" - this requires a new SlickGrid core version with these new options --- package.json | 2 +- src/app/examples/grid-menu.component.ts | 13 +- .../angular-slickgrid/global-grid-options.ts | 13 +- .../models/gridMenu.interface.ts | 11 +- test/cypress/integration/example01.spec.js | 254 +++++++++++++++++- test/jest.config.js | 1 + 6 files changed, 284 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 43c514be3..18d9e6f6d 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "lodash.isequal": "^4.5.0", "moment-mini": "^2.22.1", "rxjs": "^6.3.3", - "slickgrid": "^2.4.22", + "slickgrid": "^2.4.23", "text-encoding-utf-8": "^1.0.2" }, "devDependencies": { diff --git a/src/app/examples/grid-menu.component.ts b/src/app/examples/grid-menu.component.ts index de99c460a..8b4016d19 100644 --- a/src/app/examples/grid-menu.component.ts +++ b/src/app/examples/grid-menu.component.ts @@ -79,10 +79,15 @@ export class GridMenuComponent implements OnInit { enableFiltering: true, enableCellNavigation: true, gridMenu: { - menuUsabilityOverride: (args) => { - // we could disable the menu entirely by returning false - return true; - }, + // we could disable the menu entirely by returning false depending on some code logic + menuUsabilityOverride: (args) => true, + + // use the click event position to reposition the grid menu (defaults to false) + // basically which offset do we want to use for reposition the grid menu, + // option1 is where we clicked (true) or option2 is where the icon button is located (false and is the defaults) + // you probably want to set this to True if you use an external grid menu button BUT set to False when using default grid menu + useClickToRepositionMenu: true, + // all titles optionally support translation keys, if you wish to use that feature then use the title properties with the 'Key' suffix (e.g: titleKey) // example "customTitle" for a plain string OR "customTitleKey" to use a translation key customTitleKey: 'CUSTOM_COMMANDS', diff --git a/src/app/modules/angular-slickgrid/global-grid-options.ts b/src/app/modules/angular-slickgrid/global-grid-options.ts index 770448111..9c1bbd038 100644 --- a/src/app/modules/angular-slickgrid/global-grid-options.ts +++ b/src/app/modules/angular-slickgrid/global-grid-options.ts @@ -1,4 +1,4 @@ -import { DelimiterType, FileType, GridOption, OperatorType } from './models/index'; +import { Column, DelimiterType, FileType, GridOption, OperatorType } from './models/index'; import { Filters } from './filters/index'; /** Global Grid Options Defaults */ @@ -133,7 +133,16 @@ export const GlobalGridOptions: Partial = { iconToggleFilterCommand: 'fa fa-random', iconTogglePreHeaderCommand: 'fa fa-random', menuWidth: 16, - resizeOnShowHeaderRow: true + resizeOnShowHeaderRow: true, + useClickToRepositionMenu: false, // use icon location to reposition instead + headerColumnValueExtractor: (column: Column) => { + const headerGroup = column && column.columnGroup || ''; + if (headerGroup) { + // when using Column Header Grouping, we'll prefix the column group title + return headerGroup + ' - ' + column.name; + } + return column && column.name || ''; + } }, headerMenu: { autoAlign: true, diff --git a/src/app/modules/angular-slickgrid/models/gridMenu.interface.ts b/src/app/modules/angular-slickgrid/models/gridMenu.interface.ts index 3e30cfac9..183855d74 100644 --- a/src/app/modules/angular-slickgrid/models/gridMenu.interface.ts +++ b/src/app/modules/angular-slickgrid/models/gridMenu.interface.ts @@ -4,6 +4,9 @@ import { MenuCallbackArgs } from './menuCallbackArgs.interface'; import { MenuCommandItemCallbackArgs } from './menuCommandItemCallbackArgs.interface'; export interface GridMenu { + /** Defaults to 0 (auto), minimum width of grid menu content (command, column list) */ + contentMinWidth?: number; + /** Array of Custom Items (title, command, disabled, ...) */ customItems?: Array; @@ -88,7 +91,7 @@ export interface GridMenu { /** Defaults to False, which leads to leaving the menu open after a click */ leaveOpen?: boolean; - /** Defaults to 16 pixels (only the number), which is the width in pixels of the Grid Menu icon */ + /** Defaults to 16 pixels (only the number), which is the width in pixels of the Grid Menu icon container */ menuWidth?: number; /** Defaults to False, which will resize the Header Row and remove the width of the Grid Menu icon from it's total width. */ @@ -100,9 +103,15 @@ export interface GridMenu { /** Same as "syncResizeTitle", except that it's a translation key which can be used on page load and/or when switching locale */ syncResizeTitleKey?: string; + /** Defaults to true, Use the Click offset to reposition the Grid Menu, when set to False it will use the icon offset to reposition the grid menu */ + useClickToRepositionMenu?: boolean; + // -- // action/override callbacks + /** Callback method to override the column name output used by the ColumnPicker/GridMenu. */ + headerColumnValueExtractor?: (column: Column) => string; + /** Callback method that user can override the default behavior of enabling/disabling an item from the list. */ menuUsabilityOverride?: (args: MenuCallbackArgs) => boolean; diff --git a/test/cypress/integration/example01.spec.js b/test/cypress/integration/example01.spec.js index 7460f85ef..047611f3d 100644 --- a/test/cypress/integration/example01.spec.js +++ b/test/cypress/integration/example01.spec.js @@ -1,7 +1,7 @@ /// describe('Example 1 - Basic Grids', () => { - const titles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + const fullTitles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; it('should display Example title', () => { cy.visit(`${Cypress.config('baseUrl')}`); @@ -26,7 +26,7 @@ describe('Example 1 - Basic Grids', () => { cy.get('#slickGridContainer-grid1') .find('.slick-header-columns') .children() - .each(($child, index) => expect($child.text()).to.eq(titles[index])); + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); }); it('should hover over the Title column and click on "Sort Descending" command', () => { @@ -198,4 +198,254 @@ describe('Example 1 - Basic Grids', () => { cy.get('[data-test=total-items]') .contains('995'); }); + + it('should open the Grid Menu on 1st Grid and expect all Columns to be checked', () => { + let gridUid = ''; + cy.get('#grid1') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('#grid1') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-gridmenu.${gridUid}`) + .find('.slick-gridmenu-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.children('input'); + const $label = $child.children('label'); + expect($input.attr('checked')).to.eq('checked'); + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + }); + + it('should then hide "Title" column from same 1st Grid and expect the column to be removed from 1st Grid', () => { + const newColumnList = ['Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven']; + cy.get('#grid1') + .get('.slick-gridmenu:visible') + .find('.slick-gridmenu-list') + .children('li:visible:nth(0)') + .children('label') + .should('contain', 'Title') + .click({ force: true }); + + cy.get('#grid1') + .get('.slick-gridmenu:visible') + .find('span.close') + .click({ force: true }); + + cy.get('#grid1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should open the Grid Menu off 2nd Grid and expect all Columns to still be all checked', () => { + let gridUid = ''; + cy.get('#grid2') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('#grid2') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-gridmenu.${gridUid}`) + .find('.slick-gridmenu-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.children('input'); + const $label = $child.children('label'); + expect($input.attr('checked')).to.eq('checked'); + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + }); + + it('should then hide "% Complete" column from this same 2nd Grid and expect the column to be removed from 2nd Grid', () => { + const newColumnList = ['Title', 'Duration (days)', 'Start', 'Finish', 'Effort Driven']; + cy.get('#grid2') + .get('.slick-gridmenu:visible') + .find('.slick-gridmenu-list') + .children('li:visible:nth(2)') + .children('label') + .should('contain', '% Complete') + .click({ force: true }); + + cy.get('#grid2') + .get('.slick-gridmenu:visible') + .find('span.close') + .click({ force: true }); + + cy.get('#grid2') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should go back to 1st Grid and open its Grid Menu and we expect this grid to stil have the "Title" column be hidden (unchecked)', () => { + cy.get('#grid1') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('.slick-gridmenu-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.children('input'); + const $label = $child.children('label'); + if ($label.text() === 'Title') { + expect($input.attr('checked')).to.eq(undefined); + } else { + expect($input.attr('checked')).to.eq('checked'); + } + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + + it('should hide "Start" column from 1st Grid and expect to have 2 hidden columns (Title, Start)', () => { + const newColumnList = ['Duration (days)', '% Complete', 'Finish', 'Effort Driven']; + cy.get('#grid1') + .get('.slick-gridmenu:visible') + .find('.slick-gridmenu-list') + .children('li:visible:nth(3)') + .children('label') + .should('contain', 'Start') + .click({ force: true }); + + cy.get('#grid1') + .get('.slick-gridmenu:visible') + .find('span.close') + .click({ force: true }); + + cy.get('#grid1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should open Column Picker of 2nd Grid and show the "% Complete" column back to visible', () => { + cy.get('#grid2') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitles[index]); + } + }); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children('li:nth-child(3)') + .children('label') + .should('contain', '% Complete') + .click(); + + cy.get('#grid2') + .find('.slick-header-columns') + .children() + .each(($child, index) => { + if (index <= 5) { + expect($child.text()).to.eq(fullTitles[index]); + } + }); + + cy.get('#grid2') + .get('.slick-columnpicker:visible') + .find('span.close') + .trigger('click') + .click(); + }); + + it('should open the Grid Menu on 2nd Grid and expect all Columns to be checked', () => { + let gridUid = ''; + cy.get('#grid2') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('#grid2') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-gridmenu.${gridUid}`) + .find('.slick-gridmenu-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.children('input'); + const $label = $child.children('label'); + expect($input.attr('checked')).to.eq('checked'); + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + }); + + it('should still expect 1st Grid to be unchanged from previous state and still have only 4 columns shown', () => { + const newColumnList = ['Duration (days)', '% Complete', 'Finish', 'Effort Driven']; + + cy.get('#grid1') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(newColumnList[index])); + }); + + it('should open the Grid Menu on 1st Grid and also expect to only have 4 columns checked (visible)', () => { + let gridUid = ''; + cy.get('#grid1') + .find('button.slick-gridmenu-button') + .click({ force: true }); + + cy.get('#grid1') + .should(($grid) => { + const classes = $grid.prop('className').split(' '); + gridUid = classes.find(className => /slickgrid_.*/.test(className)); + expect(gridUid).to.not.be.null; + }) + .then(() => { + cy.get(`.slick-gridmenu.${gridUid}`) + .find('.slick-gridmenu-list') + .children('li') + .each(($child, index) => { + if (index <= 5) { + const $input = $child.children('input'); + const $label = $child.children('label'); + if ($label.text() === 'Title' || $label.text() === 'Start') { + expect($input.attr('checked')).to.eq(undefined); + } else { + expect($input.attr('checked')).to.eq('checked'); + } + expect($label.text()).to.eq(fullTitles[index]); + } + }); + }); + + cy.get('#grid1') + .get('.slick-gridmenu:visible') + .find('span.close') + .click({ force: true }); + }); }); diff --git a/test/jest.config.js b/test/jest.config.js index 733572627..70809d1e7 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -15,6 +15,7 @@ module.exports = { coverageDirectory: './test/jest-coverage/', coveragePathIgnorePatterns: [ 'example-data.js', + 'global-grid-options.ts', 'jest-global-mocks.ts', 'jest-pretest.ts', 'polyfills.ts',