diff --git a/e2e/testcafe-devextreme/tests/common/treeList/column_auto_width.ts b/e2e/testcafe-devextreme/tests/common/treeList/column_auto_width.ts new file mode 100644 index 000000000000..94b4c9b27be1 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/common/treeList/column_auto_width.ts @@ -0,0 +1,142 @@ +import { ClientFunction } from 'testcafe'; +import ExpandableCell from 'devextreme-testcafe-models/treeList/expandableCell'; +import TreeList from 'devextreme-testcafe-models/treeList'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`Columns Auto Width` + .page(url(__dirname, '../../container.html')); + +const treeListData = [ + { + id: 1, parentId: 0, name: 'Root item with a long name', size: 1024, date: '2024-01-01', + }, + { + id: 2, parentId: 1, name: 'Child 1', size: 512, date: '2024-02-01', + }, + { + id: 3, parentId: 1, name: 'Child 2 with a longer name value', size: 256, date: '2024-03-01', + }, + { + id: 4, parentId: 0, name: 'Second root', size: 2048, date: '2024-04-01', + }, +]; + +const treeListConfig = { + dataSource: treeListData, + keyExpr: 'id', + parentIdExpr: 'parentId', + columnAutoWidth: true, + width: 500, + repaintChangesOnly: true, + columns: [ + { dataField: 'name' }, + { dataField: 'size', width: 100 }, + { dataField: 'date', width: 150 }, + ], + scrolling: { + mode: 'standard', + useNative: false, + }, +}; + +// T1328904 +test('columns should update auto width when expanding row', async (t) => { + const treeList = new TreeList('#container'); + await t.expect(treeList.isReady()).ok(); + + const widthsBefore = await treeList.getHeaderCellWidths(); + const [nameWidthBefore] = widthsBefore; + await t.expect(widthsBefore).eql([250, 100, 150]); + + const expandableCell = new ExpandableCell(treeList.getDataRow(0).getDataCell(0)); + await t.click(expandableCell.getExpandButton()); + + const widthsAfter = await treeList.getHeaderCellWidths(); + const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter; + await t.expect(nameWidthAfter).gt(nameWidthBefore); + await t.expect(sizeWidthAfter).eql(100); + await t.expect(dateWidthAfter).eql(150); +}).before(async () => createWidget('dxTreeList', treeListConfig)); + +// T1328904 +test('columns should update auto width when collapsing row', async (t) => { + const treeList = new TreeList('#container'); + await t.expect(treeList.isReady()).ok(); + + const widthsBefore = await treeList.getHeaderCellWidths(); + const [nameWidthBefore] = widthsBefore; + + const expandableCell = new ExpandableCell(treeList.getDataRow(0).getDataCell(0)); + await t.click(expandableCell.getCollapseButton()); + + const widthsAfter = await treeList.getHeaderCellWidths(); + const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter; + await t.expect(nameWidthAfter).lt(nameWidthBefore); + await t.expect(sizeWidthAfter).eql(100); + await t.expect(dateWidthAfter).eql(150); +}).before(async () => createWidget('dxTreeList', { + ...treeListConfig, + expandedRowKeys: [1], +})); + +// T1328904 +test('columns should update auto width when expanded row keys are updated using API', async (t) => { + const treeList = new TreeList('#container'); + await t.expect(treeList.isReady()).ok(); + + const widthsBefore = await treeList.getHeaderCellWidths(); + const [nameWidthBefore] = widthsBefore; + await t.expect(widthsBefore).eql([250, 100, 150]); + + await treeList.apiOption('expandedRowKeys', [1]); + + const widthsAfter = await treeList.getHeaderCellWidths(); + const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter; + await t.expect(nameWidthAfter).gt(nameWidthBefore); + await t.expect(sizeWidthAfter).eql(100); + await t.expect(dateWidthAfter).eql(150); +}).before(async () => createWidget('dxTreeList', treeListConfig)); + +test('columns should update auto width after loadDescendants call', async (t) => { + const treeList = new TreeList('#container'); + await t.expect(treeList.isReady()).ok(); + + await treeList.apiLoadDescendants(1); + + const widthsBefore = await treeList.getHeaderCellWidths(); + await t.expect(widthsBefore).eql([250, 100, 150]); + + const expandableCell = new ExpandableCell(treeList.getDataRow(0).getDataCell(0)); + await t.click(expandableCell.getExpandButton()); + + const widthsAfter = await treeList.getHeaderCellWidths(); + const [nameWidthAfter, sizeWidthAfter, dateWidthAfter] = widthsAfter; + await t.expect(nameWidthAfter).gt(250); + await t.expect(sizeWidthAfter).eql(100); + await t.expect(dateWidthAfter).eql(150); +}).before(async () => createWidget('dxTreeList', { + ...treeListConfig, + dataSource: { + key: 'id', + load: ClientFunction((loadOptions: any) => { + let result = treeListData; + + if (loadOptions.filter) { + const parentIds = loadOptions.filter[0] === 'parentId' + ? [loadOptions.filter[2]] + : loadOptions.filter + .filter((f: any) => Array.isArray(f) && f[0] === 'parentId') + .map((f: any) => f[2]); + + if (parentIds.length) { + result = treeListData.filter((item) => parentIds.includes(item.parentId)); + } + } + return Promise.resolve(result); + }, { dependencies: { treeListData } }), + }, + remoteOperations: { + filtering: true, + }, +})); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts index c9b720bbf9a0..796133a4126f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts @@ -1244,7 +1244,7 @@ export class DataController extends DataHelperMixin(modules.Controller) { change.isDataChanged = true; change.repaintChangesOnly = operationTypes && !operationTypes.grouping && !operationTypes.filtering && this.option('repaintChangesOnly'); - if (operationTypes && (operationTypes.reload || operationTypes.paging || operationTypes.groupExpanding)) { + if (this.needUpdateDimensions(operationTypes)) { change.needUpdateDimensions = true; } } @@ -1261,6 +1261,12 @@ export class DataController extends DataHelperMixin(modules.Controller) { this._fireChanged(change); } + protected needUpdateDimensions(operationTypes) { + return operationTypes && ( + operationTypes.reload || operationTypes.paging || operationTypes.groupExpanding + ); + } + public loadingOperationTypes() { const dataSource = this.dataSource(); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts index f95e1d6e6d02..563d03534e02 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts @@ -198,7 +198,7 @@ export default class DataSourceAdapter extends modules.Controller { private _needClearStoreDataCache() { const remoteOperations = this.remoteOperations(); - const operationTypes = calculateOperationTypes(this._lastLoadOptions || {}, {}); + const operationTypes = this._calculateOperationTypes(this._lastLoadOptions || {}, {}); const isLocalOperations = Object.keys(remoteOperations).every((operationName) => !operationTypes[operationName] || !remoteOperations[operationName]); return !isLocalOperations; @@ -333,6 +333,10 @@ export default class DataSourceAdapter extends modules.Controller { return currentOperationTypes.some((operationType) => remoteOperations[operationType]); } + protected _calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload?: boolean) { + return calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload); + } + /** * @extended: virtual_scrolling, TreeLists's data_source_adapter, DataGrid's m_grouping */ @@ -412,7 +416,7 @@ export default class DataSourceAdapter extends modules.Controller { const loadOptions = extend({ pageIndex: this.pageIndex(), pageSize: this.pageSize() }, options.storeLoadOptions); - const operationTypes = calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload); + const operationTypes = this._calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload); this._customizeRemoteOperations(options, operationTypes); diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts index 1ef32df7ad38..b121904058a5 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/m_grid_view.ts @@ -172,38 +172,45 @@ export class ResizingController extends modules.ViewController { } private _refreshSizes(e) { - // @ts-expect-error - let resizeDeferred = new Deferred().resolve(null); const changeType = e?.changeType; const isDelayed = e?.isDelayed; - const items = this._dataController.items(); - if (!e || changeType === 'refresh' || changeType === 'prepend' || changeType === 'append') { + if (!e || ['refresh', 'prepend', 'append'].includes(changeType)) { if (!isDelayed) { - resizeDeferred = this.resize(); + return this.resize(); } - } else if (changeType === 'update') { - if (e.changeTypes?.length === 0) { - return resizeDeferred; + } + + if (changeType === 'update') { + if (!e.changeTypes?.length) { + // @ts-expect-error + return new Deferred().resolve(null); } - if ((items.length > 1 || e.changeTypes[0] !== 'insert') - && !(items.length === 0 && e.changeTypes[0] === 'remove') && !e.needUpdateDimensions) { + + const items = this._dataController.items(); + const isHidingNoDataPanel = items.length <= 1 && e.changeTypes[0] === 'insert'; + const isShowingNoDataPanel = items.length === 0 && e.changeTypes[0] === 'remove'; + + if (!isHidingNoDataPanel && !isShowingNoDataPanel && !e.needUpdateDimensions) { // @ts-expect-error - resizeDeferred = new Deferred(); + const deferred = new Deferred(); this._waitAsyncTemplates().done(() => { deferUpdate(() => deferRender(() => deferUpdate(() => { this._setScrollerSpacing(); this._rowsView.resize(); - resizeDeferred.resolve(); + deferred.resolve(); }))); - }).fail(resizeDeferred.reject); - } else { - resizeDeferred = this.resize(); + }).fail(deferred.reject); + + return deferred; } + + return this.resize(); } - return resizeDeferred; + // @ts-expect-error + return new Deferred().resolve(null); } /** diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts index 5091bd8a65f7..7f15512d86e2 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/data_controller/m_data_controller.ts @@ -95,6 +95,12 @@ export class TreeListDataController extends DataController { return super.publicMethods().concat(['expandRow', 'collapseRow', 'isRowExpanded', 'getRootNode', 'getNodeByKey', 'loadDescendants', 'forEachNode']); } + protected override needUpdateDimensions(operationTypes) { + return super.needUpdateDimensions(operationTypes) || ( + operationTypes && operationTypes.nodeExpanding + ); + } + private changeRowExpand(key) { if (this._dataSource) { const args: any = { diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts index ac9fc8a47263..f3c8a877f4ed 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts @@ -72,6 +72,8 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { private _totalItemsCount: any; + private _lastExpandedRowKeys: any; + private _createKeyGetter() { const keyExpr = this.getKeyExpr(); @@ -262,6 +264,15 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { return gridCoreUtils.combineFilters(parentIdFilters, 'or'); } + protected override _calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload?: boolean) { + const currentExpandedKeys = this.option('expandedRowKeys'); + + return { + ...super._calculateOperationTypes(loadOptions, lastLoadOptions, isFullReload), + nodeExpanding: !equalByValue(this._lastExpandedRowKeys, currentExpandedKeys), + }; + } + protected _customizeRemoteOperations(options, operationTypes) { super._customizeRemoteOperations.apply(this, arguments as any); @@ -607,6 +618,10 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { this._updateHasItemsMap(options); super._handleDataLoaded(options); + if (!options.isCustomLoading) { + this._lastExpandedRowKeys = this.option('expandedRowKeys')?.slice(); + } + if (data.isConverted && this._cachedStoreData) { this._cachedStoreData.isConverted = true; } diff --git a/packages/testcafe-models/dataGrid/index.ts b/packages/testcafe-models/dataGrid/index.ts index 95401009d7eb..4a9ce0e9f039 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -177,6 +177,19 @@ export default class DataGrid extends GridCore { return this.getHeadersContainer().find(`.${CLASS.scrollContainer}`); } + async getHeaderCellWidths(): Promise { + const cells = this.getHeaders().getHeaderRow(0).getHeaderCells(); + const count = await cells.count; + const widths: number[] = []; + + for (let i = 0; i < count; i += 1) { + const { width } = await cells.nth(i).boundingClientRect; + widths.push(Math.round(width)); + } + + return widths; + } + getRowsView(): Selector { return this.element.find(`.${this.addWidgetPrefix(CLASS.rowsView)}`); } diff --git a/packages/testcafe-models/treeList/index.ts b/packages/testcafe-models/treeList/index.ts index 4322e1ee429b..7a6ce27e167d 100644 --- a/packages/testcafe-models/treeList/index.ts +++ b/packages/testcafe-models/treeList/index.ts @@ -1,3 +1,4 @@ +import { ClientFunction } from 'testcafe'; import type { WidgetName } from '../types'; import DataGrid from '../dataGrid'; @@ -17,4 +18,13 @@ export default class TreeList extends DataGrid { getAdaptiveButtonSelector(): string { return `.${CLASS.adaptiveColumnButton}`; } + + apiLoadDescendants(key?: unknown): Promise { + const { getInstance } = this; + + return ClientFunction( + () => (getInstance() as any).loadDescendants(key), + { dependencies: { getInstance, key } }, + )(); + } }