Skip to content

Commit

Permalink
feat(extension): add latest slickgrid with RowMove improvements (#428)
Browse files Browse the repository at this point in the history
- now create column definition just by the enableRowMoveManager flag
- also keep row selection with dataview syncGridSelection
- latest slickgrid version also brought the usabilityOverride callback method that was added in RowMoveManager plugin
- possible ref #256

Co-authored-by: Ghislain Beaulac <[email protected]>
  • Loading branch information
ghiscoding and ghiscoding-SE authored Apr 7, 2020
1 parent ff210ec commit 4f4b231
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 59 deletions.
17 changes: 7 additions & 10 deletions src/app/examples/grid-rowmove.component.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
<div class="container-fluid">
<h2>{{title}}</h2>
<div class="subtitle" [innerHTML]="subTitle"></div>
<h2>{{title}}</h2>
<div class="subtitle" [innerHTML]="subTitle"></div>

<div class="col-sm-12">
<angular-slickgrid gridId="grid2"
(onAngularGridCreated)="angularGridReady($event)"
[columnDefinitions]="columnDefinitions"
[gridOptions]="gridOptions"
[dataset]="dataset">
</angular-slickgrid>
</div>
<div class="col-sm-12">
<angular-slickgrid gridId="grid17" (onAngularGridCreated)="angularGridReady($event)"
[columnDefinitions]="columnDefinitions" [gridOptions]="gridOptions" [dataset]="dataset">
</angular-slickgrid>
</div>
</div>
61 changes: 39 additions & 22 deletions src/app/examples/grid-rowmove.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { AngularGridInstance, Column, Formatters, GridOption } from './../modules/angular-slickgrid';
import { AngularGridInstance, Column, ExtensionName, Formatters, GridOption } from './../modules/angular-slickgrid';
import { TranslateService } from '@ngx-translate/core';

@Component({
templateUrl: './grid-rowmove.component.html'
})
export class GridRowMoveComponent implements OnInit {
title = 'Example 17: Row Move Plugin';
title = 'Example 17: Row Move & Checkbox Selector';
subTitle = `This example demonstrates using the <b>Slick.Plugins.RowMoveManager</b> plugin to easily move a row in the grid.<br/>
<ul>
<li>Click to select, Ctrl+Click to toggle selection, Shift+Click to select a range.</li>
<li>Drag one or more rows by the handle (icon) to reorder</li>
<li>If you plan to use Row Selection + Row Move, then use "singleRowMove: true" and "disableRowSelection: true"</li>
<li>You can change "columnIndexPosition" to move the icon position of any extension (RowMove, RowDetail or RowSelector icon)</li>
<ul>
<li>You will also want to enable the DataView "syncGridSelection: true" to keep row selection even after a row move</li>
</ul>
<li>If you plan to use only Row Move, then you could keep default values (or omit them completely) of "singleRowMove: false" and "disableRowSelection: false"</li>
<ul>
<li>SingleRowMove has the name suggest will only move 1 row at a time, by default it will move any row(s) that are selected unless you disable the flag</li>
</ul>
</ul>
`;

Expand All @@ -28,18 +37,12 @@ export class GridRowMoveComponent implements OnInit {
this.angularGrid = angularGrid;
}

get rowMoveInstance(): any {
return this.angularGrid && this.angularGrid.extensionService.getSlickgridAddonInstance(ExtensionName.rowMoveManager) || {};
}

ngOnInit(): void {
this.columnDefinitions = [
{
id: '#', field: '', name: '', width: 40,
behavior: 'selectAndMove',
selectable: false, resizable: false,
cssClass: 'cell-reorder dnd',
excludeFromExport: true,
excludeFromColumnPicker: true,
excludeFromHeaderMenu: true,
excludeFromGridMenu: true
},
{ id: 'title', name: 'Title', field: 'title' },
{ id: 'duration', name: 'Duration', field: 'duration', sortable: true },
{ id: '%', name: '% Complete', field: 'percentComplete', sortable: true },
Expand All @@ -55,13 +58,31 @@ export class GridRowMoveComponent implements OnInit {
sidePadding: 10
},
enableCellNavigation: true,
enableRowMoveManager: true,
gridMenu: {
iconCssClass: 'fa fa-ellipsis-v',
enableCheckboxSelector: true,
enableRowSelection: true,
rowSelectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false
},
dataView: {
syncGridSelection: true, // enable this flag so that the row selection follows the row even if we move it to another position
},
enableRowMoveManager: true,
rowMoveManager: {
// when using Row Move + Row Selection, you want to enable the following 2 flags so it doesn't cancel row selection
singleRowMove: true,
disableRowSelection: true,
cancelEditOnDrag: true,
onBeforeMoveRows: (e, args) => this.onBeforeMoveRow(e, args),
onMoveRows: (e, args) => this.onMoveRows(e, args),

// you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon)
// note that you might have to play with the position when using multiple extension
// since it really depends on which extension get created first to know what their real position are
// columnIndexPosition: 1,

// you can also override the usability of the rows, for example make every 2nd row the only moveable rows,
// usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1
},
enableTranslate: true,
i18n: this.translate
Expand Down Expand Up @@ -105,13 +126,10 @@ export class GridRowMoveComponent implements OnInit {
const left = this.dataset.slice(0, insertBefore);
const right = this.dataset.slice(insertBefore, this.dataset.length);
rows.sort((a, b) => a - b);

for (let i = 0; i < rows.length; i++) {
extractedRows.push(this.dataset[rows[i]]);
}

rows.reverse();

for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (row < insertBefore) {
Expand All @@ -120,14 +138,13 @@ export class GridRowMoveComponent implements OnInit {
right.splice(row - insertBefore, 1);
}
}
const updatedDataset = left.concat(extractedRows.concat(right));
const tmpDataset = left.concat(extractedRows.concat(right));
const selectedRows = [];
for (let i = 0; i < rows.length; i++) {
selectedRows.push(left.length + i);
}

this.angularGrid.slickGrid.resetActiveCell();
this.angularGrid.slickGrid.setData(updatedDataset);
this.angularGrid.slickGrid.setSelectedRows(selectedRows);
this.angularGrid.slickGrid.render();
this.dataset = tmpDataset;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TestBed } from '@angular/core/testing';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { GridOption } from '../../models/gridOption.interface';
import { Column, GridOption } from '../../models';
import { RowMoveManagerExtension } from '../rowMoveManagerExtension';
import { ExtensionUtility } from '../extensionUtility';
import { SharedService } from '../../services/shared.service';
Expand All @@ -17,6 +17,7 @@ const gridStub = {
const mockAddon = jest.fn().mockImplementation(() => ({
init: jest.fn(),
destroy: jest.fn(),
getColumnDefinition: jest.fn(),
onBeforeMoveRows: new Slick.Event(),
onMoveRows: new Slick.Event(),
}));
Expand All @@ -38,6 +39,9 @@ describe('rowMoveManagerExtension', () => {
const gridOptionsMock = {
enableRowMoveManager: true,
rowMoveManager: {
cancelEditOnDrag: true,
singleRowMove: true,
disableRowSelection: true,
onExtensionRegistered: jest.fn(),
onBeforeMoveRows: (e, args: { insertBefore: number; rows: number[]; }) => { },
onMoveRows: (e, args: { insertBefore: number; rows: number[]; }) => { },
Expand All @@ -53,13 +57,29 @@ describe('rowMoveManagerExtension', () => {
translate = TestBed.get(TranslateService);
});

it('should return null when either the grid object or the grid options is missing', () => {
it('should return null after calling "create" method when either the column definitions or the grid options is missing', () => {
const output = extension.create([] as Column[], null);
expect(output).toBeNull();
});

it('should return null after calling "loadAddonWhenNotExists" method when either the column definitions or the grid options is missing', () => {
const output = extension.loadAddonWhenNotExists([] as Column[], null);
expect(output).toBeNull();
});

it('should return null after calling "register" method when either the grid object or the grid options is missing', () => {
const output = extension.register();
expect(output).toBeNull();
});

describe('registered addon', () => {
describe('create method', () => {
let columnsMock: Column[];

beforeEach(() => {
columnsMock = [
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
{ id: 'field2', field: 'field2', width: 50 }
];
jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub);
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
});
Expand All @@ -68,16 +88,108 @@ describe('rowMoveManagerExtension', () => {
jest.clearAllMocks();
});

it('should add a reserved column for icons in 1st column index', () => {
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' });
extension.create(columnsMock, gridOptionsMock);

expect(spy).toHaveBeenCalled();
expect(columnsMock).toEqual([
{
excludeFromColumnPicker: true,
excludeFromExport: true,
excludeFromGridMenu: true,
excludeFromHeaderMenu: true,
excludeFromQuery: true,
field: 'move',
id: '_move'
},
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
{ id: 'field2', field: 'field2', width: 50 },
]);
});

it('should NOT add the move icon column if it already exist in the column definitions', () => {
columnsMock = [{
id: '_move', name: '', field: 'move', width: 40,
behavior: 'selectAndMove', selectable: false, resizable: false, cssClass: '',
formatter: (row, cell, value, columnDef, dataContext, grid) => ({ addClasses: 'cell-reorder dnd' })
}, ...columnsMock] as Column[];
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' });
extension.create(columnsMock, gridOptionsMock);

expect(spy).toHaveBeenCalled();
expect(columnsMock).toEqual([
{
behavior: 'selectAndMove',
cssClass: '',
field: 'move',
formatter: expect.anything(),
id: '_move',
name: '',
resizable: false,
selectable: false,
width: 40,
excludeFromColumnPicker: true,
excludeFromExport: true,
excludeFromGridMenu: true,
excludeFromHeaderMenu: true,
excludeFromQuery: true,
},
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
{ id: 'field2', field: 'field2', width: 50 },
]);
});

it('should expect the column to be at a different column index position when "columnIndexPosition" is defined', () => {
gridOptionsMock.rowMoveManager.columnIndexPosition = 2;
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' });
extension.create(columnsMock, gridOptionsMock);

expect(spy).toHaveBeenCalled();
expect(columnsMock).toEqual([
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
{ id: 'field2', field: 'field2', width: 50 },
{
excludeFromColumnPicker: true,
excludeFromExport: true,
excludeFromGridMenu: true,
excludeFromHeaderMenu: true,
excludeFromQuery: true,
field: 'move',
id: '_move'
},
]);
});
});

describe('registered addon', () => {
let columnsMock: Column[];

beforeEach(() => {
columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }];
jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub);
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
jest.clearAllMocks();
});
it('should register the addon', () => {
const onRegisteredSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager, 'onExtensionRegistered');
const pluginSpy = jest.spyOn(SharedService.prototype.grid, 'registerPlugin');

const instance = extension.register();
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
extension.create(columnsMock, gridOptionsMock);
extension.register();
const addonInstance = extension.getAddonInstance();

expect(instance).toBeTruthy();
expect(instance).toEqual(addonInstance);
expect(mockAddon).toHaveBeenCalledWith({
cancelEditOnDrag: true,
disableRowSelection: true,
singleRowMove: true,
columnIndexPosition: 2,
onExtensionRegistered: expect.anything(),
onBeforeMoveRows: expect.anything(),
onMoveRows: expect.anything(),
Expand All @@ -87,7 +199,8 @@ describe('rowMoveManagerExtension', () => {
});

it('should dispose of the addon', () => {
const instance = extension.register();
const instance = extension.create(columnsMock, gridOptionsMock);
extension.register();
const destroySpy = jest.spyOn(instance, 'destroy');

extension.dispose();
Expand All @@ -96,21 +209,23 @@ describe('rowMoveManagerExtension', () => {
});

it('should provide addon options and expect them to be called in the addon constructor', () => {
const optionMock = { cancelEditOnDrag: true };
const optionMock = { cancelEditOnDrag: true, singleRowMove: true, disableRowSelection: true };
const addonOptions = { ...gridOptionsMock, rowMoveManager: optionMock };
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(addonOptions);

const instance = extension.create(columnsMock, gridOptionsMock);
extension.register();

expect(mockAddon).toHaveBeenCalledWith(optionMock);
expect(mockAddon).toHaveBeenCalledWith(gridOptionsMock.rowMoveManager);
});

it('should call internal event handler subscribe and expect the "onBeforeMoveRows" option to be called when addon notify is called', () => {
const handlerSpy = jest.spyOn(extension.eventHandler, 'subscribe');
const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager, 'onBeforeMoveRows');
const onMoveSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager, 'onMoveRows');

const instance = extension.register();
const instance = extension.create(columnsMock, gridOptionsMock);
extension.register();
instance.onBeforeMoveRows.notify({ insertBefore: 3, rows: [1] }, new Slick.EventData(), gridStub);

expect(handlerSpy).toHaveBeenCalledTimes(2);
Expand All @@ -127,7 +242,8 @@ describe('rowMoveManagerExtension', () => {
const onBeforeSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager, 'onBeforeMoveRows');
const onMoveSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager, 'onMoveRows');

const instance = extension.register();
const instance = extension.create(columnsMock, gridOptionsMock);
extension.register();
instance.onMoveRows.notify({ insertBefore: 3, rows: [1] }, new Slick.EventData(), gridStub);

expect(handlerSpy).toHaveBeenCalledTimes(2);
Expand Down
Loading

0 comments on commit 4f4b231

Please sign in to comment.