Skip to content

Commit e524a14

Browse files
authored
fix: onBeforeSort should be cancellable from Header Menu (#2610)
1 parent 1f0103a commit e524a14

10 files changed

Lines changed: 176 additions & 84 deletions

File tree

frameworks/angular-slickgrid/src/library/components/angular-slickgrid-outputs.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type {
22
Column,
3-
ColumnSort,
43
CurrentFilter,
54
CurrentSorter,
65
DragRowMove,
@@ -50,6 +49,7 @@ import type {
5049
OnHeaderMouseEventArgs,
5150
OnHeaderRowCellRenderedEventArgs,
5251
OnKeyDownEventArgs,
52+
OnLocalSortChangedArgs,
5353
OnRenderedEventArgs,
5454
OnRowCountChangedEventArgs,
5555
OnRowsChangedEventArgs,
@@ -190,7 +190,7 @@ export interface AngularSlickgridOutputs {
190190
onBeforeFilterChange: (e: CurrentFilter[]) => void;
191191
onBeforeFilterClear: (e: { columnId: string } | boolean) => void;
192192
onBeforeSearchChange: (e: OnSearchChangeEventArgs) => boolean | void;
193-
onBeforeSortChange: (e: Array<ColumnSort & { clearSortTriggered?: boolean }>) => void;
193+
onBeforeSortChange: (e: OnLocalSortChangedArgs) => void;
194194
onContextMenuClearGrouping: () => void;
195195
onContextMenuCollapseAllGroups: () => void;
196196
onContextMenuExpandAllGroups: () => void;

frameworks/slickgrid-react/src/components/slickgridReactProps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type {
22
Column,
3-
ColumnSort,
43
ContainerService,
54
CurrentFilter,
65
CurrentSorter,
@@ -53,6 +52,7 @@ import type {
5352
OnHeaderMouseEventArgs,
5453
OnHeaderRowCellRenderedEventArgs,
5554
OnKeyDownEventArgs,
55+
OnLocalSortChangedArgs,
5656
OnRenderedEventArgs,
5757
OnRowCountChangedEventArgs,
5858
OnRowsChangedEventArgs,
@@ -186,7 +186,7 @@ export interface SlickgridReactProps {
186186
onBeforeFilterChange?: ReactRegularEventHandler<CurrentFilter[]>;
187187
onBeforeFilterClear?: ReactRegularEventHandler<{ columnId: string } | boolean>;
188188
onBeforeSearchChange?: ReactRegularEventHandler<OnSearchChangeEventArgs, boolean | void>;
189-
onBeforeSortChange?: ReactRegularEventHandler<Array<ColumnSort & { clearSortTriggered?: boolean }>>;
189+
onBeforeSortChange?: ReactRegularEventHandler<OnLocalSortChangedArgs>;
190190
onContextMenuClearGrouping?: ReactRegularEventHandler<void>;
191191
onContextMenuCollapseAllGroups?: ReactRegularEventHandler<void>;
192192
onContextMenuExpandAllGroups?: ReactRegularEventHandler<void>;

frameworks/slickgrid-vue/src/components/slickgridVueProps.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type {
22
Column,
3-
ColumnSort,
43
CurrentFilter,
54
CurrentSorter,
65
DragRowMove,
@@ -51,6 +50,7 @@ import type {
5150
OnHeaderMouseEventArgs,
5251
OnHeaderRowCellRenderedEventArgs,
5352
OnKeyDownEventArgs,
53+
OnLocalSortChangedArgs,
5454
OnRenderedEventArgs,
5555
OnRowCountChangedEventArgs,
5656
OnRowsChangedEventArgs,
@@ -176,7 +176,7 @@ export interface SlickgridVueProps {
176176
onOnBeforeFilterChange?: VueRegularEventHandler<CurrentFilter[]>;
177177
onOnBeforeFilterClear?: VueRegularEventHandler<{ columnId: string } | boolean>;
178178
onOnBeforeSearchChange?: VueRegularEventHandler<OnSearchChangeEventArgs, boolean | void>;
179-
onOnBeforeSortChange?: VueRegularEventHandler<Array<ColumnSort & { clearSortTriggered?: boolean }>>;
179+
onOnBeforeSortChange?: VueRegularEventHandler<OnLocalSortChangedArgs>;
180180
onOnContextMenuClearGrouping?: VueRegularEventHandler<void>;
181181
onOnContextMenuCollapseAllGroups?: VueRegularEventHandler<void>;
182182
onOnContextMenuExpandAllGroups?: VueRegularEventHandler<void>;

packages/common/src/extensions/__tests__/slickHeaderMenu.spec.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const gridStub = {
6464
updateColumns: vi.fn(),
6565
validateColumnFreezeWidth: vi.fn(),
6666
validateColumnFreeze: vi.fn(),
67+
onBeforeSort: new SlickEvent(),
6768
onBeforeSetColumns: new SlickEvent(),
6869
onBeforeHeaderCellDestroy: new SlickEvent(),
6970
onClick: new SlickEvent(),
@@ -1231,7 +1232,7 @@ describe('HeaderMenu Plugin', () => {
12311232

12321233
// Verify slotRenderer was called with the click event as the third argument
12331234
expect(mockSlotRenderer).toHaveBeenCalledTimes(2); // once for render, once for click
1234-
const clickCallArgs = mockSlotRenderer.mock.calls[1]; // second call is from click
1235+
const clickCallArgs: any[] = mockSlotRenderer.mock.calls[1]; // second call is from click
12351236
expect(clickCallArgs[2]).toBeDefined();
12361237
expect(clickCallArgs[2]!.type).toBe('click');
12371238
});
@@ -2104,6 +2105,42 @@ describe('HeaderMenu Plugin', () => {
21042105
expect(sharedService.slickGrid.setSortColumns).toHaveBeenCalled();
21052106
});
21062107

2108+
it('should NOT trigger the command "sort-desc" and expect Sort Service to call "onBackendSortChanged" being called without the sorted column', () => {
2109+
const mockSortedCols: ColumnSort[] = [
2110+
{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } },
2111+
{ columnId: 'field2', sortAsc: true, sortCol: { id: 'field2', field: 'field2' } },
2112+
];
2113+
const mockSortedOuput: ColumnSort[] = [
2114+
{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } },
2115+
{ columnId: 'field2', sortAsc: false, sortCol: { id: 'field2', field: 'field2' } },
2116+
];
2117+
const previousSortSpy = vi.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[0]]);
2118+
const backendSortSpy = vi.spyOn(sortServiceStub, 'onBackendSortChanged');
2119+
vi.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
2120+
...gridOptionsMock,
2121+
enableSorting: true,
2122+
headerMenu: { hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true },
2123+
});
2124+
2125+
// cancel onBeforeSort event
2126+
const sed = new SlickEventData();
2127+
vi.spyOn(sed, 'getReturnValue').mockReturnValueOnce(false);
2128+
vi.spyOn(gridStub.onBeforeSort, 'notify').mockReturnValue(sed);
2129+
2130+
gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData as any, gridStub);
2131+
gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData as any, gridStub);
2132+
const headerButtonElm = headerDiv.querySelector('.slick-header-menu-button') as HTMLDivElement;
2133+
headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
2134+
const commandDivElm = gridContainerDiv.querySelector('[data-command="sort-desc"]') as HTMLDivElement;
2135+
2136+
const clickEvent = new Event('click');
2137+
commandDivElm.dispatchEvent(clickEvent);
2138+
expect(previousSortSpy).toHaveBeenCalled();
2139+
mockSortedOuput[1].sortCol = { ...columnsMock[1], ...mockSortedOuput[1].sortCol }; // merge with column header menu
2140+
expect(backendSortSpy).not.toHaveBeenCalledWith(expect.anything(), { multiColumnSort: true, sortCols: mockSortedOuput, grid: gridStub });
2141+
expect(sharedService.slickGrid.setSortColumns).not.toHaveBeenCalled();
2142+
});
2143+
21072144
it('should trigger the command "sort-desc" and expect Sort Service to call "onLocalSortChanged" being called without the sorted column', () => {
21082145
sharedService.dataView = dataViewStub;
21092146
const mockSortedCols: ColumnSort[] = [
@@ -2165,6 +2202,42 @@ describe('HeaderMenu Plugin', () => {
21652202
expect(gridSortSpy).toHaveBeenCalledWith(mockSortedOuput);
21662203
expect(gridStub.setSortColumns).toHaveBeenCalled();
21672204
});
2205+
2206+
it('should NOT trigger the command "sort-desc" and expect "onSort" event triggered when no DataView is provided', () => {
2207+
sharedService.dataView = undefined as any;
2208+
const mockSortedCols: ColumnSort[] = [
2209+
{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } },
2210+
{ columnId: 'field2', sortAsc: true, sortCol: { id: 'field2', field: 'field2' } },
2211+
];
2212+
const mockSortedOuput: ColumnSort[] = [
2213+
{ columnId: 'field1', sortAsc: true, sortCol: { id: 'field1', field: 'field1' } },
2214+
{ columnId: 'field2', sortAsc: false, sortCol: { id: 'field2', field: 'field2' } },
2215+
];
2216+
const previousSortSpy = vi.spyOn(sortServiceStub, 'getCurrentColumnSorts').mockReturnValue([mockSortedCols[0]]);
2217+
const gridSortSpy = vi.spyOn(gridStub.onSort, 'notify');
2218+
vi.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue({
2219+
...gridOptionsMock,
2220+
enableSorting: true,
2221+
backendServiceApi: undefined,
2222+
headerMenu: { hideFreezeColumnsCommand: true, hideColumnHideCommand: true, hideColumnResizeByContentCommand: true },
2223+
});
2224+
2225+
// cancel onBeforeSort event
2226+
const sed = new SlickEventData();
2227+
vi.spyOn(sed, 'getReturnValue').mockReturnValueOnce(false);
2228+
vi.spyOn(gridStub.onBeforeSort, 'notify').mockReturnValue(sed);
2229+
2230+
gridStub.onBeforeSetColumns.notify({ previousColumns: [], newColumns: columnsMock, grid: gridStub }, eventData as any, gridStub);
2231+
gridStub.onHeaderCellRendered.notify({ column: columnsMock[1], node: headerDiv, grid: gridStub }, eventData as any, gridStub);
2232+
const headerButtonElm = headerDiv.querySelector('.slick-header-menu-button') as HTMLDivElement;
2233+
headerButtonElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));
2234+
gridContainerDiv.querySelector('[data-command="sort-desc"]')!.dispatchEvent(new Event('click'));
2235+
expect(previousSortSpy).toHaveBeenCalled();
2236+
mockSortedOuput[1].sortCol = { ...columnsMock[1], ...mockSortedOuput[1].sortCol }; // merge with column header menu
2237+
expect(previousSortSpy).toHaveBeenCalled();
2238+
expect(gridSortSpy).not.toHaveBeenCalledWith(mockSortedOuput);
2239+
expect(gridStub.setSortColumns).not.toHaveBeenCalled();
2240+
});
21682241
});
21692242
});
21702243
});

