From 42f1632ca0e96a96c695f7b0f53729687c305941 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Wed, 24 Jul 2019 16:17:19 -0400 Subject: [PATCH 1/2] fix(gridMenu): adding user customItems in GridMenu was no longer showing --- .../__tests__/gridMenuExtension.spec.ts | 64 +++++++ .../extensions/gridMenuExtension.ts | 158 ++++++++++-------- 2 files changed, 156 insertions(+), 66 deletions(-) diff --git a/src/app/modules/angular-slickgrid/extensions/__tests__/gridMenuExtension.spec.ts b/src/app/modules/angular-slickgrid/extensions/__tests__/gridMenuExtension.spec.ts index 15c2a352f..7b9f30b70 100644 --- a/src/app/modules/angular-slickgrid/extensions/__tests__/gridMenuExtension.spec.ts +++ b/src/app/modules/angular-slickgrid/extensions/__tests__/gridMenuExtension.spec.ts @@ -126,6 +126,7 @@ describe('gridMenuExtension', () => { CLEAR_ALL_SORTING: 'Supprimer tous les tris', EXPORT_TO_CSV: 'Exporter en format CSV', EXPORT_TO_TAB_DELIMITED: 'Exporter en format texte (délimité par tabulation)', + HELP: 'Aide', COMMANDS: 'Commandes', COLUMNS: 'Colonnes', FORCE_FIT_COLUMNS: 'Ajustement forcé des colonnes', @@ -294,6 +295,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-filter text-danger', title: 'Supprimer tous les filtres', disabled: false, command: 'clear-filter', positionOrder: 50 }, { iconCssClass: 'fa fa-random', title: 'Basculer la ligne des filtres', disabled: false, command: 'toggle-filter', positionOrder: 52 }, @@ -305,6 +307,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, gridMenu: { hideToggleFilterCommand: true, hideRefreshDatasetCommand: true } } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-filter text-danger', title: 'Supprimer tous les filtres', disabled: false, command: 'clear-filter', positionOrder: 50 } ]); @@ -314,6 +317,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, gridMenu: { hideClearAllFiltersCommand: true, hideRefreshDatasetCommand: true } } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-random', title: 'Basculer la ligne des filtres', disabled: false, command: 'toggle-filter', positionOrder: 52 }, ]); @@ -323,6 +327,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableFiltering: true, gridMenu: { hideClearAllFiltersCommand: true, hideToggleFilterCommand: true } } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-refresh', title: 'Rafraîchir les données', disabled: false, command: 'refresh-dataset', positionOrder: 54 } ]); @@ -332,6 +337,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, showPreHeaderPanel: true } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-random', title: 'Basculer la ligne de pré-en-tête', disabled: false, command: 'toggle-preheader', positionOrder: 52 } ]); @@ -348,6 +354,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableSorting: true } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-unsorted text-danger', title: 'Supprimer tous les tris', disabled: false, command: 'clear-sorting', positionOrder: 51 } ]); @@ -364,6 +371,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableExport: true, gridMenu: { hideExportTextDelimitedCommand: true } } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-download', title: 'Exporter en format CSV', disabled: false, command: 'export-csv', positionOrder: 53 } ]); @@ -380,6 +388,7 @@ describe('gridMenuExtension', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableExport: true, gridMenu: { hideExportCsvCommand: true } } as unknown as GridOption; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); extension.register(); + extension.register(); // calling 2x register to make sure it doesn't duplicate commands expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ { iconCssClass: 'fa fa-download', title: 'Exporter en format texte (délimité par tabulation)', disabled: false, command: 'export-text-delimited', positionOrder: 54 } ]); @@ -393,6 +402,61 @@ describe('gridMenuExtension', () => { }); }); + describe('adding Grid Menu Custom Items', () => { + const customItemsMock = [{ + iconCssClass: 'fa fa-question-circle', + titleKey: 'HELP', + disabled: false, + command: 'help', + positionOrder: 99 + }]; + + beforeEach(() => { + const copyGridOptionsMock = { + ...gridOptionsMock, + enableExport: true, + gridMenu: { + customItems: customItemsMock, + hideExportCsvCommand: false, + hideExportTextDelimitedCommand: true, + hideRefreshDatasetCommand: true, + hideSyncResizeButton: true, + hideToggleFilterCommand: true, + hideTogglePreHeaderCommand: true + } + } as unknown as GridOption; + + jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(dataViewStub); + jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); + jest.spyOn(SharedService.prototype, 'allColumns', 'get').mockReturnValue(columnsMock); + jest.spyOn(SharedService.prototype, 'visibleColumns', 'get').mockReturnValue(columnsMock); + jest.spyOn(SharedService.prototype, 'columnDefinitions', 'get').mockReturnValue(columnsMock); + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + }); + + afterEach(() => { + extension.dispose(); + }); + + it('should have user grid menu custom items', () => { + extension.register(); + expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ + { command: 'export-csv', disabled: false, iconCssClass: 'fa fa-download', positionOrder: 53, title: 'Exporter en format CSV' }, + { command: 'help', disabled: false, iconCssClass: 'fa fa-question-circle', positionOrder: 99, title: 'Aide', titleKey: 'HELP' }, + ]); + }); + + it('should have same user grid menu custom items even when grid menu extension is registered multiple times', () => { + extension.register(); + extension.register(); + expect(SharedService.prototype.gridOptions.gridMenu.customItems).toEqual([ + { command: 'export-csv', disabled: false, iconCssClass: 'fa fa-download', positionOrder: 53, title: 'Exporter en format CSV' }, + { command: 'help', disabled: false, iconCssClass: 'fa fa-question-circle', positionOrder: 99, title: 'Aide', titleKey: 'HELP' }, + ]); + }); + }); + describe('refreshBackendDataset method', () => { afterEach(() => { jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); diff --git a/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts b/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts index fa8ce6223..951892837 100644 --- a/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts @@ -52,9 +52,8 @@ export class GridMenuExtension implements Extension { if (this._addon && this._addon.destroy) { this._addon.destroy(); } - this._userOriginalGridMenu = undefined; if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && this.sharedService.gridOptions.gridMenu.customItems) { - this.sharedService.gridOptions.gridMenu.customItems = []; + this.sharedService.gridOptions.gridMenu = this._userOriginalGridMenu; } } @@ -75,7 +74,8 @@ export class GridMenuExtension implements Extension { // merge original user grid menu items with internal items // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) - this.sharedService.gridOptions.gridMenu.customItems = [...this._userOriginalGridMenu.customItems || [], ...this.addGridMenuCustomCommands()]; + const originalCustomItems = this._userOriginalGridMenu.customItems || []; + this.sharedService.gridOptions.gridMenu.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; this.extensionUtility.translateItems(this.sharedService.gridOptions.gridMenu.customItems, 'titleKey', 'title'); this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.customItems, 'positionOrder'); @@ -194,7 +194,8 @@ export class GridMenuExtension implements Extension { // merge original user grid menu items with internal items // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) - this.sharedService.gridOptions.gridMenu.customItems = [...this._userOriginalGridMenu.customItems || [], ...this.addGridMenuCustomCommands()]; + const originalCustomItems = this._userOriginalGridMenu.customItems || []; + this.sharedService.gridOptions.gridMenu.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; this.extensionUtility.translateItems(this.sharedService.gridOptions.gridMenu.customItems, 'titleKey', 'title'); this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.customItems, 'positionOrder'); @@ -218,104 +219,129 @@ export class GridMenuExtension implements Extension { // ------------------ /** Create Grid Menu with Custom Commands if user has enabled Filters and/or uses a Backend Service (OData, GraphQL) */ - private addGridMenuCustomCommands() { + private addGridMenuCustomCommands(originalCustomItems: GridMenuItem[]) { const backendApi = this.sharedService.gridOptions.backendServiceApi || null; const gridMenuCustomItems: GridMenuItem[] = []; + if (!Array.isArray(originalCustomItems)) { + originalCustomItems = []; + } if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableFiltering) { // show grid menu: clear all filters if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideClearAllFiltersCommand) { - gridMenuCustomItems.push( - { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconClearAllFiltersCommand || 'fa fa-filter text-danger', - title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('CLEAR_ALL_FILTERS') : Constants.TEXT_CLEAR_ALL_FILTERS, - disabled: false, - command: 'clear-filter', - positionOrder: 50 - } - ); + const commandName = 'clear-filter'; + if (!originalCustomItems.find((item) => item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.sharedService.gridOptions.gridMenu.iconClearAllFiltersCommand || 'fa fa-filter text-danger', + title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('CLEAR_ALL_FILTERS') : Constants.TEXT_CLEAR_ALL_FILTERS, + disabled: false, + command: commandName, + positionOrder: 50 + } + ); + } } // show grid menu: toggle filter row if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideToggleFilterCommand) { - gridMenuCustomItems.push( - { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconToggleFilterCommand || 'fa fa-random', - title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('TOGGLE_FILTER_ROW') : Constants.TEXT_TOGGLE_FILTER_ROW, - disabled: false, - command: 'toggle-filter', - positionOrder: 52 - } - ); + const commandName = 'toggle-filter'; + if (!originalCustomItems.find((item) => item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.sharedService.gridOptions.gridMenu.iconToggleFilterCommand || 'fa fa-random', + title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('TOGGLE_FILTER_ROW') : Constants.TEXT_TOGGLE_FILTER_ROW, + disabled: false, + command: commandName, + positionOrder: 52 + } + ); + } } // show grid menu: refresh dataset if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideRefreshDatasetCommand && backendApi) { - gridMenuCustomItems.push( - { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconRefreshDatasetCommand || 'fa fa-refresh', - title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('REFRESH_DATASET') : Constants.TEXT_REFRESH_DATASET, - disabled: false, - command: 'refresh-dataset', - positionOrder: 54 - } - ); + const commandName = 'refresh-dataset'; + if (!originalCustomItems.find((item) => item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.sharedService.gridOptions.gridMenu.iconRefreshDatasetCommand || 'fa fa-refresh', + title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('REFRESH_DATASET') : Constants.TEXT_REFRESH_DATASET, + disabled: false, + command: commandName, + positionOrder: 54 + } + ); + } } } if (this.sharedService.gridOptions.showPreHeaderPanel) { // show grid menu: toggle pre-header row if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideTogglePreHeaderCommand) { - gridMenuCustomItems.push( - { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconTogglePreHeaderCommand || 'fa fa-random', - title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('TOGGLE_PRE_HEADER_ROW') : Constants.TEXT_TOGGLE_PRE_HEADER_ROW, - disabled: false, - command: 'toggle-preheader', - positionOrder: 52 - } - ); + const commandName = 'toggle-preheader'; + if (!originalCustomItems.find((item) => item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.sharedService.gridOptions.gridMenu.iconTogglePreHeaderCommand || 'fa fa-random', + title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('TOGGLE_PRE_HEADER_ROW') : Constants.TEXT_TOGGLE_PRE_HEADER_ROW, + disabled: false, + command: commandName, + positionOrder: 52 + } + ); + } } } if (this.sharedService.gridOptions.enableSorting) { // show grid menu: clear all sorting if (this.sharedService.gridOptions && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideClearAllSortingCommand) { + const commandName = 'clear-sorting'; + if (!originalCustomItems.find((item) => item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.sharedService.gridOptions.gridMenu.iconClearAllSortingCommand || 'fa fa-unsorted text-danger', + title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('CLEAR_ALL_SORTING') : Constants.TEXT_CLEAR_ALL_SORTING, + disabled: false, + command: commandName, + positionOrder: 51 + } + ); + } + } + } + + // show grid menu: export to file + if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExport && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideExportCsvCommand) { + const commandName = 'export-csv'; + if (!originalCustomItems.find((item) => item.command === commandName)) { gridMenuCustomItems.push( { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconClearAllSortingCommand || 'fa fa-unsorted text-danger', - title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('CLEAR_ALL_SORTING') : Constants.TEXT_CLEAR_ALL_SORTING, + iconCssClass: this.sharedService.gridOptions.gridMenu.iconExportCsvCommand || 'fa fa-download', + title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('EXPORT_TO_CSV') : Constants.TEXT_EXPORT_IN_CSV_FORMAT, disabled: false, - command: 'clear-sorting', - positionOrder: 51 + command: commandName, + positionOrder: 53 } ); } } - // show grid menu: export to file - if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExport && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideExportCsvCommand) { - gridMenuCustomItems.push( - { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconExportCsvCommand || 'fa fa-download', - title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('EXPORT_TO_CSV') : Constants.TEXT_EXPORT_IN_CSV_FORMAT, - disabled: false, - command: 'export-csv', - positionOrder: 53 - } - ); - } // show grid menu: export to text file as tab delimited if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableExport && this.sharedService.gridOptions.gridMenu && !this.sharedService.gridOptions.gridMenu.hideExportTextDelimitedCommand) { - gridMenuCustomItems.push( - { - iconCssClass: this.sharedService.gridOptions.gridMenu.iconExportTextDelimitedCommand || 'fa fa-download', - title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('EXPORT_TO_TAB_DELIMITED') : Constants.TEXT_EXPORT_IN_TEXT_FORMAT, - disabled: false, - command: 'export-text-delimited', - positionOrder: 54 - } - ); + const commandName = 'export-text-delimited'; + if (!originalCustomItems.find((item) => item.command === commandName)) { + gridMenuCustomItems.push( + { + iconCssClass: this.sharedService.gridOptions.gridMenu.iconExportTextDelimitedCommand || 'fa fa-download', + title: this.sharedService.gridOptions.enableTranslate ? this.translate.instant('EXPORT_TO_TAB_DELIMITED') : Constants.TEXT_EXPORT_IN_TEXT_FORMAT, + disabled: false, + command: commandName, + positionOrder: 54 + } + ); + } } // add the custom "Commands" title if there are any commands From 3a55313f036da06f4cb095989d0b36fa5d1fc920 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Wed, 24 Jul 2019 16:30:53 -0400 Subject: [PATCH 2/2] refactor(gridMenu): add customItems array check in code before using it --- .../angular-slickgrid/extensions/gridMenuExtension.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts b/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts index 951892837..a5827119a 100644 --- a/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts @@ -74,7 +74,7 @@ export class GridMenuExtension implements Extension { // merge original user grid menu items with internal items // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) - const originalCustomItems = this._userOriginalGridMenu.customItems || []; + const originalCustomItems = Array.isArray(this._userOriginalGridMenu.customItems) && this._userOriginalGridMenu.customItems || []; this.sharedService.gridOptions.gridMenu.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; this.extensionUtility.translateItems(this.sharedService.gridOptions.gridMenu.customItems, 'titleKey', 'title'); this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.customItems, 'positionOrder'); @@ -194,7 +194,7 @@ export class GridMenuExtension implements Extension { // merge original user grid menu items with internal items // then sort all Grid Menu Custom Items (sorted by pointer, no need to use the return) - const originalCustomItems = this._userOriginalGridMenu.customItems || []; + const originalCustomItems = Array.isArray(this._userOriginalGridMenu.customItems) && this._userOriginalGridMenu.customItems || []; this.sharedService.gridOptions.gridMenu.customItems = [...originalCustomItems, ...this.addGridMenuCustomCommands(originalCustomItems)]; this.extensionUtility.translateItems(this.sharedService.gridOptions.gridMenu.customItems, 'titleKey', 'title'); this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.customItems, 'positionOrder'); @@ -222,9 +222,6 @@ export class GridMenuExtension implements Extension { private addGridMenuCustomCommands(originalCustomItems: GridMenuItem[]) { const backendApi = this.sharedService.gridOptions.backendServiceApi || null; const gridMenuCustomItems: GridMenuItem[] = []; - if (!Array.isArray(originalCustomItems)) { - originalCustomItems = []; - } if (this.sharedService.gridOptions && this.sharedService.gridOptions.enableFiltering) { // show grid menu: clear all filters