From fcff2e63b8bd0817abb1bc2a48f63ff1ec14abba Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 20 Jul 2020 17:10:02 -0400 Subject: [PATCH 1/2] fix(menu): context menu to copy cell with queryFieldNameGetterFn --- .../__tests__/contextMenuExtension.spec.ts | 86 +++++++++++++++++++ .../extensions/contextMenuExtension.ts | 40 ++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/app/modules/angular-slickgrid/extensions/__tests__/contextMenuExtension.spec.ts b/src/app/modules/angular-slickgrid/extensions/__tests__/contextMenuExtension.spec.ts index 4f78b6652..ec7a2c3f2 100644 --- a/src/app/modules/angular-slickgrid/extensions/__tests__/contextMenuExtension.spec.ts +++ b/src/app/modules/angular-slickgrid/extensions/__tests__/contextMenuExtension.spec.ts @@ -659,6 +659,54 @@ describe('contextMenuExtension', () => { expect(execSpy).toHaveBeenCalledWith('copy', false, 'JOHN'); }); + it('should call "copyToClipboard" and get the value even when there is a "queryFieldNameGetterFn" callback defined with dot notation the command triggered is "copy"', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableExport: false, contextMenu: { hideCopyCellValueCommand: false } } as GridOption; + const columnMock = { id: 'firstName', name: 'First Name', field: 'firstName', queryFieldNameGetterFn: () => 'lastName' } as Column; + const dataContextMock = { id: 123, firstName: 'John', lastName: 'Doe', age: 50 }; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + const execSpy = jest.spyOn(window.document, 'execCommand'); + extension.register(); + extension.register(); + + const menuItemCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'copy') as MenuCommandItem; + menuItemCommand.action(new CustomEvent('change'), { + command: 'copy', + cell: 2, + row: 5, + grid: gridStub, + column: columnMock, + dataContext: dataContextMock, + item: menuItemCommand, + value: 'John' + }); + + expect(execSpy).toHaveBeenCalledWith('copy', false, 'Doe'); + }); + + it('should call "copyToClipboard" and get the value even when there is a "queryFieldNameGetterFn" callback defined with dot notation the command triggered is "copy"', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableExport: false, contextMenu: { hideCopyCellValueCommand: false } } as GridOption; + const columnMock = { id: 'firstName', name: 'First Name', field: 'firstName', queryFieldNameGetterFn: () => 'user.lastName' } as Column; + const dataContextMock = { id: 123, user: { firstName: 'John', lastName: 'Doe', age: 50 } }; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + const execSpy = jest.spyOn(window.document, 'execCommand'); + extension.register(); + extension.register(); + + const menuItemCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'copy') as MenuCommandItem; + menuItemCommand.action(new CustomEvent('change'), { + command: 'copy', + cell: 2, + row: 5, + grid: gridStub, + column: columnMock, + dataContext: dataContextMock, + item: menuItemCommand, + value: 'John' + }); + + expect(execSpy).toHaveBeenCalledWith('copy', false, 'Doe'); + }); + it('should expect "itemUsabilityOverride" callback from the "copy" command to return True when a value to copy is found in the dataContext object', () => { const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableExport: false, contextMenu: { hideCopyCellValueCommand: false } } as GridOption; const columnMock = { id: 'firstName', name: 'First Name', field: 'firstName' } as Column; @@ -735,6 +783,44 @@ describe('contextMenuExtension', () => { expect(isCommandUsable).toBe(false); }); + it('should expect "itemUsabilityOverride" callback from the "copy" command to return True when there is a "queryFieldNameGetterFn" which itself returns a value', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableExport: false, contextMenu: { hideCopyCellValueCommand: false } } as GridOption; + const columnMock = { id: 'firstName', name: 'First Name', field: 'firstName', queryFieldNameGetterFn: () => 'lastName' } as Column; + const dataContextMock = { id: 123, firstName: null, lastName: 'Doe', age: 50 }; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + extension.register(); + + const menuItemCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'copy') as MenuCommandItem; + const isCommandUsable = menuItemCommand.itemUsabilityOverride({ + cell: 2, + row: 2, + grid: gridStub, + column: columnMock, + dataContext: dataContextMock, + }); + + expect(isCommandUsable).toBe(true); + }); + + it('should expect "itemUsabilityOverride" callback from the "copy" command to return True when there is a "queryFieldNameGetterFn" and a dot notation field which does return a value', () => { + const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: false, enableExport: false, contextMenu: { hideCopyCellValueCommand: false } } as GridOption; + const columnMock = { id: 'firstName', name: 'First Name', field: 'user.firstName', queryFieldNameGetterFn: () => 'user.lastName' } as Column; + const dataContextMock = { id: 123, user: { firstName: null, lastName: 'Doe', age: 50 } }; + jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock); + extension.register(); + + const menuItemCommand = copyGridOptionsMock.contextMenu.commandItems.find((item: MenuCommandItem) => item.command === 'copy') as MenuCommandItem; + const isCommandUsable = menuItemCommand.itemUsabilityOverride({ + cell: 2, + row: 2, + grid: gridStub, + column: columnMock, + dataContext: dataContextMock, + }); + + expect(isCommandUsable).toBe(true); + }); + it('should call "exportToExcel" when the command triggered is "export-excel"', () => { const excelExportSpy = jest.spyOn(excelExportServiceStub, 'exportToExcel'); const copyGridOptionsMock = { ...gridOptionsMock, enableExcelExport: true, enableExport: false, contextMenu: { hideCopyCellValueCommand: true, hideExportCsvCommand: true, hideExportExcelCommand: false } } as GridOption; diff --git a/src/app/modules/angular-slickgrid/extensions/contextMenuExtension.ts b/src/app/modules/angular-slickgrid/extensions/contextMenuExtension.ts index 1ca9156c7..94731580c 100644 --- a/src/app/modules/angular-slickgrid/extensions/contextMenuExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/contextMenuExtension.ts @@ -20,7 +20,7 @@ import { ExportService } from '../services/export.service'; import { ExcelExportService } from '../services/excelExport.service'; import { TreeDataService } from '../services/treeData.service'; import { exportWithFormatterWhenDefined } from '../services/export-utilities'; -import { getTranslationPrefix } from '../services/utilities'; +import { getDescendantProperty, getTranslationPrefix } from '../services/utilities'; // using external non-typed js libraries declare const Slick: any; @@ -194,7 +194,12 @@ export class ContextMenuExtension implements Extension { // make sure there's an item to copy before enabling this command const columnDef = args && args.column as Column; const dataContext = args && args.dataContext; - if (columnDef && dataContext.hasOwnProperty(columnDef.field)) { + if (typeof columnDef.queryFieldNameGetterFn === 'function') { + const cellValue = this.getCellValueFromQueryFieldGetter(columnDef, dataContext); + if (cellValue !== '' && cellValue !== undefined) { + return true; + } + } else if (columnDef && dataContext.hasOwnProperty(columnDef.field)) { return dataContext[columnDef.field] !== '' && dataContext[columnDef.field] !== null && dataContext[columnDef.field] !== undefined; } return false; @@ -376,11 +381,15 @@ export class ContextMenuExtension implements Extension { const gridOptions = this.sharedService && this.sharedService.gridOptions || {}; const cell = args && args.cell || 0; const row = args && args.row || 0; - const column = args && args.column; + const columnDef = args && args.column; const dataContext = args && args.dataContext; const grid = this.sharedService && this.sharedService.grid; const exportOptions = gridOptions && (gridOptions.excelExportOptions || gridOptions.exportOptions); - const textToCopy = exportWithFormatterWhenDefined(row, cell, dataContext, column, grid, exportOptions); + let textToCopy = exportWithFormatterWhenDefined(row, cell, dataContext, columnDef, grid, exportOptions); + + if (typeof columnDef.queryFieldNameGetterFn === 'function') { + textToCopy = this.getCellValueFromQueryFieldGetter(columnDef, dataContext); + } // create fake
to copy into clipboard & delete it from the DOM once we're done const range = document.createRange(); @@ -399,4 +408,27 @@ export class ContextMenuExtension implements Extension { } } catch (e) { } } + + /** + * When a queryFieldNameGetterFn is defined, then get the value from that getter callback function + * @param columnDef + * @param dataContext + * @return cellValue + */ + private getCellValueFromQueryFieldGetter(columnDef: Column, dataContext: any): string { + let cellValue = ''; + + if (typeof columnDef.queryFieldNameGetterFn === 'function') { + const queryFieldName = columnDef.queryFieldNameGetterFn(dataContext); + + // get the cell value from the item or when it's a dot notation then exploded the item and get the final value + if (queryFieldName && queryFieldName.indexOf('.') >= 0) { + cellValue = getDescendantProperty(dataContext, queryFieldName); + } else { + cellValue = dataContext[queryFieldName]; + } + } + + return cellValue; + } } From b58db202a547c9dbb3e6a9be6eb79a02817f5ad4 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 20 Jul 2020 18:11:17 -0400 Subject: [PATCH 2/2] refactor(core): fix TS build warning --- .../angular-slickgrid/components/angular-slickgrid.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts index adc8ca355..45107a8d4 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts @@ -990,7 +990,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn // using jQuery extend to do a deep clone has an unwanted side on objects and pageSizes but ES6 spread has other worst side effects // so we will just overwrite the pageSizes when needed, this is the only one causing issues so far. // jQuery wrote this on their docs:: On a deep extend, Object and Array are extended, but object wrappers on primitive types such as String, Boolean, and Number are not. - if ((gridOptions.enablePagination || gridOptions.backendServiceApi) && gridOptions.pagination && Array.isArray(gridOptions.pagination.pageSizes)) { + if (options && options.pagination && (gridOptions.enablePagination || gridOptions.backendServiceApi) && gridOptions.pagination && Array.isArray(gridOptions.pagination.pageSizes)) { options.pagination.pageSizes = gridOptions.pagination.pageSizes; }