Skip to content

Commit 467c92a

Browse files
authored
CardView: Fix cards not updating on column visibility/order changes (T1324855) (#33011)
Co-authored-by: Alyar <>
1 parent 9cad8f9 commit 467c92a

7 files changed

Lines changed: 196 additions & 1 deletion

File tree

e2e/testcafe-devextreme/tests/cardView/columnChooser/functional.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import CardView from 'devextreme-testcafe-models/cardView';
22
import url from '../../../helpers/getPageUrl';
33
import { createWidget } from '../../../helpers/createWidget';
4+
import { getCardFieldCaptions } from '../helpers/cardUtils';
45

56
fixture`CardView - ColumnChooser.Functional`
67
.page(url(__dirname, '../../container.html'));
@@ -184,3 +185,65 @@ test('ColumnChooser should receive and render custom texts', async (t) => {
184185
}).after(async (t) => {
185186
await t.eval(() => location.reload());
186187
});
188+
189+
test('cards should update when column is hidden via column chooser (select mode) (T1324855)', async (t) => {
190+
const cardView = new CardView('#container');
191+
192+
const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
193+
await t.expect(initialCaptions).eql(['A', 'B', 'C']);
194+
195+
await cardView.apiShowColumnChooser();
196+
197+
await t.click(cardView.getColumnChooser().getCheckbox(0));
198+
199+
const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2);
200+
await t.expect(captionsAfterHide).eql(['B', 'C']);
201+
202+
await t.click(cardView.getColumnChooser().getCheckbox(0));
203+
204+
const captionsAfterShow = await getCardFieldCaptions(t, cardView, 3);
205+
await t.expect(captionsAfterShow).eql(['A', 'B', 'C']);
206+
}).before(async () => createWidget('dxCardView', {
207+
dataSource: [
208+
{ a: 1, b: 2, c: 3 },
209+
],
210+
columns: ['a', 'b', 'c'],
211+
columnChooser: {
212+
enabled: true,
213+
mode: 'select',
214+
},
215+
}));
216+
217+
test('cards should update when column is hidden via column chooser (dragAndDrop mode) (T1324855)', async (t) => {
218+
const cardView = new CardView('#container');
219+
220+
const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
221+
await t.expect(initialCaptions).eql(['A', 'B', 'C']);
222+
223+
await cardView.apiShowColumnChooser();
224+
225+
await t.dragToElement(
226+
cardView.getHeaderPanel().getHeaderItem(0).element,
227+
cardView.getColumnChooser().content,
228+
);
229+
230+
const captionsAfterHide = await getCardFieldCaptions(t, cardView, 2);
231+
await t.expect(captionsAfterHide).eql(['B', 'C']);
232+
233+
await t.dragToElement(
234+
cardView.getColumnChooser().getColumn(0),
235+
cardView.getHeaderPanel().element,
236+
);
237+
238+
const captionsAfterShow = await getCardFieldCaptions(t, cardView, 3);
239+
await t.expect(captionsAfterShow).eql(['A', 'B', 'C']);
240+
}).before(async () => createWidget('dxCardView', {
241+
dataSource: [
242+
{ a: 1, b: 2, c: 3 },
243+
],
244+
columns: ['a', 'b', 'c'],
245+
columnChooser: {
246+
enabled: true,
247+
mode: 'dragAndDrop',
248+
},
249+
}));

e2e/testcafe-devextreme/tests/cardView/columnSortable/functional.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import TreeView from 'devextreme-testcafe-models/treeView';
33
import { Selector } from 'testcafe';
44
import url from '../../../helpers/getPageUrl';
55
import { createWidget } from '../../../helpers/createWidget';
6+
import { getCardFieldCaptions } from '../helpers/cardUtils';
67
import {
78
arrayMoveToGap,
89
dragToColumnChooser,
@@ -305,3 +306,38 @@ test('drag from columnChooser to headerPanel: when allowReordering: false', asyn
305306
mode: 'dragAndDrop',
306307
},
307308
}));
309+
310+
test('cards should update when columns are reordered (T1324855)', async (t) => {
311+
const cardView = new CardView('#container');
312+
313+
const initialCaptions = await getCardFieldCaptions(t, cardView, 3);
314+
await t.expect(initialCaptions).eql(['A', 'B', 'C']);
315+
316+
const headerPanel = cardView.getHeaderPanel();
317+
const firstHeader = headerPanel.getHeaderItem(0).element;
318+
const secondHeader = headerPanel.getHeaderItem(1).element;
319+
320+
await t.dragToElement(firstHeader, secondHeader, {
321+
destinationOffsetX: -5,
322+
destinationOffsetY: -20,
323+
speed: 0.5,
324+
});
325+
326+
// Wait for headers to update after drag
327+
await t.expect(cardView.getHeaders().getHeaderItemNth(0).element.innerText).notEql('A');
328+
329+
const headerCaptions: string[] = [];
330+
const headersCount = await cardView.getHeaders().getHeaderItemsElements().count;
331+
for (let i = 0; i < headersCount; i += 1) {
332+
headerCaptions.push(await cardView.getHeaders().getHeaderItemNth(i).element.innerText);
333+
}
334+
335+
const cardCaptions = await getCardFieldCaptions(t, cardView, headersCount);
336+
await t.expect(cardCaptions).eql(headerCaptions);
337+
}).before(async () => createWidget('dxCardView', {
338+
dataSource: [
339+
{ a: 1, b: 2, c: 3 },
340+
],
341+
columns: ['a', 'b', 'c'],
342+
allowColumnReordering: true,
343+
}));
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import CardView from 'devextreme-testcafe-models/cardView';
2+
3+
const getCardFieldCaptions = async (
4+
t: TestController,
5+
cardView: CardView,
6+
expectedCount: number,
7+
cardIndex = 0,
8+
): Promise<string[]> => {
9+
const card = cardView.getCard(cardIndex);
10+
const captions = await card.getCaptions();
11+
12+
await t.expect(captions.length).eql(expectedCount);
13+
14+
return captions;
15+
};
16+
17+
export { getCardFieldCaptions };

