Skip to content

Commit 4f4b231

Browse files
feat(extension): add latest slickgrid with RowMove improvements (#428)
- 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 <ghislain.beaulac@se.com>
1 parent ff210ec commit 4f4b231

File tree

10 files changed

+363
-59
lines changed

10 files changed

+363
-59
lines changed
+7-10
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
<div class="container-fluid">
2-
<h2>{{title}}</h2>
3-
<div class="subtitle" [innerHTML]="subTitle"></div>
2+
<h2>{{title}}</h2>
3+
<div class="subtitle" [innerHTML]="subTitle"></div>
44

5-
<div class="col-sm-12">
6-
<angular-slickgrid gridId="grid2"
7-
(onAngularGridCreated)="angularGridReady($event)"
8-
[columnDefinitions]="columnDefinitions"
9-
[gridOptions]="gridOptions"
10-
[dataset]="dataset">
11-
</angular-slickgrid>
12-
</div>
5+
<div class="col-sm-12">
6+
<angular-slickgrid gridId="grid17" (onAngularGridCreated)="angularGridReady($event)"
7+
[columnDefinitions]="columnDefinitions" [gridOptions]="gridOptions" [dataset]="dataset">
8+
</angular-slickgrid>
9+
</div>
1310
</div>

src/app/examples/grid-rowmove.component.ts

+39-22
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import { Component, OnInit } from '@angular/core';
2-
import { AngularGridInstance, Column, Formatters, GridOption } from './../modules/angular-slickgrid';
2+
import { AngularGridInstance, Column, ExtensionName, Formatters, GridOption } from './../modules/angular-slickgrid';
33
import { TranslateService } from '@ngx-translate/core';
44

55
@Component({
66
templateUrl: './grid-rowmove.component.html'
77
})
88
export class GridRowMoveComponent implements OnInit {
9-
title = 'Example 17: Row Move Plugin';
9+
title = 'Example 17: Row Move & Checkbox Selector';
1010
subTitle = `This example demonstrates using the <b>Slick.Plugins.RowMoveManager</b> plugin to easily move a row in the grid.<br/>
1111
<ul>
1212
<li>Click to select, Ctrl+Click to toggle selection, Shift+Click to select a range.</li>
1313
<li>Drag one or more rows by the handle (icon) to reorder</li>
14+
<li>If you plan to use Row Selection + Row Move, then use "singleRowMove: true" and "disableRowSelection: true"</li>
15+
<li>You can change "columnIndexPosition" to move the icon position of any extension (RowMove, RowDetail or RowSelector icon)</li>
16+
<ul>
17+
<li>You will also want to enable the DataView "syncGridSelection: true" to keep row selection even after a row move</li>
18+
</ul>
19+
<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>
20+
<ul>
21+
<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>
22+
</ul>
1423
</ul>
1524
`;
1625

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

40+
get rowMoveInstance(): any {
41+
return this.angularGrid && this.angularGrid.extensionService.getSlickgridAddonInstance(ExtensionName.rowMoveManager) || {};
42+
}
43+
3144
ngOnInit(): void {
3245
this.columnDefinitions = [
33-
{
34-
id: '#', field: '', name: '', width: 40,
35-
behavior: 'selectAndMove',
36-
selectable: false, resizable: false,
37-
cssClass: 'cell-reorder dnd',
38-
excludeFromExport: true,
39-
excludeFromColumnPicker: true,
40-
excludeFromHeaderMenu: true,
41-
excludeFromGridMenu: true
42-
},
4346
{ id: 'title', name: 'Title', field: 'title' },
4447
{ id: 'duration', name: 'Duration', field: 'duration', sortable: true },
4548
{ id: '%', name: '% Complete', field: 'percentComplete', sortable: true },
@@ -55,13 +58,31 @@ export class GridRowMoveComponent implements OnInit {
5558
sidePadding: 10
5659
},
5760
enableCellNavigation: true,
58-
enableRowMoveManager: true,
59-
gridMenu: {
60-
iconCssClass: 'fa fa-ellipsis-v',
61+
enableCheckboxSelector: true,
62+
enableRowSelection: true,
63+
rowSelectionOptions: {
64+
// True (Single Selection), False (Multiple Selections)
65+
selectActiveRow: false
66+
},
67+
dataView: {
68+
syncGridSelection: true, // enable this flag so that the row selection follows the row even if we move it to another position
6169
},
70+
enableRowMoveManager: true,
6271
rowMoveManager: {
72+
// when using Row Move + Row Selection, you want to enable the following 2 flags so it doesn't cancel row selection
73+
singleRowMove: true,
74+
disableRowSelection: true,
75+
cancelEditOnDrag: true,
6376
onBeforeMoveRows: (e, args) => this.onBeforeMoveRow(e, args),
6477
onMoveRows: (e, args) => this.onMoveRows(e, args),
78+
79+
// you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon)
80+
// note that you might have to play with the position when using multiple extension
81+
// since it really depends on which extension get created first to know what their real position are
82+
// columnIndexPosition: 1,
83+
84+
// you can also override the usability of the rows, for example make every 2nd row the only moveable rows,
85+
// usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1
6586
},
6687
enableTranslate: true,
6788
i18n: this.translate
@@ -105,13 +126,10 @@ export class GridRowMoveComponent implements OnInit {
105126
const left = this.dataset.slice(0, insertBefore);
106127
const right = this.dataset.slice(insertBefore, this.dataset.length);
107128
rows.sort((a, b) => a - b);
108-
109129
for (let i = 0; i < rows.length; i++) {
110130
extractedRows.push(this.dataset[rows[i]]);
111131
}
112-
113132
rows.reverse();
114-
115133
for (let i = 0; i < rows.length; i++) {
116134
const row = rows[i];
117135
if (row < insertBefore) {
@@ -120,14 +138,13 @@ export class GridRowMoveComponent implements OnInit {
120138
right.splice(row - insertBefore, 1);
121139
}
122140
}
123-
const updatedDataset = left.concat(extractedRows.concat(right));
141+
const tmpDataset = left.concat(extractedRows.concat(right));
124142
const selectedRows = [];
125143
for (let i = 0; i < rows.length; i++) {
126144
selectedRows.push(left.length + i);
127145
}
146+
128147
this.angularGrid.slickGrid.resetActiveCell();
129-
this.angularGrid.slickGrid.setData(updatedDataset);
130-
this.angularGrid.slickGrid.setSelectedRows(selectedRows);
131-
this.angularGrid.slickGrid.render();
148+
this.dataset = tmpDataset;
132149
}
133150
}

src/app/modules/angular-slickgrid/extensions/__tests__/rowMoveManagerExtension.spec.ts

+125-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TestBed } from '@angular/core/testing';
22
import { TranslateService, TranslateModule } from '@ngx-translate/core';
3-
import { GridOption } from '../../models/gridOption.interface';
3+
import { Column, GridOption } from '../../models';
44
import { RowMoveManagerExtension } from '../rowMoveManagerExtension';
55
import { ExtensionUtility } from '../extensionUtility';
66
import { SharedService } from '../../services/shared.service';
@@ -17,6 +17,7 @@ const gridStub = {
1717
const mockAddon = jest.fn().mockImplementation(() => ({
1818
init: jest.fn(),
1919
destroy: jest.fn(),
20+
getColumnDefinition: jest.fn(),
2021
onBeforeMoveRows: new Slick.Event(),
2122
onMoveRows: new Slick.Event(),
2223
}));
@@ -38,6 +39,9 @@ describe('rowMoveManagerExtension', () => {
3839
const gridOptionsMock = {
3940
enableRowMoveManager: true,
4041
rowMoveManager: {
42+
cancelEditOnDrag: true,
43+
singleRowMove: true,
44+
disableRowSelection: true,
4145
onExtensionRegistered: jest.fn(),
4246
onBeforeMoveRows: (e, args: { insertBefore: number; rows: number[]; }) => { },
4347
onMoveRows: (e, args: { insertBefore: number; rows: number[]; }) => { },
@@ -53,13 +57,29 @@ describe('rowMoveManagerExtension', () => {
5357
translate = TestBed.get(TranslateService);
5458
});
5559

56-
it('should return null when either the grid object or the grid options is missing', () => {
60+
it('should return null after calling "create" method when either the column definitions or the grid options is missing', () => {
61+
const output = extension.create([] as Column[], null);
62+
expect(output).toBeNull();
63+
});
64+
65+
it('should return null after calling "loadAddonWhenNotExists" method when either the column definitions or the grid options is missing', () => {
66+
const output = extension.loadAddonWhenNotExists([] as Column[], null);
67+
expect(output).toBeNull();
68+
});
69+
70+
it('should return null after calling "register" method when either the grid object or the grid options is missing', () => {
5771
const output = extension.register();
5872
expect(output).toBeNull();
5973
});
6074

61-
describe('registered addon', () => {
75+
describe('create method', () => {
76+
let columnsMock: Column[];
77+
6278
beforeEach(() => {
79+
columnsMock = [
80+
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
81+
{ id: 'field2', field: 'field2', width: 50 }
82+
];
6383
jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub);
6484
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
6585
});
@@ -68,16 +88,108 @@ describe('rowMoveManagerExtension', () => {
6888
jest.clearAllMocks();
6989
});
7090

91+
it('should add a reserved column for icons in 1st column index', () => {
92+
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
93+
const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' });
94+
extension.create(columnsMock, gridOptionsMock);
95+
96+
expect(spy).toHaveBeenCalled();
97+
expect(columnsMock).toEqual([
98+
{
99+
excludeFromColumnPicker: true,
100+
excludeFromExport: true,
101+
excludeFromGridMenu: true,
102+
excludeFromHeaderMenu: true,
103+
excludeFromQuery: true,
104+
field: 'move',
105+
id: '_move'
106+
},
107+
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
108+
{ id: 'field2', field: 'field2', width: 50 },
109+
]);
110+
});
111+
112+
it('should NOT add the move icon column if it already exist in the column definitions', () => {
113+
columnsMock = [{
114+
id: '_move', name: '', field: 'move', width: 40,
115+
behavior: 'selectAndMove', selectable: false, resizable: false, cssClass: '',
116+
formatter: (row, cell, value, columnDef, dataContext, grid) => ({ addClasses: 'cell-reorder dnd' })
117+
}, ...columnsMock] as Column[];
118+
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
119+
const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' });
120+
extension.create(columnsMock, gridOptionsMock);
121+
122+
expect(spy).toHaveBeenCalled();
123+
expect(columnsMock).toEqual([
124+
{
125+
behavior: 'selectAndMove',
126+
cssClass: '',
127+
field: 'move',
128+
formatter: expect.anything(),
129+
id: '_move',
130+
name: '',
131+
resizable: false,
132+
selectable: false,
133+
width: 40,
134+
excludeFromColumnPicker: true,
135+
excludeFromExport: true,
136+
excludeFromGridMenu: true,
137+
excludeFromHeaderMenu: true,
138+
excludeFromQuery: true,
139+
},
140+
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
141+
{ id: 'field2', field: 'field2', width: 50 },
142+
]);
143+
});
144+
145+
it('should expect the column to be at a different column index position when "columnIndexPosition" is defined', () => {
146+
gridOptionsMock.rowMoveManager.columnIndexPosition = 2;
147+
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
148+
const spy = jest.spyOn(instance, 'getColumnDefinition').mockReturnValue({ id: '_move', field: 'move' });
149+
extension.create(columnsMock, gridOptionsMock);
150+
151+
expect(spy).toHaveBeenCalled();
152+
expect(columnsMock).toEqual([
153+
{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' },
154+
{ id: 'field2', field: 'field2', width: 50 },
155+
{
156+
excludeFromColumnPicker: true,
157+
excludeFromExport: true,
158+
excludeFromGridMenu: true,
159+
excludeFromHeaderMenu: true,
160+
excludeFromQuery: true,
161+
field: 'move',
162+
id: '_move'
163+
},
164+
]);
165+
});
166+
});
167+
168+
describe('registered addon', () => {
169+
let columnsMock: Column[];
170+
171+
beforeEach(() => {
172+
columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }];
173+
jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub);
174+
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock);
175+
jest.clearAllMocks();
176+
});
71177
it('should register the addon', () => {
72178
const onRegisteredSpy = jest.spyOn(SharedService.prototype.gridOptions.rowMoveManager, 'onExtensionRegistered');
73179
const pluginSpy = jest.spyOn(SharedService.prototype.grid, 'registerPlugin');
74180

75-
const instance = extension.register();
181+
const instance = extension.loadAddonWhenNotExists(columnsMock, gridOptionsMock);
182+
extension.create(columnsMock, gridOptionsMock);
183+
extension.register();
76184
const addonInstance = extension.getAddonInstance();
77185

78186
expect(instance).toBeTruthy();
79187
expect(instance).toEqual(addonInstance);
80188
expect(mockAddon).toHaveBeenCalledWith({
189+
cancelEditOnDrag: true,
190+
disableRowSelection: true,
191+
singleRowMove: true,
192+
columnIndexPosition: 2,
81193
onExtensionRegistered: expect.anything(),
82194
onBeforeMoveRows: expect.anything(),
83195
onMoveRows: expect.anything(),
@@ -87,7 +199,8 @@ describe('rowMoveManagerExtension', () => {
87199
});
88200

89201
it('should dispose of the addon', () => {
90-
const instance = extension.register();
202+
const instance = extension.create(columnsMock, gridOptionsMock);
203+
extension.register();
91204
const destroySpy = jest.spyOn(instance, 'destroy');
92205

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

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

216+
const instance = extension.create(columnsMock, gridOptionsMock);
103217
extension.register();
104218

105-
expect(mockAddon).toHaveBeenCalledWith(optionMock);
219+
expect(mockAddon).toHaveBeenCalledWith(gridOptionsMock.rowMoveManager);
106220
});
107221

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

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

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

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

133249
expect(handlerSpy).toHaveBeenCalledTimes(2);

0 commit comments

Comments
 (0)