Skip to content

Commit

Permalink
feat(sorting): header menu clear sort, reset sorting when nothing left (
Browse files Browse the repository at this point in the history
#509)

* feat(sorting): header menu clear sort, reset sorting when nothing left
- when there is no more column to sort, we could resort by default sort id which would display dataset the way it was when it was first loaded
- ref SO question: https://stackoverflow.com/questions/62489108/angular-slickgrid-column-wise-remove-sort-option-not-working-properly
- also found that Clear Column Sort on a Local Grid was not trigger a "sortChanged" event while it should so that the Grid State is also notified
  • Loading branch information
ghiscoding authored Jun 25, 2020
1 parent f7b5270 commit 5898c18
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
private _fixedWidth: number | null;
private _hideHeaderRowAfterPageLoad = false;
private _isGridInitialized = false;
private _isGridHavingFilters = false;
private _isDatasetInitialized = false;
private _isPaginationInitialized = false;
private _isLocalGrid = true;
Expand Down Expand Up @@ -241,9 +240,6 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn

ngAfterViewInit() {
this.initialization();
if (this.columnDefinitions.findIndex((col) => col.filterable) > -1) {
this._isGridHavingFilters = true;
}
this._isGridInitialized = true;

// user must provide a "gridHeight" or use "autoResize: true" in the grid options
Expand Down Expand Up @@ -528,9 +524,9 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn
}
// bind external filter (backend) unless specified to use the local one
if (gridOptions.backendServiceApi && !gridOptions.backendServiceApi.useLocalFiltering) {
this.filterService.bindBackendOnFilter(grid, this.dataView);
this.filterService.bindBackendOnFilter(grid, dataView);
} else {
this.filterService.bindLocalOnFilter(grid, this.dataView);
this.filterService.bindLocalOnFilter(grid, dataView);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const filterServiceStub = {
} as unknown as FilterService;

const sortServiceStub = {
clearSortByColumnId: jest.fn(),
clearSorting: jest.fn(),
emitSortChanged: jest.fn(),
getCurrentColumnSorts: jest.fn(),
Expand Down Expand Up @@ -396,58 +397,15 @@ describe('headerMenuExtension', () => {
expect(filterSpy).toHaveBeenCalledWith(expect.anything(), columnsMock[0].id);
});

it('should trigger the command "clear-sort" and expect Sort Service to call "onBackendSortChanged" being called without the sorted column', () => {
const mockSortedCols: ColumnSort[] = [{ sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];
const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[1]]).mockReturnValueOnce(mockSortedCols);
const backendSortSpy = jest.spyOn(sortServiceStub, 'onBackendSortChanged');
const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.headerMenu, 'onCommand');
const setSortSpy = jest.spyOn(SharedService.prototype.grid, 'setSortColumns');

const instance = extension.register();
instance.onCommand.notify({ column: columnsMock[0], grid: gridStub, command: 'clear-sort' }, new Slick.EventData(), gridStub);

expect(previousSortSpy).toHaveBeenCalled();
expect(backendSortSpy).toHaveBeenCalledWith(expect.anything(), { multiColumnSort: true, sortCols: [mockSortedCols[1]], grid: gridStub });
expect(onCommandSpy).toHaveBeenCalled();
expect(setSortSpy).toHaveBeenCalled();
});

it('should trigger the command "clear-sort" and expect Sort Service to call "onLocalSortChanged" being called without the sorted column', () => {
const copyGridOptionsMock = { ...gridOptionsMock, backendServiceApi: undefined } as unknown as GridOption;
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
const mockSortedCols: ColumnSort[] = [{ sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];
const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[1]]).mockReturnValueOnce(mockSortedCols);
const localSortSpy = jest.spyOn(sortServiceStub, 'onLocalSortChanged');
const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.headerMenu, 'onCommand');
const setSortSpy = jest.spyOn(SharedService.prototype.grid, 'setSortColumns');

const instance = extension.register();
instance.onCommand.notify({ column: columnsMock[0], grid: gridStub, command: 'clear-sort' }, new Slick.EventData(), gridStub);

expect(previousSortSpy).toHaveBeenCalled();
expect(localSortSpy).toHaveBeenCalledWith(gridStub, dataViewStub, [mockSortedCols[1]], true);
expect(onCommandSpy).toHaveBeenCalled();
expect(setSortSpy).toHaveBeenCalled();
});

