diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts new file mode 100644 index 000000000000..b8a7ed48ca1a --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts @@ -0,0 +1,22 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`CardView - ColumnChooser.A11y.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('column chooser popup should have aria-label attribute', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const columnChooser = cardView.getColumnChooser(); + + await cardView.apiShowColumnChooser(); + + await t.expect(columnChooser.content.getAttribute('aria-label')).ok(); +}).before(async () => createWidget('dxCardView', { + columnChooser: { + enabled: true, + }, + columns: ['Column 1'], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts new file mode 100644 index 000000000000..5f1a73eeb508 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts @@ -0,0 +1,44 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`CardView - ColumnChooser.Functional` + .page(url(__dirname, '../../container.html')); + +// TODO: refator this when cardView is merged to 25.1 . This should be in ColumnChooser POM +const isOpened = (columnChooser) => columnChooser.element.exists; + +test('public method showColumnChooser', async (t) => { + const cardView = new CardView('#container'); + const columnChooser = cardView.getColumnChooser(); + + await t.expect(isOpened(columnChooser)).notOk(); + + await cardView.apiShowColumnChooser(); + await t.expect(isOpened(columnChooser)).ok(); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); +}); + +test('public method hideColumnChooser', async (t) => { + const cardView = new CardView('#container'); + const columnChooser = cardView.getColumnChooser(); + + await t.click(cardView.getColumnChooserButton()); + await t.expect(isOpened(columnChooser)).ok(); + + await cardView.apiHideColumnChooser(); + await t.expect(isOpened(columnChooser)).notOk(); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png new file mode 100644 index 000000000000..50f1a51a6783 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png new file mode 100644 index 000000000000..c11f6eb3b6f4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png new file mode 100644 index 000000000000..e02c71ffb903 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts new file mode 100644 index 000000000000..f41bca9eea68 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts @@ -0,0 +1,43 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../helpers/getPageUrl'; +import { testScreenshot } from '../../../helpers/themeUtils'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture`CardView - ColumnChooser.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('column chooser in select mode', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + const columnChooser = cardView.getColumnChooser(); + + await cardView.apiShowColumnChooser(); + + await testScreenshot(t, takeScreenshot, 'card-view_column-chooser.png', { element: columnChooser.content }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + columnChooser: { + enabled: true, + mode: 'select', + height: 400, + width: 400, + search: { + enabled: true, + }, + selection: { + allowSelectAll: true, + }, + }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2', allowHiding: false }, + { dataField: 'Column 3', showInColumnChooser: false }, + { dataField: 'Column 4' }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png b/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png new file mode 100644 index 000000000000..f867d818c52a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png differ diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts new file mode 100644 index 000000000000..9d180a29dcf8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts @@ -0,0 +1,26 @@ +import messageLocalization from '@js/common/core/localization/message'; +import type { ColumnChooser } from '@js/common/grids'; + +export const defaultOptions = { + columnChooser: { + enabled: false, + search: { + enabled: false, + timeout: 500, + editorOptions: {}, + }, + selection: { + allowSelectAll: false, + selectByClick: false, + recursive: false, + }, + position: undefined, + sortOrder: undefined, + mode: 'dragAndDrop', + width: 250, + height: 260, + title: messageLocalization.format('dxDataGrid-columnChooserTitle'), + emptyPanelText: messageLocalization.format('dxDataGrid-columnChooserEmptyText'), + container: undefined, + } as ColumnChooser, +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 6db8f21ffb23..4cb0ef5d6f09 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -19,6 +19,7 @@ import type { HeaderPanel } from '../header_panel/m_header_panel'; import modules from '../m_modules'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; +import { defaultOptions } from './const'; const COLUMN_CHOOSER_CLASS = 'column-chooser'; const COLUMN_CHOOSER_BUTTON_CLASS = 'column-chooser-button'; @@ -600,29 +601,7 @@ const columnHeadersView = (Base: ModuleType) => class ColumnC export const columnChooserModule = { defaultOptions() { - return { - columnChooser: { - enabled: false, - search: { - enabled: false, - timeout: 500, - editorOptions: {}, - }, - selection: { - allowSelectAll: false, - selectByClick: false, - recursive: false, - }, - position: undefined, - mode: 'dragAndDrop', - width: 250, - height: 260, - title: messageLocalization.format('dxDataGrid-columnChooserTitle'), - emptyPanelText: messageLocalization.format('dxDataGrid-columnChooserEmptyText'), - // TODO private option - container: undefined, - }, - }; + return defaultOptions; }, controllers: { columnChooser: ColumnChooserController, diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap index 23187a4262ea..c684f0dd7cdb 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -181,5 +181,6 @@ exports[`common initial render should be successfull 1`] = `
+ `; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx index 05dad093833c..8d8e122f05d4 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx +++ b/packages/devextreme/js/__internal/grids/new/card_view/main_view.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { combined } from '@ts/core/reactive/index'; +import { ColumnChooserView } from '@ts/grids/new/grid_core/column_chooser/index'; import { View } from '@ts/grids/new/grid_core/core/view'; import { FilterPanelView } from '@ts/grids/new/grid_core/filtering/filter_panel/view'; import { HeaderFilterPopupView } from '@ts/grids/new/grid_core/filtering/header_filter/index'; @@ -25,12 +26,14 @@ interface MainViewProps { HeaderPanel: ComponentType; HeaderFilterPopup: ComponentType; FilterPanel: ComponentType; + ColumnChooser: ComponentType; config: Config; rootElementRef: RefObject; } function MainViewComponent({ - Toolbar, Content, Pager, HeaderPanel, HeaderFilterPopup, FilterPanel, config, rootElementRef, + Toolbar, Content, Pager, HeaderPanel, HeaderFilterPopup, + FilterPanel, ColumnChooser, config, rootElementRef, }: MainViewProps): JSX.Element { return (<> @@ -53,6 +56,7 @@ function MainViewComponent({ */} + ); @@ -68,6 +72,7 @@ export class MainView extends View { HeaderPanelView, HeaderFilterPopupView, FilterPanelView, + ColumnChooserView, OptionsController, ] as const; @@ -78,6 +83,7 @@ export class MainView extends View { private readonly headerPanel: HeaderPanelView, private readonly headerFilterPopup: HeaderFilterPopupView, private readonly filterPanel: FilterPanelView, + private readonly columnsChooser: ColumnChooserView, private readonly options: OptionsController, ) { super(); @@ -93,6 +99,7 @@ export class MainView extends View { HeaderPanel: this.headerPanel.asInferno(), HeaderFilterPopup: this.headerFilterPopup.asInferno(), FilterPanel: this.filterPanel.asInferno(), + ColumnChooser: this.columnsChooser.asInferno(), config: combined({ rtlEnabled: this.options.oneWay('rtlEnabled'), disabled: this.options.oneWay('disabled'), diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/column_chooser.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/column_chooser.tsx new file mode 100644 index 000000000000..df3a43f3011f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/column_chooser.tsx @@ -0,0 +1,46 @@ +import type { ColumnChooserMode } from '@js/common/grids'; +import type { Properties as PopupProperties } from '@js/ui/popup'; +import type dxPopup from '@js/ui/popup'; +import type { Properties as TreeViewProperties } from '@js/ui/tree_view'; +import type dxTreeView from '@js/ui/tree_view'; +import type { RefObject } from 'inferno'; + +import { Popup } from '../inferno_wrappers/popup'; +import { TreeView } from '../inferno_wrappers/tree_view'; + +export interface ColumnChooserProps { + popupRef: RefObject; + + treeViewRef: RefObject; + + visible: boolean; + + mode: ColumnChooserMode; + + popupConfig: PopupProperties; + + treeViewConfig: TreeViewProperties; +} + +export function ColumnChooser(props: ColumnChooserProps): JSX.Element | null { + const { + visible, treeViewConfig, popupConfig, popupRef, treeViewRef, + } = props; + + if (!visible) { + return null; + } + + return ( + + + + ); +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.test.ts new file mode 100644 index 000000000000..e8b3fd765864 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.test.ts @@ -0,0 +1,192 @@ +/* eslint-disable spellcheck/spell-checker */ +import { describe, expect, it } from '@jest/globals'; +import type { SelectionChangedEvent } from '@js/ui/tree_view'; + +import { ColumnsController } from '../columns_controller'; +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller/options_controller.mock'; +import { ColumnChooserController } from './controller'; +import { expectColumnVisibility } from './test-utils'; + +const createColumnChooserController = (options?: Options): { + controller: ColumnChooserController; + columnsController: ColumnsController; + optionsController: OptionsControllerMock; +} => { + const optionsController = new OptionsControllerMock(options ?? { + columnChooser: { + enabled: true, + }, + }); + const columnsController = new ColumnsController(optionsController); + + const columnChooserController = new ColumnChooserController(columnsController, optionsController); + + return { + controller: columnChooserController, + columnsController, + optionsController, + }; +}; + +describe('ColumnChooser', () => { + describe('Controller', () => { + describe('chooserColumns state', () => { + const expectChooserColumns = ( + controller: ColumnChooserController, + columnNames: string[], + ): void => { + const columns = controller.chooserColumns + .unreactive_get() + .map((column) => column.name); + + expect(columns).toEqual(columnNames); + }; + + it('is correct', () => { + const { controller } = createColumnChooserController({ + columns: [ + { dataField: 'A Column' }, + { dataField: 'B Column' }, + ], + }); + + expectChooserColumns(controller, ['A Column', 'B Column']); + }); + + it('is correct when a column has showInColumnChooser=false', () => { + const { controller } = createColumnChooserController({ + columns: [ + { dataField: 'A Column' }, + { dataField: 'B Column', showInColumnChooser: false }, + { dataField: 'C Column' }, + ], + }); + + expectChooserColumns(controller, ['A Column', 'C Column']); + }); + + it('is correct when sortOrder is set', () => { + const { controller, optionsController } = createColumnChooserController({ + columns: [ + { dataField: 'C Column' }, + { dataField: 'A Column' }, + { dataField: 'B Column' }, + ], + columnChooser: { + sortOrder: 'asc', + }, + }); + + expectChooserColumns(controller, ['A Column', 'B Column', 'C Column']); + + optionsController.option('columnChooser.sortOrder', 'desc'); + + expectChooserColumns(controller, ['C Column', 'B Column', 'A Column']); + }); + }); + + describe('items state', () => { + it('is correct', () => { + const { controller } = createColumnChooserController({ + columns: [ + { dataField: 'A Column' }, + { dataField: 'B Column', caption: 'B Caption' }, + ], + }); + + expect(controller.items.unreactive_get()).toEqual( + [ + { + id: 0, columnName: 'A Column', selected: true, text: 'A Column', disabled: false, + }, + { + id: 1, columnName: 'B Column', selected: true, text: 'B Caption', disabled: false, + }, + ], + ); + }); + + it('updated when column option changed on select mode', () => { + const { columnsController, controller } = createColumnChooserController({ + columns: ['Column 1', 'Column 2', 'Column 3', 'Column 4', 'Column 5'], + columnChooser: { + enabled: true, + mode: 'select', + }, + }); + + const getItems = () => controller.items.unreactive_get(); + const getItem = (index: number) => getItems()[index]; + + const columnOption = (index: number, option, value) => { + const column = columnsController.columns.unreactive_get()[index]; + columnsController.columnOption(column, option, value); + }; + + columnOption(0, 'showInColumnChooser', false); + expect(getItems().filter((item) => item.columnName === 'Column 1')).toHaveLength(0); + + columnOption(0, 'showInColumnChooser', true); + expect(getItem(0).columnName).toBe('Column 1'); + + // test column.visible + columnOption(1, 'visible', false); + expect(getItem(1).selected).toBeFalsy(); + + columnOption(1, 'visible', true); + expect(getItem(1).selected).toBeTruthy(); + + // test column.caption + columnOption(2, 'caption', 'new caption'); + expect(getItem(2).text).toBe('new caption'); + + // test column.name + columnOption(3, 'name', 'new name'); + expect(getItem(3).columnName).toBe('new name'); + + // test column.allowHiding + columnOption(4, 'allowHiding', false); + expect(getItem(4).disabled).toBeTruthy(); + + columnOption(4, 'allowHiding', true); + expect(getItem(4).disabled).toBeFalsy(); + }); + }); + + it('onSelectionChanged', () => { + const { controller, columnsController } = createColumnChooserController({ + columns: [ + { dataField: 'A Column' }, + { dataField: 'B Column' }, + { dataField: 'C Column', allowHiding: false }, + { dataField: 'D Column', allowHiding: false }, + { dataField: 'E Column', visible: false }, + { dataField: 'F Column', visible: false }, + { dataField: 'G Column', allowHiding: false, visible: false }, + { dataField: 'H Column', allowHiding: false, visible: false }, + ], + }); + + controller.onSelectionChanged({ + component: { + getNodes: () => [ + { itemData: { columnName: 'A Column' }, selected: true }, + { itemData: { columnName: 'B Column' }, selected: false }, + { itemData: { columnName: 'C Column' }, selected: true }, + { itemData: { columnName: 'D Column' }, selected: false }, + { itemData: { columnName: 'E Column' }, selected: true }, + { itemData: { columnName: 'F Column' }, selected: false }, + { itemData: { columnName: 'G Column' }, selected: true }, + { itemData: { columnName: 'H Column' }, selected: false }, + ], + }, + } as unknown as SelectionChangedEvent); + + expectColumnVisibility( + columnsController, + [true, false, true, true, true, false, true, false], + ); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.ts new file mode 100644 index 000000000000..c29829ce15cd --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/controller.ts @@ -0,0 +1,65 @@ +import type { Item as TreeViewItemProperties, SelectionChangedEvent } from '@js/ui/tree_view'; +import type { SubsGets } from '@ts/core/reactive/index'; +import { computed } from '@ts/core/reactive/index'; +import { sortColumns } from '@ts/grids/grid_core/columns_controller/m_columns_controller_utils'; + +import { ColumnsController } from '../columns_controller/columns_controller'; +import type { Column } from '../columns_controller/types'; +import { getColumnIndexByName } from '../columns_controller/utils'; +import { OptionsController } from '../options_controller/options_controller'; + +export class ColumnChooserController { + public static dependencies = [ColumnsController, OptionsController] as const; + + public readonly chooserColumns: SubsGets; + + public readonly items: SubsGets; + + constructor( + private readonly columnsController: ColumnsController, + private readonly options: OptionsController, + ) { + this.chooserColumns = computed( + (columns, sortOrder) => { + let chooserColumns = columns.filter((column) => column.showInColumnChooser); + chooserColumns = sortColumns(chooserColumns, sortOrder); + + return chooserColumns; + }, + [ + this.columnsController.columns, + this.options.oneWay('columnChooser.sortOrder'), + ], + ); + + this.items = computed( + (chooserColumns) => chooserColumns.map((column, index) => ({ + id: index, + columnName: column.name, + selected: column.visible, + text: column.caption, + disabled: !column.allowHiding, + }) as TreeViewItemProperties), + [this.chooserColumns], + ); + } + + public onSelectionChanged(e: SelectionChangedEvent): void { + const nodes = e.component.getNodes(); + + this.columnsController.updateColumns((columns) => { + for (const node of nodes) { + const columnIndex = getColumnIndexByName(columns, node.itemData?.columnName); + const canHide = columns[columnIndex].allowHiding ?? true; + // in case when allowHiding=false and node.selected=false, we do not hide column + const skip = !canHide && !node.selected; + + if (!skip) { + columns[columnIndex].visible = node.selected; + } + } + + return [...columns]; + }); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/index.ts new file mode 100644 index 000000000000..6e100f85fc05 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/index.ts @@ -0,0 +1,4 @@ +export { ColumnChooserController } from './controller'; +export { defaultOptions, type Options } from './options'; +export { PublicMethods } from './public_methods'; +export { ColumnChooserView } from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/options.integration.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/options.integration.test.ts new file mode 100644 index 000000000000..7f4e1262bee4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/options.integration.test.ts @@ -0,0 +1,304 @@ +import { + afterEach, describe, expect, it, +} from '@jest/globals'; +import type { PositionConfig } from '@js/common/core/animation'; +import $ from '@js/core/renderer'; +import type dxPopup from '@js/ui/popup'; +import type dxTreeView from '@js/ui/tree_view'; +import CardView from '@ts/grids/new/card_view/widget'; +import type { Options as GridCoreOptions } from '@ts/grids/new/grid_core/options'; +// eslint-disable-next-line spellcheck/spell-checker +import { rerender } from 'inferno'; + +import { CLASSES as ContentViewClasses } from '../content_view/content_view'; +import { defaultOptions } from './options'; + +const SELECTORS = { + cardView: '.dx-cardview', + columnChooserBtn: '.dx-cardview-column-chooser-button', + popup: '.dx-popup', + treeView: '.dx-treeview', +}; + +const rootQuerySelector = (selector: string) => document.body.querySelector(selector); + +const setup = (options: GridCoreOptions = {}): CardView => { + const container = document.createElement('div'); + const { body } = document; + body.append(container); + + return new CardView(container, options); +}; + +const setupOpened = (options: GridCoreOptions = {}) => { + const cardView = setup(options); + + cardView.showColumnChooser(); + // eslint-disable-next-line spellcheck/spell-checker + rerender(); + + return cardView; +}; + +const getPopupInstance = (): dxPopup => { + const popupElement = rootQuerySelector(SELECTORS.popup); + const instance = ($(popupElement ?? undefined) as any).dxPopup('instance') as dxPopup; + + return instance; +}; + +const getTreeViewInstance = (): dxTreeView => { + const treeViewElement = rootQuerySelector(SELECTORS.treeView); + const instance = ($(treeViewElement ?? undefined) as any).dxTreeView('instance') as dxTreeView; + + return instance; +}; + +describe('Options', () => { + afterEach(() => { + const cardView = rootQuerySelector(SELECTORS.cardView); + // @ts-expect-error bad typed renderer + $(cardView ?? undefined as any)?.dxCardView('dispose'); + }); + + describe('ColumnChooser', () => { + it.each([ + { value: true, result: true }, + { value: false, result: false }, + { value: undefined, result: false }, + ])('enabled: %s', ({ value, result }) => { + setup({ columnChooser: { enabled: value } }); + + const button = rootQuerySelector(SELECTORS.columnChooserBtn); + + expect(!!button).toBe(result); + }); + + it.each<{ + value?: number; result: number | string | undefined; + }>([ + { value: undefined, result: defaultOptions.columnChooser?.width }, + { value: 100, result: 100 }, + { value: 1000, result: 1000 }, + ])('width: $value', ({ value, result }) => { + const cardView = setup({ + columnChooser: { enabled: true, width: value }, + }); + cardView.showColumnChooser(); + + const popup = getPopupInstance(); + + expect(popup.option('width')).toBe(result); + }); + + it.each<{ + value?: number; result: number | string | undefined; + }>([ + { value: undefined, result: defaultOptions.columnChooser?.height }, + { value: 100, result: 100 }, + { value: 1000, result: 1000 }, + ])('height: $value', ({ value, result }) => { + const cardView = setup({ + columnChooser: { enabled: true, height: value }, + }); + cardView.showColumnChooser(); + + const popup = getPopupInstance(); + + expect(popup.option('height')).toBe(result); + }); + + it.each<{ + value?: string; result?: string; + }>([ + { value: undefined, result: undefined }, + { value: '#custom', result: '#custom' }, + ])('container: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, container: value }, + }); + + const popup = getPopupInstance(); + + expect(popup.option('container')).toBe(result); + }); + + it.each<{ + value?: PositionConfig; result: PositionConfig; + }>([ + { + value: undefined, + result: { + my: 'right bottom', + at: 'right bottom', + of: ContentViewClasses.contentView, + collision: 'fit', + offset: '-2 -2', + boundaryOffset: '2 2', + }, + }, + { + value: { + my: 'right top', + at: 'right bottom', + of: '.dx-cardview-column-chooser-button', + }, + result: { + my: 'right top', + at: 'right bottom', + of: '.dx-cardview-column-chooser-button', + }, + }, + ])('position: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, position: value }, + }); + + const popup = getPopupInstance(); + + expect(popup.option('position')).toMatchObject(result); + }); + + // Implement when dragAndDrop mode is developed + it.skip.each<{ + value?: string; result?: string; + }>([ + { value: undefined, result: defaultOptions.columnChooser?.emptyPanelText }, + { value: 'custom value', result: 'custom value' }, + ])('emptyPanelText: $value', ({ value }) => { + setupOpened({ + columnChooser: { enabled: true, emptyPanelText: value }, + }); + + // TODO + }); + + it.each<{ + value?: string; result?: string; + }>([ + { value: undefined, result: defaultOptions.columnChooser?.title }, + { value: 'custom value', result: 'custom value' }, + ])('title: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, title: value }, + }); + + const popup = getPopupInstance(); + + expect(popup.option('toolbarItems[0].text')).toBe(result); + }); + + it.each<{ + value?: boolean; result: boolean; + }>([ + { value: undefined, result: false }, + { value: true, result: true }, + { value: false, result: false }, + ])('search.enabled: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, search: { enabled: value } }, + }); + + const treeView = getTreeViewInstance(); + + expect(treeView.option('searchEnabled')).toBe(result); + }); + + it.each<{ + value?: number; result: number; + }>([ + { value: undefined, result: 500 }, + { value: 100, result: 100 }, + { value: 1000, result: 1000 }, + ])('search.timeout: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, search: { enabled: true, timeout: value } }, + }); + + const treeView = getTreeViewInstance(); + + expect(treeView.option('searchTimeout')).toBe(result); + }); + + it.each<{ + value?: Record; result: Record; + }>([ + { result: {} }, + { value: { disabled: true }, result: { disabled: true } }, + { value: { height: 999 }, result: { height: 999 } }, + ])('search.editorOptions: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, search: { enabled: true, editorOptions: value } }, + }); + + const treeView = getTreeViewInstance(); + + expect(treeView.option('searchEditorOptions')).toMatchObject(result); + }); + + // Implement when dragAndDrop mode is developed + it.skip.each<{ + value?: 'select' | 'dragAndDrop'; result?: 'select' | 'dragAndDrop'; + }>([ + { value: undefined, result: defaultOptions.columnChooser?.mode }, + { value: 'select', result: 'select' }, + { value: 'dragAndDrop', result: 'dragAndDrop' }, + ])('mode: $value', ({ value }) => { + setupOpened({ + columnChooser: { enabled: true, mode: value }, + }); + + // TODO + }); + + it.each<{ + value?: boolean; result: 'selectAll' | 'normal'; + }>([ + { value: undefined, result: 'normal' }, + { value: true, result: 'selectAll' }, + { value: false, result: 'normal' }, + ])('selection.allowSelectAll: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, mode: 'select', selection: { allowSelectAll: value } }, + }); + + const treeView = getTreeViewInstance(); + + expect(treeView.option('showCheckBoxesMode')).toBe(result); + }); + + it.each<{ + value?: boolean; result: boolean; + }>([ + { value: undefined, result: false }, + { value: true, result: true }, + { value: false, result: false }, + ])('selection.selectByClick: $value', ({ value, result }) => { + setupOpened({ + columnChooser: { enabled: true, mode: 'select', selection: { selectByClick: value } }, + }); + + const treeView = getTreeViewInstance(); + + expect(treeView.option('selectByClick')).toBe(result); + }); + + it.each<{ + value?: 'asc' | 'desc'; result: string[]; + }>([ + { value: undefined, result: ['B Column', 'C Column', 'A Column'] }, + { value: 'asc', result: ['A Column', 'B Column', 'C Column'] }, + { value: 'desc', result: ['C Column', 'B Column', 'A Column'] }, + ])('sortOrder: $value', ({ value, result }) => { + setupOpened({ + columns: ['B Column', 'C Column', 'A Column'], + columnChooser: { enabled: true, sortOrder: value, mode: 'select' }, + }); + + const treeView = getTreeViewInstance(); + const items = (treeView.option('items') ?? []).map((item) => item.text); + + expect(items).toEqual(result); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/options.ts new file mode 100644 index 000000000000..0d305be6bacd --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/options.ts @@ -0,0 +1,12 @@ +import type { ColumnChooser, ColumnChooserSelectionConfig } from '@js/common/grids'; + +import { defaultOptions as columnChooserDefaultOptions } from '../../../grid_core/column_chooser/const'; + +export interface Options { + columnChooser?: Omit & { + // TODO: change d.ts files. Recursive selection isn't supported in CardView yet. + selection?: Omit; + }; +} + +export const defaultOptions = columnChooserDefaultOptions as Options; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/public_methods.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/public_methods.ts new file mode 100644 index 000000000000..3b157859e9b4 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/public_methods.ts @@ -0,0 +1,18 @@ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +import type { Constructor } from '../types'; +import type { GridCoreNewBase } from '../widget'; + +export function PublicMethods>(GridCore: TBase) { + return class GridCoreWithColumnChooser extends GridCore { + public showColumnChooser(): void { + this.columnChooserView.show(); + } + + public hideColumnChooser(): void { + this.columnChooserView.hide(); + } + }; +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/test-utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/test-utils.ts new file mode 100644 index 000000000000..f396f4071c89 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/test-utils.ts @@ -0,0 +1,130 @@ +/* eslint-disable spellcheck/spell-checker */ +// eslint-disable-next-line import/no-extraneous-dependencies +import { expect } from '@jest/globals'; + +import { ColumnsController } from '../columns_controller'; +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller/options_controller.mock'; +import { ToolbarController } from '../toolbar/controller'; +import { ToolbarView } from '../toolbar/view'; +import { ColumnChooserController } from './controller'; +import { ColumnChooserView } from './view'; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const createToolbarView = (optionsController: OptionsControllerMock) => { + const toolbarElement = document.createElement('div'); + const toolbarController = new ToolbarController(optionsController); + const toolbar = new ToolbarView(toolbarController, optionsController); + + return { toolbarElement, toolbar, toolbarController }; +}; + +const createColumnChooserView = ( + optionsController: OptionsControllerMock, + toolbarController?: ToolbarController, +): { + columnChooserElement: HTMLDivElement; + columnChooser: ColumnChooserView; + columnChooserController: ColumnChooserController; + columnsController: ColumnsController; +} => { + const columnChooserElement = document.createElement('div'); + + const columnsController = new ColumnsController(optionsController); + const columnChooserController = new ColumnChooserController(columnsController, optionsController); + + const columnChooser = new ColumnChooserView( + toolbarController ?? new ToolbarController(optionsController), + columnChooserController, + optionsController, + ); + + return { + columnChooserElement, + columnChooser, + columnChooserController, + columnsController, + }; +}; + +export const renderColumnChooser = async (options?: Options): Promise<{ + element: HTMLDivElement; + optionsController: OptionsControllerMock; + columnChooser: ColumnChooserView; + columnChooserController: ColumnChooserController; + columnsController: ColumnsController; +}> => { + const optionsController = new OptionsControllerMock(options ?? {}); + const { + columnChooserElement, columnChooser, columnChooserController, columnsController, + } = createColumnChooserView(optionsController); + + columnChooser.render(columnChooserElement); + + columnChooser.show(); + + await new Promise((resolve) => { setTimeout(resolve); }); + + // we need to fire 'onShown' event manually, so that setPopupAttributes() is called + // @ts-expect-error + columnChooser.popupRef.current?.option('onShown')({ + component: columnChooser.popupRef.current, + }); + + return { + element: columnChooserElement, + optionsController, + columnChooser, + columnChooserController, + columnsController, + }; +}; + +export const renderColumnChooserWithToolbar = (options?: Options): { + element: HTMLDivElement; + toolbar: ToolbarView; + toolbarController: ToolbarController; + columnChooser: ColumnChooserView; + optionsController: OptionsControllerMock; + columnChooserController: ColumnChooserController; + columnsController: ColumnsController; +} => { + const optionsController = new OptionsControllerMock(options ?? {}); + const { + toolbarElement, + toolbar, + toolbarController, + } = createToolbarView(optionsController); + const { + columnChooserElement, + columnChooser, + columnChooserController, + columnsController, + } = createColumnChooserView(optionsController, toolbarController); + + const element = document.createElement('div'); + element.append(toolbarElement, columnChooserElement); + + toolbar.render(toolbarElement); + columnChooser.render(columnChooserElement); + + return { + element, + toolbar, + toolbarController, + columnChooser, + optionsController, + columnChooserController, + columnsController, + }; +}; + +export const expectColumnVisibility = ( + columnsController: ColumnsController, + visibility: boolean[], +): void => { + const columns = columnsController.columns.unreactive_get(); + const columnsVisibility = columns.map((column) => column.visible); + + expect(columnsVisibility).toEqual(visibility); +}; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.test.ts new file mode 100644 index 000000000000..2c32e42a6b1b --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.test.ts @@ -0,0 +1,124 @@ +/* eslint-disable spellcheck/spell-checker */ +import { describe, expect, it } from '@jest/globals'; + +import { expectColumnVisibility, renderColumnChooser, renderColumnChooserWithToolbar } from './test-utils'; + +describe('ColumnChooser', () => { + describe('View', () => { + it('toolbar button', () => { + const { toolbarController, optionsController } = renderColumnChooserWithToolbar({ + toolbar: { + visible: true, + }, + columnChooser: { + enabled: true, + }, + }); + + let toolbarItems = toolbarController.items.unreactive_get(); + + expect(toolbarItems).toHaveLength(1); + expect(toolbarItems[0].name).toEqual('columnChooserButton'); + + optionsController.option('columnChooser.enabled', false); + + toolbarItems = toolbarController.items.unreactive_get(); + + expect(toolbarItems).toHaveLength(0); + }); + }); + + describe('Select mode', () => { + it('toggles column visibility on select/unselect', async () => { + const { columnChooser, columnsController } = await renderColumnChooser({ + columns: ['Column 1', 'Column 2', 'Column 3', 'Column 4'], + columnChooser: { + enabled: true, + mode: 'select', + }, + }); + const treeView = columnChooser.treeViewRef.current; + + treeView?.unselectItem(0); + expectColumnVisibility(columnsController, [false, true, true, true]); + + treeView?.selectItem(0); + expectColumnVisibility(columnsController, [true, true, true, true]); + }); + + it('toggles column visibility on selectAll/unselectAll', async () => { + const { columnsController, columnChooser } = await renderColumnChooser({ + columns: [ + { name: 'Column 1', visible: false }, + { name: 'Column 2', visible: true }, + ], + columnChooser: { + enabled: true, + mode: 'select', + }, + }); + const treeView = columnChooser.treeViewRef.current; + + treeView?.selectAll(); + expectColumnVisibility(columnsController, [true, true]); + + treeView?.unselectAll(); + expectColumnVisibility(columnsController, [false, false]); + }); + + it('toggles column visibility on selectAll/unselectAll when some column have showInColumnChooser=false', async () => { + const { columnsController, columnChooser } = await renderColumnChooser({ + columns: [ + { name: 'Column 1' }, + { name: 'Column 2', showInColumnChooser: false }, + { name: 'Column 3' }, + ], + columnChooser: { + enabled: true, + mode: 'select', + }, + }); + const treeView = columnChooser.treeViewRef.current; + + treeView?.unselectAll(); + expectColumnVisibility(columnsController, [false, true, false]); + + // make second column invisible + columnsController.columnOption( + columnsController.columns.unreactive_get()[1], + 'visible', + false, + ); + + treeView?.selectAll(); + expectColumnVisibility(columnsController, [true, false, true]); + }); + + it('does not toggle columns with allowHiding=false on selectAll/unselectAll', async () => { + const { columnsController, columnChooser } = await renderColumnChooser({ + columns: [ + { name: 'Column 1' }, + { name: 'Column 2', allowHiding: false }, + ], + columnChooser: { + enabled: true, + mode: 'select', + }, + }); + const treeView = columnChooser.treeViewRef.current; + + treeView?.unselectAll(); + expectColumnVisibility(columnsController, [false, true]); + + // make second column invisible + columnsController.columnOption( + columnsController.columns.unreactive_get()[1], + 'visible', + false, + ); + + treeView?.selectAll(); + expectColumnVisibility(columnsController, [true, true]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.tsx new file mode 100644 index 000000000000..633a1d3d23c8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/column_chooser/view.tsx @@ -0,0 +1,215 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { ColumnChooserMode } from '@js/common/grids'; +import messageLocalization from '@js/localization/message'; +import type { Properties as ButtonProperties } from '@js/ui/button'; +import type { Properties as PopupProperties, ShownEvent } from '@js/ui/popup'; +import type dxPopup from '@js/ui/popup'; +import { current, isGeneric, isMaterial } from '@js/ui/themes'; +import type { Properties as TreeViewProperties } from '@js/ui/tree_view'; +import type dxTreeView from '@js/ui/tree_view'; +import type { MapMaybeSubscribable, SubsGets } from '@ts/core/reactive/index'; +import { combined, computed, state } from '@ts/core/reactive/index'; +import { createRef } from 'inferno'; + +import { CLASSES as ContentViewClasses } from '../content_view/content_view'; +import { View } from '../core/view'; +import { OptionsController } from '../options_controller/options_controller'; +import { ToolbarController } from '../toolbar/controller'; +import type { PredefinedToolbarItem } from '../toolbar/types'; +import type { ColumnChooserProps } from './column_chooser'; +import { ColumnChooser } from './column_chooser'; +import { ColumnChooserController } from './controller'; + +const CLASS = { + root: 'column-chooser', + toolbarBtn: 'column-chooser-button', + list: 'column-chooser-list', + plain: 'column-chooser-plain', + dragMode: 'column-chooser-mode-drag', + selectMode: 'column-chooser-mode-select', +}; + +export class ColumnChooserView extends View { + protected override component = ColumnChooser; + + private readonly popupVisible = state(false); + + public readonly popupRef = createRef(); + + public readonly treeViewRef = createRef(); + + private readonly mode: SubsGets; + + public static dependencies = [ + ToolbarController, ColumnChooserController, OptionsController, + ] as const; + + constructor( + private readonly toolbarController: ToolbarController, + private readonly columnChooserController: ColumnChooserController, + private readonly options: OptionsController, + ) { + super(); + + this.mode = this.options.oneWay('columnChooser.mode'); + + this.toolbarController.addDefaultItem( + { + name: 'columnChooserButton', + widget: 'dxButton', + options: { + icon: 'column-chooser', + onClick: () => { this.popupVisible.update(true); }, + elementAttr: { + 'aria-haspopup': 'dialog', + class: this.addWidgetPrefix(CLASS.toolbarBtn), + }, + } as ButtonProperties, + showText: 'inMenu', + location: 'after', + locateInMenu: 'auto', + visible: true, + } as PredefinedToolbarItem, + this.options.oneWay('columnChooser.enabled'), + ); + } + + public show(): void { + this.popupVisible.update(true); + } + + public hide(): void { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.popupRef.current?.hide(); + } + + // TODO: move it to the other place + private addWidgetPrefix(cssClass: string): string { + return `dx-cardview-${cssClass}`; + } + + protected override getProps(): SubsGets { + return combined({ + popupRef: this.popupRef, + treeViewRef: this.treeViewRef, + + visible: this.popupVisible, + mode: this.mode, + + popupConfig: combined({ + shading: false, + showCloseButton: this.isMaterialOrGeneric(), + dragEnabled: true, + resizeEnabled: true, + wrapperAttr: { + class: this.getPopupWrapperClass(), + }, + _loopFocus: true, + + width: this.options.oneWay('columnChooser.width'), + height: this.options.oneWay('columnChooser.height'), + container: this.options.oneWay('columnChooser.container'), + rtlEnabled: this.options.oneWay('rtlEnabled'), + + position: computed( + (v) => v ?? { + my: 'right bottom', + at: 'right bottom', + of: ContentViewClasses.contentView, + collision: 'fit', + offset: '-2 -2', + boundaryOffset: '2 2', + }, + [this.options.oneWay('columnChooser.position')], + ), + + toolbarItems: computed( + (title) => { + const items = [ + { text: title, toolbar: 'top', location: this.isMaterialOrGeneric() ? 'before' : 'center' }, + ]; + + if (!this.isMaterialOrGeneric()) { + // @ts-expect-error + items.push({ shortcut: 'cancel' }); + } + + return items; + }, + [this.options.oneWay('columnChooser.title')], + ), + + onShown: (e: ShownEvent) => { this.setPopupAttributes(e?.component); }, + onHidden: () => { this.popupVisible.update(false); }, + } as MapMaybeSubscribable), + + treeViewConfig: combined({ + dataStructure: 'plain', + activeStateEnabled: true, + focusStateEnabled: true, + hoverStateEnabled: true, + disabled: false, + rootValue: null, + rtlEnabled: this.options.oneWay('rtlEnabled'), + + searchEditorOptions: this.options.oneWay('columnChooser.search.editorOptions'), + searchEnabled: this.options.oneWay('columnChooser.search.enabled'), + searchTimeout: this.options.oneWay('columnChooser.search.timeout'), + + ...this.getTreeViewConfig(), + } as MapMaybeSubscribable), + }); + } + + protected getTreeViewConfig(): MapMaybeSubscribable { + if (this.isSelectMode()) { + const controller = this.columnChooserController; + + return { + items: controller.items, + showCheckBoxesMode: computed( + (v) => (v ? 'selectAll' : 'normal'), + [this.options.oneWay('columnChooser.selection.allowSelectAll')], + ), + selectByClick: this.options.oneWay('columnChooser.selection.selectByClick'), + onSelectionChanged: controller.onSelectionChanged.bind(controller), + }; + } + + return {}; + } + + private isMaterialOrGeneric(): boolean { + const theme = current(); + + return isMaterial(theme) || isGeneric(theme); + } + + private getPopupWrapperClass(): string { + const modeSpecificClass = this.isSelectMode() ? CLASS.selectMode : CLASS.dragMode; + + return [this.addWidgetPrefix(CLASS.root), this.addWidgetPrefix(modeSpecificClass)].join(' '); + } + + private setPopupAttributes(popup: dxPopup): void { + // TODO: band columns aren't yet implemented in cardview + const isBandColumnsUsed = false; + + // @ts-expect-error + popup.setAria({ + label: messageLocalization.format('dxDataGrid-columnChooserTitle'), + }); + + // @ts-expect-error + popup.$content().addClass(this.addWidgetPrefix(CLASS.list)); + + if (this.isSelectMode() && !isBandColumnsUsed) { + // @ts-expect-error + popup.$content().addClass(this.addWidgetPrefix(CLASS.plain)); + } + } + + private isSelectMode(): boolean { + return this.mode.unreactive_get() === 'select'; + } +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap index e71a7eea318e..224c371d5d23 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/columns_controller.test.ts.snap @@ -6,6 +6,7 @@ exports[`ColumnsController columns should contain processed column configs 1`] = "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -27,6 +28,7 @@ exports[`ColumnsController columns should contain processed column configs 1`] = }, "headerItemTemplate": undefined, "name": "a", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 0, @@ -35,6 +37,7 @@ exports[`ColumnsController columns should contain processed column configs 1`] = "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -56,6 +59,7 @@ exports[`ColumnsController columns should contain processed column configs 1`] = }, "headerItemTemplate": undefined, "name": "b", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 1, @@ -64,6 +68,7 @@ exports[`ColumnsController columns should contain processed column configs 1`] = "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -85,6 +90,7 @@ exports[`ColumnsController columns should contain processed column configs 1`] = }, "headerItemTemplate": undefined, "name": "c", + "showInColumnChooser": true, "trueText": "true", "visible": false, "visibleIndex": 2, diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap index edd6e47bde76..bdd27340a613 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/__snapshots__/options.test.ts.snap @@ -6,6 +6,7 @@ exports[`Options columns when given as object should be normalized 1`] = ` "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -27,6 +28,7 @@ exports[`Options columns when given as object should be normalized 1`] = ` }, "headerItemTemplate": undefined, "name": "a", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 0, @@ -35,6 +37,7 @@ exports[`Options columns when given as object should be normalized 1`] = ` "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -56,6 +59,7 @@ exports[`Options columns when given as object should be normalized 1`] = ` }, "headerItemTemplate": undefined, "name": "b", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 1, @@ -64,6 +68,7 @@ exports[`Options columns when given as object should be normalized 1`] = ` "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -85,6 +90,7 @@ exports[`Options columns when given as object should be normalized 1`] = ` }, "headerItemTemplate": undefined, "name": "c", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 2, @@ -98,6 +104,7 @@ exports[`Options columns when given as string should be normalized 1`] = ` "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -119,6 +126,7 @@ exports[`Options columns when given as string should be normalized 1`] = ` }, "headerItemTemplate": undefined, "name": "a", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 0, @@ -127,6 +135,7 @@ exports[`Options columns when given as string should be normalized 1`] = ` "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -148,6 +157,7 @@ exports[`Options columns when given as string should be normalized 1`] = ` }, "headerItemTemplate": undefined, "name": "b", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 1, @@ -156,6 +166,7 @@ exports[`Options columns when given as string should be normalized 1`] = ` "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -177,6 +188,7 @@ exports[`Options columns when given as string should be normalized 1`] = ` }, "headerItemTemplate": undefined, "name": "c", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 2, diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts index 72796d7fc786..0aea78a31658 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/options.ts @@ -32,10 +32,12 @@ export const defaultColumnProperties = { visible: true, allowReordering: true, allowSorting: true, + allowHiding: true, allowFiltering: true, allowHeaderFiltering: true, trueText: messageLocalization.format('dxDataGrid-trueText'), falseText: messageLocalization.format('dxDataGrid-falseText'), + showInColumnChooser: true, } satisfies Partial; export const defaultColumnPropertiesByDataType: Record< diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts index ee36cd4e12fe..3be3fc5f738e 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/types.ts @@ -12,11 +12,13 @@ type InheritedColumnProps = | 'visible' | 'visibleIndex' | 'allowReordering' + | 'allowHiding' | 'allowFiltering' | 'allowHeaderFiltering' | 'trueText' | 'falseText' - | 'caption'; + | 'caption' + | 'showInColumnChooser'; export type Column = Pick, InheritedColumnProps> & { dataField?: string; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap index 25afd10b1355..bddd204dc5a6 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/grid_core/items_controller/__snapshots__/items_controller.test.ts.snap @@ -8,6 +8,7 @@ exports[`ItemsController createDataRow should process data object to data row us "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -29,6 +30,7 @@ exports[`ItemsController createDataRow should process data object to data row us }, "headerItemTemplate": undefined, "name": "a", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 0, @@ -43,6 +45,7 @@ exports[`ItemsController createDataRow should process data object to data row us "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -64,6 +67,7 @@ exports[`ItemsController createDataRow should process data object to data row us }, "headerItemTemplate": undefined, "name": "b", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 1, @@ -93,6 +97,7 @@ exports[`ItemsController createDataRow should process data object to data row us "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -114,6 +119,7 @@ exports[`ItemsController createDataRow should process data object to data row us }, "headerItemTemplate": undefined, "name": "a", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 0, @@ -128,6 +134,7 @@ exports[`ItemsController createDataRow should process data object to data row us "alignment": "left", "allowFiltering": true, "allowHeaderFiltering": false, + "allowHiding": true, "allowReordering": true, "allowSorting": true, "calculateCellValue": [Function], @@ -149,6 +156,7 @@ exports[`ItemsController createDataRow should process data object to data row us }, "headerItemTemplate": undefined, "name": "b", + "showInColumnChooser": true, "trueText": "true", "visible": true, "visibleIndex": 1, diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts index 75b9bfae7f1e..2c6f26bbb251 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts @@ -2,6 +2,7 @@ import browser from '@js/core/utils/browser'; import { isMaterialBased } from '@js/ui/themes'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; +import * as columnChooser from './column_chooser/index'; import * as columnsController from './columns_controller/index'; import * as contentView from './content_view/index'; import * as dataController from './data_controller/index'; @@ -33,6 +34,7 @@ export type Options = & selection.Options // TODO: Remove this mock search options during search implementation & SearchProperties + & columnChooser.Options & toolbar.Options; export const defaultOptions = { @@ -44,6 +46,7 @@ export const defaultOptions = { ...headerFilter.defaultOptions, ...contentView.defaultOptions, ...searchPanel.defaultOptions, + ...columnChooser.defaultOptions, ...selection.defaultOptions, searchText: '', } satisfies Options; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts index 7d3fa0f48a11..cf168d3afb54 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts @@ -9,6 +9,7 @@ import type { Subscription } from '@ts/core/reactive/index'; import { SearchView } from '@ts/grids/new/grid_core/search/view'; import { render } from 'inferno'; +import * as ColumnChooserModule from './column_chooser/index'; import { CompatibilityColumnsController } from './columns_controller/compatibility'; import * as ColumnsControllerModule from './columns_controller/index'; import * as DataControllerModule from './data_controller/index'; @@ -50,6 +51,10 @@ export class GridCoreNewBase< private pagerView!: PagerView; + private columnChooserController!: ColumnChooserModule.ColumnChooserController; + + protected columnChooserView!: ColumnChooserModule.ColumnChooserView; + private toolbarController!: ToolbarController; private toolbarView!: ToolbarView; @@ -78,6 +83,8 @@ export class GridCoreNewBase< this.diContext.register(PagerView); this.diContext.register(SearchController); this.diContext.register(SearchView); + this.diContext.register(ColumnChooserModule.ColumnChooserController); + this.diContext.register(ColumnChooserModule.ColumnChooserView); this.diContext.register(FilterControllerModule.FilterController); this.diContext.register(FilterControllerModule.FilterPanelView); this.diContext.register(FilterPanelView); @@ -105,6 +112,8 @@ export class GridCoreNewBase< this.pagerView = this.diContext.get(PagerView); this.searchController = this.diContext.get(SearchController); this.searchView = this.diContext.get(SearchView); + this.columnChooserController = this.diContext.get(ColumnChooserModule.ColumnChooserController); + this.columnChooserView = this.diContext.get(ColumnChooserModule.ColumnChooserView); this.errorController = this.diContext.get(ErrorController); this.filterController = this.diContext.get(FilterControllerModule.FilterController); this.filterPanelView = this.diContext.get(FilterControllerModule.FilterPanelView); @@ -167,8 +176,10 @@ export class GridCoreNew extends ColumnsControllerModule.PublicMethods( DataControllerModule.PublicMethods( SortingControllerModule.PublicMethods( FilterControllerModule.PublicMethods( - SelectionControllerModule.PublicMethods( - GridCoreNewBase, + ColumnChooserModule.PublicMethods( + SelectionControllerModule.PublicMethods( + GridCoreNewBase, + ), ), ), ),