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
- 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-SE committed Jun 25, 2020
1 parent 87808ce commit 032886b
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const pubSubServiceStub = {
} as PubSubService;

const sortServiceStub = {
clearSortByColumnId: jest.fn(),
clearSorting: jest.fn(),
emitSortChanged: jest.fn(),
getCurrentColumnSorts: jest.fn(),
Expand Down Expand Up @@ -430,58 +431,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[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', sortAsc: false, sortCol: { id: 'field2', field: 'field2' } }];
const previousSortSpy = jest.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[1]]).mockReturnValueOnce(mockSortedCols);
const backendSortSpy = jest.spyOn(sortServiceStub, 'onBackendSortChanged');
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 instance = extension.register();
instance.onCommand.notify({ column: columnsMock[0], grid: gridStub, command: 'clear-sort', item: { 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[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', 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', item: { command: 'clear-sort' } }, new Slick.EventData(), gridStub);

expect(previousSortSpy).toHaveBeenCalled();
expect(localSortSpy).toHaveBeenCalledWith(gridStub, dataViewStub, [mockSortedCols[1]], true);
expect(clearSortSpy).toHaveBeenCalledWith(expect.anything(), columnsMock[0].id);
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[] = [{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } }, { columnId: 'field2', 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);
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', item: { command: 'clear-sort' } }, new Slick.EventData(), gridStub);

expect(previousSortSpy).toHaveBeenCalled();
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
28 changes: 2 additions & 26 deletions packages/common/src/extensions/headerMenuExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,33 +323,9 @@ export class HeaderMenuExtension implements Extension {
}

/** Clear the Sort on the current column (if it's actually sorted) */
private clearColumnSort(e: Event, args: MenuCommandItemCallbackArgs) {
private clearColumnSort(event: Event, args: MenuCommandItemCallbackArgs) {
if (args && args.column && this.sharedService) {
// get previously sorted columns
const allSortedCols = this.sortService.getCurrentColumnSorts();
const sortedColsWithoutCurrent = this.sortService.getCurrentColumnSorts(args.column.id + '');

if (Array.isArray(allSortedCols) && Array.isArray(sortedColsWithoutCurrent) && allSortedCols.length !== sortedColsWithoutCurrent.length) {
if (this.sharedService.gridOptions.backendServiceApi) {
this.sortService.onBackendSortChanged(e, { 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 && 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 = sortedColsWithoutCurrent.map((col) => {
return {
columnId: col && col.sortCol && col.sortCol.id,
sortAsc: col && col.sortAsc,
};
});
this.sharedService.grid.setSortColumns(updatedSortColumns); // add sort icon in UI
}
this.sortService.clearSortByColumnId(event, args.column.id);
}
}

Expand Down
122 changes: 122 additions & 0 deletions packages/common/src/services/__tests__/sort.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,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 = [
{ columnId: 'firstName', sortCol: { id: 'firstName', field: 'firstName', width: 100 }, sortAsc: false },
{ columnId: 'lastName', sortCol: { id: 'lastName', field: 'lastName', width: 100 }, sortAsc: true },
];
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, [{ columnId: 'id', 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 = { columnId: 'firstName', sortCol: { id: 'firstName', field: 'firstName', width: 100 }, sortAsc: false };
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, [{ columnId: 'customId', clearSortTriggered: true, sortAsc: true, sortCol: { field: 'customId', id: 'customId' } }]);
expect(setSortSpy).toHaveBeenCalled();
expect(sortDefaultSpy).toHaveBeenCalled();
});
});

describe('clearSorting method', () => {
let mockSortedCol: SingleColumnSort;
const mockColumns = [{ id: 'lastName', field: 'lastName' }, { id: 'firstName', field: 'firstName' }] as Column[];
Expand Down
58 changes: 53 additions & 5 deletions packages/common/src/services/sort.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,40 @@ export class SortService {
});
}

clearSortByColumnId(event: Event | undefined, columnId: string | number) {
// get previously sorted columns
const allSortedCols: ColumnSort[] = this.getCurrentColumnSorts();
const sortedColsWithoutCurrent: ColumnSort[] = this.getCurrentColumnSorts(`${columnId}`);

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

// update the grid 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._grid.setSortColumns(updatedSortColumns); // add sort icon in UI
}

// when there's no more sorting, we re-sort by the default sort field, user can customize it "defaultColumnSortFieldId", defaults to "id"
if (Array.isArray(sortedColsWithoutCurrent) && sortedColsWithoutCurrent.length === 0) {
this.sortLocalGridByDefaultSortFieldId();
}
}

/**
* Clear Sorting
* - 1st, remove the SlickGrid sort icons (this setSortColumns function call really does only that)
Expand All @@ -128,9 +162,7 @@ export class SortService {
this.onBackendSortChanged(undefined, { grid: this._grid, multiColumnSort: true, sortCols: [], clearSortTriggered: true });
} else {
if (this._columnDefinitions && Array.isArray(this._columnDefinitions) && this._columnDefinitions.length > 0) {
const sortColFieldId = this._gridOptions && this._gridOptions.defaultColumnSortFieldId || 'id';
const sortCol = { id: sortColFieldId, field: sortColFieldId } as Column;
this.onLocalSortChanged(this._grid, this._dataView, new Array({ columnId: sortCol.id, sortAsc: true, sortCol, clearSortTriggered: true }));
this.sortLocalGridByDefaultSortFieldId();
}
}
} else if (this._isBackendGrid) {
Expand Down Expand Up @@ -264,7 +296,7 @@ export class SortService {
}
}

onBackendSortChanged(event: Event | undefined, args: MultiColumnSort & { clearSortTriggered?: boolean }) {
onBackendSortChanged(event: Event | undefined, args: MultiColumnSort & { clearSortTriggered?: boolean; }) {
if (!args || !args.grid) {
throw new Error('Something went wrong when trying to bind the "onBackendSortChanged(event, args)" function, it seems that "args" is not populated correctly');
}
Expand All @@ -289,7 +321,7 @@ export class SortService {
}

/** When a Sort Changes on a Local grid (JSON dataset) */
onLocalSortChanged(grid: SlickGrid, dataView: SlickDataView, sortColumns: Array<ColumnSort & { clearSortTriggered?: boolean; }>, forceReSort = false) {
onLocalSortChanged(grid: SlickGrid, dataView: SlickDataView, sortColumns: Array<ColumnSort & { clearSortTriggered?: boolean; }>, forceReSort = false, emitSortChanged = false) {
const isTreeDataEnabled = this._gridOptions?.enableTreeData ?? false;

if (grid && dataView) {
Expand All @@ -310,9 +342,25 @@ export class SortService {
}

grid.invalidate();

if (emitSortChanged) {
this.emitSortChanged(EmitterType.local, sortColumns.map(col => {
return {
columnId: col.sortCol && col.sortCol.id || 'id',
direction: col.sortAsc ? SortDirection.ASC : SortDirection.DESC
};
}));
}
}
}

/** Call a local grid sort by its default sort field id (user can customize default field by configuring "defaultColumnSortFieldId" in the grid options, defaults to "id") */
sortLocalGridByDefaultSortFieldId() {
const sortColFieldId = this._gridOptions && this._gridOptions.defaultColumnSortFieldId || this._gridOptions.datasetIdPropertyName || 'id';
const sortCol = { id: sortColFieldId, field: sortColFieldId } as Column;
this.onLocalSortChanged(this._grid, this._dataView, new Array({ columnId: sortCol.id, sortAsc: true, sortCol, clearSortTriggered: true }));
}

sortComparers(sortColumns: ColumnSort[], dataRow1: any, dataRow2: any): number {
if (Array.isArray(sortColumns)) {
for (const sortColumn of sortColumns) {
Expand Down
Binary file not shown.
Loading

0 comments on commit 032886b

Please sign in to comment.