it('should trigger the command "clear-sort" and expect "onSort" event triggered when no DataView is provided', () => {
const copyGridOptionsMock = { ...gridOptionsMock, backendServiceApi: undefined } as unknown as GridOption;
const mockSortedCols: ColumnSort[] = [{ sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];

jest.spyOn(SharedService.prototype, 'dataView', 'get').mockReturnValue(undefined);
jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(copyGridOptionsMock);
const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[1]]).mockReturnValueOnce(mockSortedCols);
it('should trigger the command "clear-sort" and expect "clearSortByColumnId" being called with event and column id', () => {
const clearSortSpy = jest.spyOn(sortServiceStub, 'clearSortByColumnId');
const onCommandSpy = jest.spyOn(SharedService.prototype.gridOptions.headerMenu, 'onCommand');
const setSortSpy = jest.spyOn(SharedService.prototype.grid, 'setSortColumns');
const gridSortSpy = jest.spyOn(gridStub.onSort, 'notify');

const instance = extension.register();
instance.onCommand.notify({ column: columnsMock[0], grid: gridStub, command: 'clear-sort' }, new Slick.EventData(), gridStub);

expect(previousSortSpy).toHaveBeenCalled();
expect(clearSortSpy).toHaveBeenCalledWith(expect.anything(), columnsMock[0].id);
expect(onCommandSpy).toHaveBeenCalled();
expect(setSortSpy).toHaveBeenCalled();
expect(gridSortSpy).toHaveBeenCalledWith([mockSortedCols[1]]);
});

it('should trigger the command "sort-asc" and expect Sort Service to call "onBackendSortChanged" being called without the sorted column', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,32 +286,7 @@ export class HeaderMenuExtension implements Extension {
/** Clear the Sort on the current column (if it's actually sorted) */
private clearColumnSort(event: Event, args: MenuCommandItemCallbackArgs) {
if (args && args.column && this.sharedService) {
// get current sorted columns, prior to calling the new column sort
const allSortedCols: ColumnSort[] = this.sortService.getCurrentColumnSorts();
const sortedColsWithoutCurrent: ColumnSort[] = this.sortService.getCurrentColumnSorts(args.column.id + '');

if (Array.isArray(allSortedCols) && Array.isArray(sortedColsWithoutCurrent) && allSortedCols.length !== sortedColsWithoutCurrent.length) {
if (this.sharedService.gridOptions && this.sharedService.gridOptions.backendServiceApi) {
this.sortService.onBackendSortChanged(event, { multiColumnSort: true, sortCols: sortedColsWithoutCurrent, grid: this.sharedService.grid });
} else if (this.sharedService.dataView) {
this.sortService.onLocalSortChanged(this.sharedService.grid, this.sharedService.dataView, sortedColsWithoutCurrent, true);
} else {
// when using customDataView, we will simply send it as a onSort event with notify
const isMultiSort = this.sharedService.gridOptions && this.sharedService.gridOptions.multiColumnSort || false;
const sortOutput = isMultiSort ? sortedColsWithoutCurrent : sortedColsWithoutCurrent[0];
args.grid.onSort.notify(sortOutput);
}

// update the this.sharedService.gridObj sortColumns array which will at the same add the visual sort icon(s) on the UI
const updatedSortColumns: ColumnSort[] = sortedColsWithoutCurrent.map((col) => {
return {
columnId: col && col.sortCol && col.sortCol.id,
sortAsc: col && col.sortAsc,
sortCol: col && col.sortCol,
};
});
this.sharedService.grid.setSortColumns(updatedSortColumns); // add sort icon in UI
}
this.sortService.clearSortByColumnId(event, args.column.id);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,128 @@ describe('SortService', () => {
expect(spy).toHaveBeenCalled();
});

describe('clearSortByColumnId method', () => {
let mockSortedCols: ColumnSort[];
const mockColumns = [{ id: 'firstName', field: 'firstName' }, { id: 'lastName', field: 'lastName' }] as Column[];

beforeEach(() => {
mockSortedCols = [
{ sortCol: { id: 'firstName', field: 'firstName', width: 100 }, sortAsc: false, grid: gridStub },
{ sortCol: { id: 'lastName', field: 'lastName', width: 100 }, sortAsc: true, grid: gridStub },
];
gridOptionMock.backendServiceApi = {
service: backendServiceStub,
process: () => new Promise((resolve) => resolve(jest.fn()))
};
jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns);
});

it('should expect Sort Service to call "onBackendSortChanged" being called without the sorted column', () => {
const previousSortSpy = jest.spyOn(service, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[1]]).mockReturnValueOnce(mockSortedCols);
const backendSortSpy = jest.spyOn(service, 'onBackendSortChanged');
const setSortSpy = jest.spyOn(gridStub, 'setSortColumns');

const mockMouseEvent = new Event('mouseup');
service.bindBackendOnSort(gridStub, dataViewStub);
service.clearSortByColumnId(mockMouseEvent, 'firstName');

expect(previousSortSpy).toHaveBeenCalled();
expect(backendSortSpy).toHaveBeenCalledWith(mockMouseEvent, { multiColumnSort: true, sortCols: [mockSortedCols[1]], grid: gridStub });
expect(setSortSpy).toHaveBeenCalled();
});

it('should expect Sort Service to call "onLocalSortChanged" being called without the sorted column (firstName DESC)', () => {
gridOptionMock.backendServiceApi = undefined;
const previousSortSpy = jest.spyOn(service, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[0]]).mockReturnValueOnce(mockSortedCols);
const localSortSpy = jest.spyOn(service, 'onLocalSortChanged');
const emitSortChangedSpy = jest.spyOn(service, 'emitSortChanged');
const setSortSpy = jest.spyOn(gridStub, 'setSortColumns');

const mockMouseEvent = new Event('mouseup');
service.bindLocalOnSort(gridStub, dataViewStub);
service.clearSortByColumnId(mockMouseEvent, 'firstName');

expect(previousSortSpy).toHaveBeenCalled();
expect(localSortSpy).toHaveBeenCalledWith(gridStub, dataViewStub, [mockSortedCols[0]], true, true);
expect(emitSortChangedSpy).toHaveBeenCalledWith('local', [{ columnId: 'firstName', direction: 'DESC' }]);
expect(setSortSpy).toHaveBeenCalled();
});

it('should expect Sort Service to call "onLocalSortChanged" being called without the sorted column (lastName ASC)', () => {
gridOptionMock.backendServiceApi = undefined;
const previousSortSpy = jest.spyOn(service, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[1]]).mockReturnValueOnce(mockSortedCols);
const localSortSpy = jest.spyOn(service, 'onLocalSortChanged');
const emitSortChangedSpy = jest.spyOn(service, 'emitSortChanged');
const setSortSpy = jest.spyOn(gridStub, 'setSortColumns');

const mockMouseEvent = new Event('mouseup');
service.bindLocalOnSort(gridStub, dataViewStub);
service.clearSortByColumnId(mockMouseEvent, 'lastName');

expect(previousSortSpy).toHaveBeenCalled();
expect(localSortSpy).toHaveBeenCalledWith(gridStub, dataViewStub, [mockSortedCols[1]], true, true);
expect(emitSortChangedSpy).toHaveBeenCalledWith('local', [{ columnId: 'lastName', direction: 'ASC' }]);
expect(setSortSpy).toHaveBeenCalled();
});

it('should expect "onSort" event triggered when no DataView is provided', () => {
gridOptionMock.backendServiceApi = undefined;
const previousSortSpy = jest.spyOn(service, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[1]]).mockReturnValueOnce(mockSortedCols);
const setSortSpy = jest.spyOn(gridStub, 'setSortColumns');
const gridSortSpy = jest.spyOn(gridStub.onSort, 'notify');

const mockMouseEvent = new Event('mouseup');
service.bindLocalOnSort(gridStub, null);
service.clearSortByColumnId(mockMouseEvent, 'firstName');

expect(previousSortSpy).toHaveBeenCalled();
expect(setSortSpy).toHaveBeenCalled();
expect(gridSortSpy).toHaveBeenCalledWith(mockSortedCols[1]);
});

it('should expect Sort Service to call "onLocalSortChanged" with empty array then also "sortLocalGridByDefaultSortFieldId" when there is no more columns left to sort', () => {
gridOptionMock.backendServiceApi = undefined;
const previousSortSpy = jest.spyOn(service, 'getCurrentColumnSorts').mockReturnValue([]).mockReturnValueOnce([mockSortedCols[0]]);
const localSortSpy = jest.spyOn(service, 'onLocalSortChanged');
const emitSortChangedSpy = jest.spyOn(service, 'emitSortChanged');
const sortDefaultSpy = jest.spyOn(service, 'sortLocalGridByDefaultSortFieldId');
const setSortSpy = jest.spyOn(gridStub, 'setSortColumns');

const mockMouseEvent = new Event('mouseup');
service.bindLocalOnSort(gridStub, dataViewStub);
service.clearSortByColumnId(mockMouseEvent, 'firstName');

expect(previousSortSpy).toHaveBeenCalled();
expect(localSortSpy).toHaveBeenNthCalledWith(1, gridStub, dataViewStub, [], true, true);
expect(localSortSpy).toHaveBeenNthCalledWith(2, gridStub, dataViewStub, [{ clearSortTriggered: true, sortAsc: true, sortCol: { field: 'id', id: 'id' } }]);
expect(emitSortChangedSpy).toHaveBeenCalledWith('local', []);
expect(setSortSpy).toHaveBeenCalled();
expect(sortDefaultSpy).toHaveBeenCalled();
});

it('should expect Sort Service to call "onLocalSortChanged" with empty array then also "sortLocalGridByDefaultSortFieldId" with custom Id when there is no more columns left to sort', () => {
gridOptionMock.backendServiceApi = undefined;
gridOptionMock.defaultColumnSortFieldId = 'customId';
const mockSortedCol = { sortCol: { id: 'firstName', field: 'firstName', width: 100 }, sortAsc: false, grid: gridStub };
const previousSortSpy = jest.spyOn(service, 'getCurrentColumnSorts').mockReturnValue([]).mockReturnValueOnce([mockSortedCol]);
const localSortSpy = jest.spyOn(service, 'onLocalSortChanged');
const emitSortChangedSpy = jest.spyOn(service, 'emitSortChanged');
const sortDefaultSpy = jest.spyOn(service, 'sortLocalGridByDefaultSortFieldId');
const setSortSpy = jest.spyOn(gridStub, 'setSortColumns');

const mockMouseEvent = new Event('mouseup');
service.bindLocalOnSort(gridStub, dataViewStub);
service.clearSortByColumnId(mockMouseEvent, 'firstName');

expect(previousSortSpy).toHaveBeenCalled();
expect(localSortSpy).toHaveBeenNthCalledWith(1, gridStub, dataViewStub, [], true, true);
expect(emitSortChangedSpy).toHaveBeenCalledWith('local', []);
expect(localSortSpy).toHaveBeenNthCalledWith(2, gridStub, dataViewStub, [{ clearSortTriggered: true, sortAsc: true, sortCol: { field: 'customId', id: 'customId' } }]);
expect(setSortSpy).toHaveBeenCalled();
expect(sortDefaultSpy).toHaveBeenCalled();
});
});

describe('clearSorting method', () => {
let mockSortedCol: ColumnSort;
const mockColumns = [{ id: 'lastName', field: 'lastName' }, { id: 'firstName', field: 'firstName' }] as Column[];
Expand Down
Loading

0 comments on commit 5898c18

Please sign in to comment.