Skip to content

Commit ddd6407

Browse files
authored
DataGrid - AI Column: Fix display of AI data during virtual scrolling (#31876)
Co-authored-by: Alyar <>
1 parent ed03ec3 commit ddd6407

File tree

8 files changed

+360
-15
lines changed

8 files changed

+360
-15
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>TestCafe Tests Container</title>
5+
6+
<link rel="dx-theme" data-theme="fluent.blue.light" href="../../../../../../../packages/devextreme/artifacts/css/dx.fluent.blue.light.css" data-active="false">
7+
<link rel="dx-theme" data-theme="fluent.blue.light.compact" href="../../../../../../../packages/devextreme/artifacts/css/dx.fluent.blue.light.compact.css" data-active="false">
8+
<link rel="dx-theme" data-theme="generic.light" href="../../../../../../../packages/devextreme/artifacts/css/dx.light.css" data-active="true">
9+
<link rel="dx-theme" data-theme="generic.light.compact" href="../../../../../../../packages/devextreme/artifacts/css/dx.light.compact.css" data-active="false">
10+
<link rel="dx-theme" data-theme="material.blue.light" href="../../../../../../../packages/devextreme/artifacts/css/dx.material.blue.light.css" data-active="false">
11+
<link rel="dx-theme" data-theme="material.blue.light.compact" href="../../../../../../../packages/devextreme/artifacts/css/dx.material.blue.light.compact.css" data-active="false">
12+
13+
<script type="text/javascript" src="../../../../../../../packages/devextreme/artifacts/js/jquery.min.js"></script>
14+
<script type="text/javascript" src="../../../../../../../packages/devextreme/artifacts/js/dx.all.js"></script>
15+
<script type="text/javascript" src="../../../../../../../packages/devextreme/artifacts/js/dx.ai-integration.js"></script>
16+
<script type="text/javascript" src="../../../../../../../packages/devextreme/artifacts/js/dx.aspnet.data.js"></script>
17+
18+
<style>
19+
* { caret-color: transparent !important; }
20+
.dx-scheduler .dx-scrollable-scroll { visibility: hidden !important; }
21+
</style>
22+
</head>
23+
<body class="dx-surface">
24+
<div id="parentContainer" role="main">
25+
<h1 style="position: fixed; left: 0; top: 0; clip: rect(1px, 1px, 1px, 1px);">Test header</h1>
26+
27+
<div id="container">
28+
</div>
29+
<div id="otherContainer">
30+
</div>
31+
</div>
32+
</body>
33+
</html>
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import DataGrid from 'devextreme-testcafe-models/dataGrid';
2+
import { ClientFunction } from 'testcafe';
3+
import url from '../../../../helpers/getPageUrl';
4+
import { createWidget } from '../../../../helpers/createWidget';
5+
6+
fixture`Ai Column.Virtual Scrolling.Functional`
7+
.page(url(__dirname, './pages/containerWithAIIntegration.html'));
8+
9+
const DATA_GRID_SELECTOR = '#container';
10+
11+
const checkAIColumnTexts = async (
12+
t: TestController,
13+
component: DataGrid,
14+
expectedRowCount: number,
15+
): Promise<void> => {
16+
const visibleRows: Record<string, any>[] = await component.apiGetVisibleRows();
17+
18+
await t.expect(visibleRows.length).eql(expectedRowCount);
19+
20+
// eslint-disable-next-line no-restricted-syntax
21+
for (const row of visibleRows) {
22+
await t
23+
.expect(component.getDataCell(row.dataIndex, 3).element.textContent)
24+
.eql(`Response ${row.data.name}`);
25+
}
26+
};
27+
28+
const resolveAIRequest = ClientFunction((): void => {
29+
const { aiResponseData } = (window as any);
30+
const { aiResolve } = (window as any);
31+
32+
if (aiResponseData && aiResolve) {
33+
aiResolve(aiResponseData);
34+
35+
(window as any).aiResponseData = null;
36+
(window as any).aiResolve = null;
37+
}
38+
});
39+
40+
const deleteGlobalVariables = ClientFunction((): void => {
41+
delete (window as any).aiResponseData;
42+
delete (window as any).aiResolve;
43+
});
44+
45+
test('DataGrid should send an AI request for rendered rows after scrolling without changing the page index', async (t) => {
46+
// arrange
47+
const dataGrid = new DataGrid(DATA_GRID_SELECTOR);
48+
49+
// assert
50+
await t
51+
.expect(dataGrid.getLoadPanel().isVisible())
52+
.ok();
53+
54+
// act
55+
await resolveAIRequest();
56+
57+
// assert
58+
await t
59+
.expect(dataGrid.isReady())
60+
.ok()
61+
.expect(dataGrid.getLoadPanel().isVisible())
62+
.notOk();
63+
await checkAIColumnTexts(t, dataGrid, 11);
64+
65+
// act
66+
await dataGrid.scrollTo(t, { y: 1000 });
67+
68+
// assert
69+
await t
70+
.expect(dataGrid.getScrollTop())
71+
.eql(1000)
72+
.expect(dataGrid.apiPageIndex())
73+
.eql(0)
74+
.expect(dataGrid.getDataCell(20, 0).element.textContent)
75+
.eql('21')
76+
.expect(dataGrid.getLoadPanel().isVisible())
77+
.ok();
78+
79+
// act
80+
await resolveAIRequest();
81+
82+
// assert
83+
await t
84+
.expect(dataGrid.isReady())
85+
.ok()
86+
.expect(dataGrid.getLoadPanel().isVisible())
87+
.notOk();
88+
await checkAIColumnTexts(t, dataGrid, 12);
89+
})
90+
.before(async () => createWidget('dxDataGrid', () => {
91+
const generateData = (rowCount: number): Record<string, number | string>[] => {
92+
const result: Record<string, number | string>[] = [];
93+
94+
for (let i = 0; i < rowCount; i += 1) {
95+
result.push({ id: i + 1, name: `Name ${i + 1}`, value: (i + 1) * 10 });
96+
}
97+
98+
return result;
99+
};
100+
101+
return {
102+
dataSource: generateData(200),
103+
height: 500,
104+
keyExpr: 'id',
105+
paging: {
106+
pageSize: 50,
107+
},
108+
scrolling: {
109+
mode: 'virtual',
110+
},
111+
columns: [
112+
{ dataField: 'id', caption: 'ID' },
113+
{ dataField: 'name', caption: 'Name' },
114+
{ dataField: 'value', caption: 'Value' },
115+
{
116+
type: 'ai',
117+
caption: 'AI Column',
118+
name: 'myColumn',
119+
ai: {
120+
prompt: 'Initial prompt',
121+
// eslint-disable-next-line new-cap
122+
aiIntegration: new (window as any).DevExpress.aiIntegration({
123+
sendRequest(prompt) {
124+
return {
125+
promise: new Promise<string>((resolve) => {
126+
const result: Record<string, string> = {};
127+
128+
Object.entries(prompt.data?.data).forEach(([key, value]) => {
129+
result[key] = `Response ${(value as any).name}`;
130+
});
131+
132+
(window as any).aiResponseData = JSON.stringify(result);
133+
(window as any).aiResolve = resolve;
134+
}),
135+
abort: (): void => {},
136+
};
137+
},
138+
}),
139+
},
140+
},
141+
],
142+
};
143+
}))
144+
.after(async () => {
145+
await deleteGlobalVariables();
146+
});
147+
148+
test('DataGrid should send an AI request for rendered rows after scrolling with changing the page index', async (t) => {
149+
// arrange
150+
const dataGrid = new DataGrid(DATA_GRID_SELECTOR);
151+
152+
// assert
153+
await t
154+
.expect(dataGrid.getLoadPanel().isVisible())
155+
.ok();
156+
157+
// act
158+
await resolveAIRequest();
159+
160+
// assert
161+
await t
162+
.expect(dataGrid.isReady())
163+
.ok()
164+
.expect(dataGrid.getLoadPanel().isVisible())
165+
.notOk();
166+
await checkAIColumnTexts(t, dataGrid, 11);
167+
168+
// act
169+
await dataGrid.scrollTo(t, { y: 1000 });
170+
171+
// assert
172+
await t
173+
.expect(dataGrid.getScrollTop())
174+
.eql(1000)
175+
.expect(dataGrid.apiPageIndex())
176+
.eql(1)
177+
.expect(dataGrid.getDataCell(20, 0).element.textContent)
178+
.eql('21')
179+
.expect(dataGrid.getLoadPanel().isVisible())
180+
.ok();
181+
182+
// act
183+
await resolveAIRequest();
184+
185+
// assert
186+
await t
187+
.expect(dataGrid.isReady())
188+
.ok()
189+
.expect(dataGrid.getLoadPanel().isVisible())
190+
.notOk();
191+
await checkAIColumnTexts(t, dataGrid, 12);
192+
})
193+
.before(async () => createWidget('dxDataGrid', () => {
194+
const generateData = (rowCount: number): Record<string, number | string>[] => {
195+
const result: Record<string, number | string>[] = [];
196+
197+
for (let i = 0; i < rowCount; i += 1) {
198+
result.push({ id: i + 1, name: `Name ${i + 1}`, value: (i + 1) * 10 });
199+
}
200+
201+
return result;
202+
};
203+
204+
return {
205+
dataSource: generateData(200),
206+
height: 500,
207+
keyExpr: 'id',
208+
paging: {
209+
pageSize: 20,
210+
},
211+
scrolling: {
212+
mode: 'virtual',
213+
},
214+
columns: [
215+
{ dataField: 'id', caption: 'ID' },
216+
{ dataField: 'name', caption: 'Name' },
217+
{ dataField: 'value', caption: 'Value' },
218+
{
219+
type: 'ai',
220+
caption: 'AI Column',
221+
name: 'myColumn',
222+
ai: {
223+
prompt: 'Initial prompt',
224+
// eslint-disable-next-line new-cap
225+
aiIntegration: new (window as any).DevExpress.aiIntegration({
226+
sendRequest(prompt) {
227+
return {
228+
promise: new Promise<string>((resolve) => {
229+
const result: Record<string, string> = {};
230+
231+
Object.entries(prompt.data?.data).forEach(([key, value]) => {
232+
result[key] = `Response ${(value as any).name}`;
233+
});
234+
235+
(window as any).aiResponseData = JSON.stringify(result);
236+
(window as any).aiResolve = resolve;
237+
}),
238+
abort: (): void => {},
239+
};
240+
},
241+
}),
242+
},
243+
},
244+
],
245+
};
246+
}))
247+
.after(async () => {
248+
await deleteGlobalVariables();
249+
});

e2e/testcafe-devextreme/tests/dataGrid/common/markup/markup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ test('Load panel should support string height and width', async (t) => {
1010
await dataGrid.apiBeginCustomLoading('test');
1111

1212
await t
13-
.expect(dataGrid.getLoadPanel().content.getStyleProperty('height'))
13+
.expect(dataGrid.getLoadPanel().getContent().getStyleProperty('height'))
1414
.eql('400px')
15-
.expect(dataGrid.getLoadPanel().content.getStyleProperty('width'))
15+
.expect(dataGrid.getLoadPanel().getContent().getStyleProperty('width'))
1616
.eql('330px');
1717
}).before(async () => createWidget('dxDataGrid', {
1818
dataSource: [],

packages/devextreme/js/__internal/grids/grid_core/ai_column/controllers/m_ai_column_controller.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isDefined } from '@ts/core/utils/m_type';
55
import type { Column, ColumnsController } from '../../columns_controller/m_columns_controller';
66
import type { DataController, HandleDataChangedArguments, UserData } from '../../data_controller/m_data_controller';
77
import { Controller } from '../../m_modules';
8+
import gridCoreUtils from '../../m_utils';
89
import type { InternalRequestCallbacks } from '../types';
910
import { getAICommandColumnDefaultOptions, isAIColumnAutoMode, isPromptOption } from '../utils';
1011
import { AIColumnIntegrationController } from './m_ai_column_integration_controller';
@@ -24,6 +25,8 @@ export class AIColumnController extends Controller {
2425

2526
private storeBeforePushHandler!: ({ changes }: { changes: DataChange[] }) => void;
2627

28+
private dataControllerChangedHandler!: () => void;
29+
2730
private aiColumnOptionChangedHandler!: (
2831
column: Column,
2932
optionName: string,
@@ -70,6 +73,31 @@ export class AIColumnController extends Controller {
7073
this.dataController.dataSource()?.changed.add(this.dataSourceChangedHandler);
7174
}
7275

76+
private unsubscribeFromDataControllerChanged(): void {
77+
if (!this.dataControllerChangedHandler) {
78+
return;
79+
}
80+
81+
this.dataController.changed.remove(this.dataControllerChangedHandler);
82+
}
83+
84+
private subscribeToDataControllerChanged(): void {
85+
if (!this.getAIColumns().length || !gridCoreUtils.isVirtualRowRendering(this)) {
86+
return;
87+
}
88+
89+
this.dataControllerChangedHandler = this.dataControllerChangedHandler
90+
?? this.handleDataControllerChanged.bind(this);
91+
92+
this.dataController.changed.add(this.dataControllerChangedHandler);
93+
}
94+
95+
private handleDataControllerChanged(): void {
96+
if (this.dataController.isViewportChanging()) {
97+
this.sendRequests();
98+
}
99+
}
100+
73101
private unsubscribeFromStoreEvents(): void {
74102
const store = this.dataController.store();
75103

@@ -143,12 +171,10 @@ export class AIColumnController extends Controller {
143171
});
144172
}
145173

146-
private handleDataSourceChanged(args?: HandleDataChangedArguments): void {
174+
private sendRequests(): void {
147175
const aiColumns = this.getAIColumns();
148176

149-
if (args?.changeType === 'loadError'
150-
|| aiColumns.length === 0
151-
|| !this.checkStoreKey()) {
177+
if (!aiColumns.length || !this.checkStoreKey()) {
152178
return;
153179
}
154180

@@ -159,6 +185,14 @@ export class AIColumnController extends Controller {
159185
}
160186
}
161187

188+
private handleDataSourceChanged(args?: HandleDataChangedArguments): void {
189+
if (args?.changeType === 'loadError') {
190+
return;
191+
}
192+
193+
this.sendRequests();
194+
}
195+
162196
protected callbackNames(): string[] {
163197
return ['aiRequestCompleted', 'aiRequestRejected'];
164198
}
@@ -178,6 +212,9 @@ export class AIColumnController extends Controller {
178212
this.unsubscribeFromStoreEvents();
179213
this.subscribeToStoreEvents();
180214

215+
this.unsubscribeFromDataControllerChanged();
216+
this.subscribeToDataControllerChanged();
217+
181218
this.addAICommandColumn();
182219
}
183220

@@ -287,5 +324,6 @@ export class AIColumnController extends Controller {
287324
super.dispose();
288325
this.dataController.dataSource()?.changed.remove(this.dataSourceChangedHandler);
289326
this.unsubscribeFromStoreEvents();
327+
this.unsubscribeFromDataControllerChanged();
290328
}
291329
}

0 commit comments

Comments
 (0)