packages/common/src/extensions/slickHeaderMenu.ts

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
MenuCommandItemCallbackArgs,
1616
MultiColumnSort,
1717
OnHeaderCellRenderedEventArgs,
18+
SingleColumnSort,
1819
} from '../interfaces/index.js';
1920
import type { FilterService } from '../services/filter.service.js';
2021
import { getTranslationPrefix } from '../services/index.js';
@@ -725,54 +726,63 @@ export class SlickHeaderMenu extends MenuBaseClass<HeaderMenu> {
725726
if (args?.column) {
726727
// get previously sorted columns
727728
const columnDef = args.column;
729+
const hasBackendServiceApi = !!this.sharedService.gridOptions.backendServiceApi;
730+
const isMultiColumnSort = !!this.sharedService.gridOptions.multiColumnSort;
728731

729732
// 1- get the sort columns without the current column, in the case of a single sort that would equal to an empty array
730-
// prettier-ignore
731-
const tmpSortedColumns = !this.sharedService.gridOptions.multiColumnSort ? [] : this.sortService.getCurrentColumnSorts(columnDef.id + '');
733+
const tmpSortedColumns = !isMultiColumnSort ? [] : this.sortService.getCurrentColumnSorts(columnDef.id + '');
732734

733-
let emitterType: EmitterType = 'local';
735+
const sortArgs = {
736+
multiColumnSort: hasBackendServiceApi || isMultiColumnSort,
737+
grid: this.grid,
738+
} as SingleColumnSort | MultiColumnSort;
734739

735-
// 2- add to the column array, the new sorted column by the header menu
736-
tmpSortedColumns.push({ columnId: columnDef.id, sortCol: columnDef, sortAsc: isSortingAsc });
737-
738-
if (this.sharedService.gridOptions.backendServiceApi) {
739-
this.sortService.onBackendSortChanged(event, {
740-
multiColumnSort: true,
741-
sortCols: tmpSortedColumns,
742-
grid: this.grid,
743-
});
744-
emitterType = 'remote';
740+
if (hasBackendServiceApi) {
741+
(sortArgs as MultiColumnSort).sortCols = tmpSortedColumns;
745742
} else if (this.sharedService.dataView) {
746-
this.sortService.onLocalSortChanged(this.grid, tmpSortedColumns);
747-
emitterType = 'local';
748-
} else {
749-
// when using customDataView, we will simply send it as a onSort event with notify
750-
args.grid.onSort.notify(tmpSortedColumns as unknown as MultiColumnSort);
743+
(sortArgs as SingleColumnSort).sortCol = columnDef;
751744
}
752745

753-
// update the sharedService.slickGrid sortColumns array which will at the same add the visual sort icon(s) on the UI
754-
const newSortColumns = tmpSortedColumns.map((col) => {
755-
return {
756-
columnId: col?.sortCol?.id ?? '',
757-
sortAsc: col?.sortAsc ?? true,
758-
};
759-
});
746+
if (this.grid.onBeforeSort.notify(sortArgs).getReturnValue() !== false) {
747+
let emitterType: EmitterType = 'local';
748+
// 2- add to the column array, the new sorted column by the header menu
749+
tmpSortedColumns.push({ columnId: columnDef.id, sortCol: columnDef, sortAsc: isSortingAsc });
750+
751+
if (hasBackendServiceApi) {
752+
this.sortService.onBackendSortChanged(event, sortArgs);
753+
emitterType = 'remote';
754+
} else if (this.sharedService.dataView) {
755+
this.sortService.onLocalSortChanged(this.grid, tmpSortedColumns);
756+
emitterType = 'local';
757+
} else {
758+
// when using customDataView, we will simply send it as a onSort event with notify
759+
args.grid.onSort.notify(tmpSortedColumns as unknown as MultiColumnSort);
760+
}
760761

761-
// add sort icon in UI
762-
this.grid.setSortColumns(newSortColumns);
763-
764-
// if we have an emitter type set, we will emit a sort changed
765-
// for the Grid State Service to see the change.
766-
// We also need to pass current sorters changed to the emitSortChanged method
767-
if (emitterType) {
768-
const currentLocalSorters: CurrentSorter[] = [];
769-
newSortColumns.forEach((sortCol) => {
770-
currentLocalSorters.push({
771-
columnId: `${sortCol.columnId}`,
772-
direction: sortCol.sortAsc ? 'ASC' : 'DESC',
773-
});
762+
// update the sharedService.slickGrid sortColumns array which will at the same add the visual sort icon(s) on the UI
763+
const newSortColumns = tmpSortedColumns.map((col) => {
764+
return {
765+
columnId: col?.sortCol?.id ?? '',
766+
sortAsc: col?.sortAsc ?? true,
767+
};
774768
});
775-
this.sortService.emitSortChanged(emitterType, currentLocalSorters);
769+
770+
// add sort icon in UI
771+
this.grid.setSortColumns(newSortColumns);
772+
773+
// if we have an emitter type set, we will emit a sort changed
774+
// for the Grid State Service to see the change.
775+
// We also need to pass current sorters changed to the emitSortChanged method
776+
if (emitterType) {
777+
const currentLocalSorters: CurrentSorter[] = [];
778+
newSortColumns.forEach((sortCol) => {
779+
currentLocalSorters.push({
780+
columnId: `${sortCol.columnId}`,
781+
direction: sortCol.sortAsc ? 'ASC' : 'DESC',
782+
});
783+
});
784+
this.sortService.emitSortChanged(emitterType, currentLocalSorters);
785+
}
776786
}
777787
}
778788
}

packages/common/src/interfaces/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export type * from './columnExcelExportOption.interface.js';
2323
export type * from './columnFilter.interface.js';
2424
export type * from './columnFilters.interface.js';
2525
export type * from './columnPicker.interface.js';
26-
export type * from './columnSort.interface.js';
2726
export type * from './componentEvents.interface.js';
2827
export type * from './compositeEditorError.interface.js';
2928
export type * from './compositeEditorLabel.interface.js';
@@ -115,7 +114,6 @@ export type * from './menuOptionItemCallbackArgs.interface.js';
115114
export type * from './metrics.interface.js';
116115
export type * from './metricTexts.interface.js';
117116
export type * from './mouseOffsetViewport.interface.js';
118-
export type * from './multiColumnSort.interface.js';
119117
export type * from './onEventArgs.interface.js';
120118
export type * from './onValidationErrorResult.interface.js';
121119
export type * from './operatorDetail.interface.js';
@@ -137,12 +135,12 @@ export type * from './searchColumnFilter.interface.js';
137135
export type * from './selectableOverrideCallback.interface.js';
138136
export type * from './selectionModelOption.interface.js';
139137
export type * from './selectOption.interface.js';
140-
export type * from './singleColumnSort.interface.js';
141138
export type * from './slickPlugin.interface.js';
142139
export type * from './slickRemoteModel.interface.js';
143140
export type * from './slickResizer.interface.js';
144141
export type * from './slickRowDetailView.interface.js';
145142
export type * from './sliderOption.interface.js';
143+
export type * from './sort.interfaces.js';
146144
export type * from './sorter.interface.js';
147145
export type * from './textExportOption.interface.js';
148146
export type * from './treeDataOption.interface.js';

packages/common/src/interfaces/multiColumnSort.interface.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

packages/common/src/interfaces/singleColumnSort.interface.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { SlickGrid } from '../core/index.js';
2+
import type { Column } from './column.interface.js';
3+
4+
export interface ColumnSort {
5+
/** Column Id to be sorted */
6+
columnId: string | number;
7+
8+
/** Are we sorting in Ascending order? It will default to ascending when this property is undefined */
9+
sortAsc?: boolean;
10+
11+
/** Column to be sorted */
12+
sortCol?: Column;
13+
}
14+
15+
export interface MultiColumnSort {
16+
/** SlickGrid grid object */
17+
grid: SlickGrid;
18+
19+
/** is it a multi-column sort? */
20+
multiColumnSort: true;
21+
22+
/** Array of Columns to be sorted */
23+
sortCols: ColumnSort[];
24+
25+
/** previous sort columns before calling onSort */
26+
previousSortColumns?: ColumnSort[];
27+
}
28+
29+
export interface SingleColumnSort extends ColumnSort {
30+
/** SlickGrid grid object */
31+
grid?: SlickGrid;
32+
33+
/** is it a multi-column sort? */
34+
multiColumnSort?: false;
35+
36+
/** previous sort columns before calling onSort */
37+
previousSortColumns?: ColumnSort[];
38+
}
39+
40+
export type OnBackendSortChangedArgs = (SingleColumnSort | MultiColumnSort) & { clearSortTriggered?: boolean };
41+
export type OnLocalSortChangedArgs = Array<ColumnSort & { clearSortTriggered?: boolean }>;

0 commit comments

Comments
 (0)