From c00b7f8cd758ce64e2fa849d20621a82c4405cc3 Mon Sep 17 00:00:00 2001 From: Andrey Dolzhikov Date: Mon, 31 Mar 2025 22:55:13 +0400 Subject: [PATCH 1/5] sorting --- .../tests/cardView/sorting/api.themes.ts | 192 +++++++++++++ .../tests/cardView/sorting/bahavior.themes.ts | 67 +++++ .../cardView/sorting/behavior.functional.ts | 253 ++++++++++++++++++ .../header_panel/column_sortable.tsx | 2 +- .../card_view/header_panel/header_panel.tsx | 6 +- .../grids/new/card_view/header_panel/item.tsx | 74 +++-- .../card_view/header_panel/options.test.ts | 10 +- .../grids/new/card_view/header_panel/view.tsx | 41 ++- .../columns_controller.test.ts | 4 +- .../columns_controller/options.test.ts | 4 +- .../grid_core/columns_controller/options.ts | 1 + .../new/grid_core/columns_controller/types.ts | 5 + .../data_controller/data_controller.ts | 20 +- .../grid_core/data_controller/options.test.ts | 6 +- .../data_controller/public_methods.test.ts | 7 +- .../items_controller/items_controller.test.ts | 4 +- .../__internal/grids/new/grid_core/options.ts | 3 + .../grids/new/grid_core/pager/view.test.ts | 9 +- .../new/grid_core/sorting_controller/index.ts | 3 + .../grid_core/sorting_controller/options.ts | 24 ++ .../sorting_controller/public_methods.ts | 15 ++ .../sorting_controller/sorting_controller.ts | 227 ++++++++++++++++ .../new/grid_core/sorting_controller/types.ts | 9 + .../sorting_controller/utils.test.ts | 103 +++++++ .../new/grid_core/sorting_controller/utils.ts | 32 +++ .../__internal/grids/new/grid_core/widget.ts | 10 +- 26 files changed, 1067 insertions(+), 64 deletions(-) create mode 100644 e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts create mode 100644 e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts create mode 100644 e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/index.ts create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/options.ts create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/public_methods.ts create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/sorting_controller.ts create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/types.ts create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/utils.test.ts create mode 100644 packages/devextreme/js/__internal/grids/new/grid_core/sorting_controller/utils.ts diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts new file mode 100644 index 000000000000..b701101632b7 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts @@ -0,0 +1,192 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - Sorting Behavior` + .page(url(__dirname, '../../container.html')); + +test('Sort index API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_sort_index_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('ShowSortIndexes API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_show_sort_indexes_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('AllowSorting API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await t + .click(cardView.getHeaders().getHeaderItemByText('Title').element); + + await testScreenshot(t, takeScreenshot, 'cardview_allow_sorting_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + allowSorting: false, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +[ + function (rowData) { + return rowData.id % 3; + }, + 'name', +].forEach((calculateSortValue) => { + test('CalculateSortValue API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, `cardview_calculate_sort_value_is_${calculateSortValue === 'name' ? 'filed' : 'function'}_api.png`, { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'asc', + calculateSortValue, + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); + }); +}); + +test('SortingMethod API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_sorting_method_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'asc', + sortingMethod(value1, value2) { + if (value1 === 'Mr.' && value2 !== 'Mr.') return 1; + if (value1 !== 'Mr.' && value2 === 'Mr.') return -1; + return value1.localeCompare(value2); + }, + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts new file mode 100644 index 000000000000..1f2b1f28cfbc --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts @@ -0,0 +1,67 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - Sorting Behavior - Themes` + .page(url(__dirname, '../../container.html')); + +test('Default render', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_headers_default_render.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); +test('Default multiple sorting render', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + await testScreenshot(t, takeScreenshot, 'cardview_headers_with_multiple_sorting_render.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + }, + { + dataField: 'name', + sortOrder: 'asc', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts new file mode 100644 index 000000000000..a3c09a607fb2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts @@ -0,0 +1,253 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; + +fixture.disablePageReloads`CardView - Sorting Behavior - Functional` + .page(url(__dirname, '../../container.html')); + +([ + ['none', false, false, false, [undefined, undefined]], + ['none', true, false, false, [undefined, undefined]], + ['none', false, true, false, [undefined, undefined]], + ['none', false, false, true, [undefined, undefined]], + + ['single', false, false, false, ['desc', undefined]], + ['single', true, false, false, ['desc', undefined]], + ['single', false, true, false, [undefined, undefined]], + ['single', false, false, true, [undefined, undefined]], + + ['multiple', false, false, false, ['desc', 0]], + ['multiple', true, false, false, ['desc', 0]], + ['multiple', false, true, false, [undefined, undefined]], + ['multiple', false, false, true, [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ]]) => { + test(`Change sorting of sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + + await t + .click(titleHeaderItem.element); + + await t + .click(titleHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); + +([ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['single', false, false, false, [undefined, undefined], ['asc', undefined]], + ['single', true, false, false, [undefined, undefined], ['asc', undefined]], + ['single', false, true, false, ['asc', undefined], [undefined, undefined]], + ['single', false, false, true, ['asc', undefined], [undefined, undefined]], + + ['multiple', false, false, false, [undefined, undefined], ['asc', 0]], + ['multiple', true, false, false, ['asc', 0], ['asc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ], + [ + nameSortOrder, + nameSortIndex, + ], +]) => { + test(`Change sorting of neighbour non sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + const nameHeaderItem = cardView.getHeaders().getHeaderItemByText('Name'); + + await t + .click(titleHeaderItem.element); + + await t + .click(nameHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex) + .expect(cardView.apiColumnOption('name', 'sortOrder')) + .eql(nameSortOrder) + .expect(cardView.apiColumnOption('name', 'sortIndex')) + .eql(nameSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); + +([ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['single', false, false, false, [undefined, undefined], ['desc', undefined]], + ['single', true, false, false, [undefined, undefined], ['desc', undefined]], + ['single', false, true, false, [undefined, undefined], [undefined, undefined]], + ['single', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['multiple', false, false, false, [undefined, undefined], ['desc', 0]], + ['multiple', true, false, false, ['asc', 0], ['desc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ], + [ + nameSortOrder, + nameSortIndex, + ], +]) => { + test(`Change sorting of neighbour sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + const nameHeaderItem = cardView.getHeaders().getHeaderItemByText('Name'); + + await t + .click(titleHeaderItem.element) + .click(nameHeaderItem.element, { + modifiers: { + shift: true, + }, + }); + + await t + .click(nameHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex) + .expect(cardView.apiColumnOption('name', 'sortOrder')) + .eql(nameSortOrder) + .expect(cardView.apiColumnOption('name', 'sortIndex')) + .eql(nameSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx index 0ba637d4c77f..0ba68ff7ae20 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx @@ -20,7 +20,7 @@ export interface Props extends Omit void; - dragTemplate?: ComponentType<{ column: Column; status: Status }>; + dragTemplate?: ComponentType<{ column: Column; status?: Status }>; } interface State { diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx index ba3efebeafd8..5604c582a316 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx @@ -1,5 +1,3 @@ -/* eslint-disable max-len */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; import { Scrollable } from '@ts/grids/new/grid_core/inferno_wrappers/scrollable'; import type { ComponentType } from 'inferno'; @@ -23,7 +21,7 @@ export interface HeaderPanelProps { showSortIndexes: boolean; - onSortClick: (column: Column) => void; + onSortClick: (column: Column, e: MouseEvent) => void; itemTemplate?: ComponentType<{ column: Column }>; @@ -63,7 +61,7 @@ export class HeaderPanel extends Component { { this.props.onSortClick(column); }} + onSortClick={(e): void => { this.props.onSortClick(column, e); }} template={this.props.itemTemplate} cssClass={this.props.itemCssClass} /> diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx index 8ad719adc970..3275d35cc66d 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx @@ -1,5 +1,6 @@ import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; import type { ComponentType } from 'inferno'; +import { Component } from 'inferno'; import type { Status } from './column_sortable'; @@ -10,6 +11,11 @@ export const CLASSES = { container: 'dx-cardview-header-item-sorting', order: 'dx-cardview-header-item-sorting-order', }, + headerFilter: { + iconEmpty: 'dx-header-filter-icon', + iconFilled: 'dx-header-filter-icon--selected', + }, + icon: 'dx-icon', }; // TODO: extract icons to separate component @@ -17,17 +23,21 @@ const ICONS = { // TODO: move to dx-icon once they are updated forbid: ( - + ), // TODO: move to dx-icon once they are updated moving: ( - + ), - sortUp: , - sortDown: , + sortUp:
, + sortDown:
, }; interface SortIconProps { @@ -38,17 +48,17 @@ interface SortIconProps { function SortIcon(props: SortIconProps): JSX.Element { return ( - +
{props.sortOrder === 'asc' && ICONS.sortUp} {props.sortOrder === 'desc' && ICONS.sortDown} { props.showSortIndex && ( - +
{props.sortIndex} - +
) } -
+
); } @@ -56,30 +66,36 @@ export interface ItemProps { column: Column; status?: Status; showSortIndexes?: boolean; - onSortClick?: () => void; template?: ComponentType<{ column: Column }>; cssClass?: string; + onSortClick?: (e: MouseEvent) => void; } -export function Item(props: ItemProps): JSX.Element { - const Template = props.column.headerItemTemplate ?? props.template; - const cssClass = `${CLASSES.item} ${props.column.headerItemCssClass ?? ''} ${props.cssClass ?? ''}`; +export class Item extends Component { + public render(): JSX.Element { + const Template = this.props.column.headerItemTemplate ?? this.props.template; + const cssClass = `${CLASSES.item} ${this.props.column.headerItemCssClass ?? ''} ${this.props.cssClass ?? ''}`; - return ( -
- { props.status && ICONS[props.status]} - { Template &&