packages/devextreme/js/__internal/grids/new/card_view/widget.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,37 @@ describe('absence of multiple re-render', () => {
224224
});
225225
});
226226
});
227+
228+
describe('reactivity to column option changes', () => {
229+
const dataSource = [
230+
{ id: 1, name: 'Audi' },
231+
{ id: 2, name: 'BMW' },
232+
];
233+
234+
const columns = [
235+
{ dataField: 'id', caption: 'ID' },
236+
{ dataField: 'name', caption: 'Name' },
237+
];
238+
239+
it('should not cause extra re-render when sort/filter options change', () => {
240+
const cardTemplate = jest.fn();
241+
242+
const container = document.createElement('div');
243+
const cardView = new CardView(container, {
244+
keyExpr: 'id',
245+
dataSource,
246+
columns,
247+
cardTemplate,
248+
sorting: {
249+
mode: 'single',
250+
},
251+
} as CardViewOptions);
252+
253+
cardTemplate.mockClear();
254+
cardView.columnOption('name', 'sortOrder', 'asc');
255+
256+
// Should be called dataSource.length times (once per card for data update),
257+
// not dataSource.length * 2 (which would indicate extra re-render).
258+
expect(cardTemplate).toBeCalledTimes(dataSource.length);
259+
});
260+
});

packages/devextreme/js/__internal/grids/new/grid_core/items_controller/items_controller.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import { ColumnsController } from '@ts/grids/new/grid_core/columns_controller/co
55
import { DataController } from '@ts/grids/new/grid_core/data_controller/data_controller';
66
import { SearchController } from '@ts/grids/new/grid_core/search/index';
77

8-
import type { CardInfo, Column, FieldInfo } from '../columns_controller/types';
8+
import type {
9+
CardInfo, Column, FieldInfo,
10+
} from '../columns_controller/types';
911
import type { DataObject, Key } from '../data_controller/types';
12+
import { getColumnLayoutKey } from './utils';
1013

1114
export class ItemsController {
1215
private readonly selectedCardKeys = signal<Key[]>([]);
@@ -19,12 +22,22 @@ export class ItemsController {
1922

2023
public readonly additionalItems = signal<CardInfo[]>([]);
2124

25+
private readonly visibleColumnsLayout = computed(
26+
() => JSON.stringify(
27+
this.columnsController.visibleColumns.value
28+
.map(getColumnLayoutKey),
29+
),
30+
);
31+
2232
public readonly items = computed(
2333
() => {
2434
// NOTE: We should trigger computed by search options change,
2535
// But all work with these options encapsulated in SearchHighlightTextProcessor
2636
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
2737
this.searchController.highlightTextOptions.value;
38+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
39+
this.visibleColumnsLayout.value;
40+
2841
return this.dataController.items.value.map(
2942
(item, itemIndex) => this.createCardInfo(
3043
item,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { VisibleColumn } from '../columns_controller/types';
2+
3+
// NOTE: Column properties that are excluded from the layout key because they
4+
// don't directly affect card rendering. Data changes caused by sort/filter
5+
// arrive separately via dataController.items, so tracking these properties
6+
// in visibleColumnsLayout would cause redundant re-renders (T1306983, T1309423).
7+
const NON_LAYOUT_COLUMN_KEYS: ReadonlySet<string> = new Set([
8+
'sortOrder',
9+
'sortIndex',
10+
'filterValues',
11+
'filterType',
12+
]);
13+
14+
export const getColumnLayoutKey = (column: VisibleColumn): string => {
15+
const entries = Object.entries(column)
16+
.filter(([key]) => !NON_LAYOUT_COLUMN_KEYS.has(key));
17+
18+
return JSON.stringify(entries);
19+
};

packages/testcafe-models/cardView/card.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,17 @@ export default class Card {
4949
getHighlightedTexts(): Selector {
5050
return this.element.find(`.${CLASS.fieldValue}__${CLASS.highlightedState}`);
5151
}
52+
53+
async getCaptions(): Promise<string[]> {
54+
const captionElements = this.element.find(`.${CLASS.fieldCaption}`);
55+
const count = await captionElements.count;
56+
const captions: string[] = [];
57+
58+
for (let i = 0; i < count; i += 1) {
59+
const caption = await captionElements.nth(i).innerText;
60+
captions.push(caption.replace(/:$/, ''));
61+
}
62+
63+
return captions;
64+
}
5265
}

0 commit comments

Comments
 (0)