Skip to content

Commit bab2d66

Browse files
authored
DataGrid - AI Column: The scroll position is reset when the ai.prompt/ai.mode changes at runtime (#31957)
1 parent 990d0e4 commit bab2d66

File tree

4 files changed

+272
-8
lines changed

4 files changed

+272
-8
lines changed

e2e/testcafe-devextreme/tests/dataGrid/common/aiColumn/functional.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import DataGrid from 'devextreme-testcafe-models/dataGrid';
2+
import Button from 'devextreme-testcafe-models/button';
23
import url from '../../../../helpers/getPageUrl';
34
import { createWidget } from '../../../../helpers/createWidget';
45

@@ -502,3 +503,62 @@ test('Change the prompt in the AI Prompt Editor', async (t) => {
502503
},
503504
],
504505
})));
506+
507+
test('The scroll position should not reset when the ai.prompt changes at runtime', async (t) => {
508+
const dataGrid = new DataGrid(DATA_GRID_SELECTOR);
509+
const changePromptButton = new Button('#otherContainer');
510+
const scrollContainer = dataGrid.getHeadersScrollContainer();
511+
const scrollX = 200;
512+
513+
await t.expect(dataGrid.isReady()).ok();
514+
515+
await dataGrid.scrollTo(t, { x: scrollX });
516+
517+
await t.expect(scrollContainer.scrollLeft).eql(scrollX);
518+
519+
await t.click(changePromptButton.element);
520+
521+
await t.expect(dataGrid.getLoadPanel().element.exists).ok();
522+
523+
await t.expect(scrollContainer.scrollLeft).eql(scrollX);
524+
}).before(async () => {
525+
await createWidget('dxDataGrid', () => ({
526+
dataSource: [{ id: 1, name: 'Name 1', value: 1 }],
527+
keyExpr: 'id',
528+
width: 200,
529+
columns: [
530+
{ dataField: 'id', caption: 'ID', width: 100 },
531+
{ dataField: 'name', caption: 'Name', width: 200 },
532+
{ dataField: 'value', caption: 'Value', width: 100 },
533+
{
534+
type: 'ai',
535+
caption: 'AI Column',
536+
name: 'aiColumn',
537+
ai: {
538+
// eslint-disable-next-line new-cap
539+
aiIntegration: new (window as any).DevExpress.aiIntegration({
540+
sendRequest() {
541+
return {
542+
promise: new Promise<string>((resolve) => {
543+
setTimeout(() => {
544+
resolve('');
545+
}, 30000);
546+
}),
547+
abort: (): void => {},
548+
};
549+
},
550+
}),
551+
},
552+
width: 300,
553+
},
554+
],
555+
}));
556+
557+
await createWidget('dxButton', {
558+
text: 'Change prompt',
559+
onClick() {
560+
const grid = ($ as any)('#container').dxDataGrid('instance');
561+
grid.columnOption('aiColumn', 'ai.prompt', 'Updated prompt');
562+
},
563+
}, '#otherContainer');
564+
});

packages/devextreme/js/__internal/grids/grid_core/ai_column/__tests__/ai_column.integration.test.ts

Lines changed: 205 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,8 @@ describe('Options', () => {
416416
});
417417
});
418418

419-
describe('when the noDataText is set', () => {
420-
it('should render this text', async () => {
419+
describe('when noDataText is set', () => {
420+
it('should render this text (initial render)', async () => {
421421
const { component } = await createDataGrid({
422422
dataSource: [
423423
{ id: 1, name: 'Name 1', value: 10 },
@@ -454,10 +454,56 @@ describe('Options', () => {
454454
expect(component.getDataCell(0, 3).getText()).toBe('Test - No Data');
455455
expect(component.getDataCell(1, 3).getText()).toBe('Test - No Data');
456456
});
457+
458+
it('should render this text (runtime change)', async () => {
459+
const { component } = await createDataGrid({
460+
dataSource: [
461+
{ id: 1, name: 'Name 1', value: 10 },
462+
{ id: 2, name: 'Name 2', value: 20 },
463+
],
464+
keyExpr: 'id',
465+
columns: [
466+
{ dataField: 'id', caption: 'ID' },
467+
{ dataField: 'name', caption: 'Name' },
468+
{ dataField: 'value', caption: 'Value' },
469+
{
470+
type: 'ai',
471+
caption: 'AI Column',
472+
name: 'myColumn',
473+
cssClass: 'custom-class',
474+
ai: {
475+
prompt: 'Initial Prompt',
476+
aiIntegration: new AIIntegration({
477+
sendRequest(): RequestResult {
478+
return {
479+
promise: new Promise<string>((resolve) => {
480+
resolve('{"1":"","2":""}');
481+
}),
482+
abort: (): void => {},
483+
};
484+
},
485+
}),
486+
},
487+
},
488+
],
489+
});
490+
491+
await Promise.resolve();
492+
493+
expect(component.getDataCell(0, 3).getText()).toBe(EMPTY_CELL_TEXT);
494+
expect(component.getDataCell(1, 3).getText()).toBe(EMPTY_CELL_TEXT);
495+
496+
component.apiColumnOption('myColumn', 'ai.noDataText', 'Test - No Data');
497+
498+
await Promise.resolve();
499+
500+
expect(component.getDataCell(0, 3).getText()).toBe('Test - No Data');
501+
expect(component.getDataCell(1, 3).getText()).toBe('Test - No Data');
502+
});
457503
});
458504

459-
describe('when the emptyText is set', () => {
460-
it('should render this text', async () => {
505+
describe('when emptyText is set', () => {
506+
it('should render this text (initial render)', async () => {
461507
const { component } = await createDataGrid({
462508
keyExpr: 'id',
463509
dataSource: [
@@ -493,10 +539,55 @@ describe('Options', () => {
493539
expect(component.getDataCell(0, 3).getText()).toBe('Test - Empty Data');
494540
expect(component.getDataCell(1, 3).getText()).toBe('Test - Empty Data');
495541
});
542+
543+
it('should render this text (runtime change)', async () => {
544+
const { component } = await createDataGrid({
545+
keyExpr: 'id',
546+
dataSource: [
547+
{ id: 1, name: 'Name 1', value: 10 },
548+
{ id: 2, name: 'Name 2', value: 20 },
549+
],
550+
columns: [
551+
{ dataField: 'id', caption: 'ID' },
552+
{ dataField: 'name', caption: 'Name' },
553+
{ dataField: 'value', caption: 'Value' },
554+
{
555+
type: 'ai',
556+
caption: 'AI Column',
557+
name: 'myColumn',
558+
cssClass: 'custom-class',
559+
ai: {
560+
aiIntegration: new AIIntegration({
561+
sendRequest(): RequestResult {
562+
return {
563+
promise: new Promise<string>((resolve) => {
564+
resolve('{"1":"","2":""}');
565+
}),
566+
abort: (): void => {},
567+
};
568+
},
569+
}),
570+
},
571+
},
572+
],
573+
});
574+
575+
await Promise.resolve();
576+
577+
expect(component.getDataCell(0, 3).getText()).toBe(EMPTY_CELL_TEXT);
578+
expect(component.getDataCell(1, 3).getText()).toBe(EMPTY_CELL_TEXT);
579+
580+
component.apiColumnOption('myColumn', 'ai.emptyText', 'Test - Empty Data');
581+
582+
await Promise.resolve();
583+
584+
expect(component.getDataCell(0, 3).getText()).toBe('Test - Empty Data');
585+
expect(component.getDataCell(1, 3).getText()).toBe('Test - Empty Data');
586+
});
496587
});
497588

498589
describe('when the noDataText is set and mode = "manual"', () => {
499-
it('should render this text', async () => {
590+
it('should render this text (initial render)', async () => {
500591
const { component, instance } = await createDataGrid({
501592
dataSource: [
502593
{ id: 1, name: 'Name 1', value: 10 },
@@ -540,10 +631,61 @@ describe('Options', () => {
540631
expect(component.getDataCell(0, 3).getText()).toBe('Test - No Data');
541632
expect(component.getDataCell(1, 3).getText()).toBe('Test - No Data');
542633
});
634+
635+
it('should render this text (runtime change)', async () => {
636+
const { component, instance } = await createDataGrid({
637+
dataSource: [
638+
{ id: 1, name: 'Name 1', value: 10 },
639+
{ id: 2, name: 'Name 2', value: 20 },
640+
],
641+
keyExpr: 'id',
642+
columns: [
643+
{ dataField: 'id', caption: 'ID' },
644+
{ dataField: 'name', caption: 'Name' },
645+
{ dataField: 'value', caption: 'Value' },
646+
{
647+
type: 'ai',
648+
caption: 'AI Column',
649+
name: 'myColumn',
650+
cssClass: 'custom-class',
651+
ai: {
652+
prompt: 'Initial Prompt',
653+
mode: 'manual',
654+
aiIntegration: new AIIntegration({
655+
sendRequest(): RequestResult {
656+
return {
657+
promise: new Promise<string>((resolve) => {
658+
resolve('{"1":"","2":""}');
659+
}),
660+
abort: (): void => {},
661+
};
662+
},
663+
}),
664+
},
665+
},
666+
],
667+
});
668+
669+
expect(component.getDataCell(0, 3).getText()).toBe(EMPTY_CELL_TEXT);
670+
expect(component.getDataCell(1, 3).getText()).toBe(EMPTY_CELL_TEXT);
671+
672+
instance.sendAIColumnRequest('myColumn');
673+
await Promise.resolve();
674+
675+
expect(component.getDataCell(0, 3).getText()).toBe(EMPTY_CELL_TEXT);
676+
expect(component.getDataCell(1, 3).getText()).toBe(EMPTY_CELL_TEXT);
677+
678+
component.apiColumnOption('myColumn', 'ai.noDataText', 'Test - No Data');
679+
instance.sendAIColumnRequest('myColumn');
680+
await Promise.resolve();
681+
682+
expect(component.getDataCell(0, 3).getText()).toBe('Test - No Data');
683+
expect(component.getDataCell(1, 3).getText()).toBe('Test - No Data');
684+
});
543685
});
544686

545687
describe('when the emptyText is set and mode = "manual"', () => {
546-
it('should render this text', async () => {
688+
it('should render this text (initial render)', async () => {
547689
const { component, instance } = await createDataGrid({
548690
keyExpr: 'id',
549691
dataSource: [
@@ -588,6 +730,63 @@ describe('Options', () => {
588730
expect(component.getDataCell(0, 3).getText()).toBe('Test - No Data');
589731
expect(component.getDataCell(1, 3).getText()).toBe('Test - No Data');
590732
});
733+
734+
it('should render this text (runtime change)', async () => {
735+
const { component, instance } = await createDataGrid({
736+
keyExpr: 'id',
737+
dataSource: [
738+
{ id: 1, name: 'Name 1', value: 10 },
739+
{ id: 2, name: 'Name 2', value: 20 },
740+
],
741+
columns: [
742+
{ dataField: 'id', caption: 'ID' },
743+
{ dataField: 'name', caption: 'Name' },
744+
{ dataField: 'value', caption: 'Value' },
745+
{
746+
type: 'ai',
747+
caption: 'AI Column',
748+
name: 'myColumn',
749+
cssClass: 'custom-class',
750+
ai: {
751+
mode: 'manual',
752+
aiIntegration: new AIIntegration({
753+
sendRequest(): RequestResult {
754+
return {
755+
promise: new Promise<string>((resolve) => {
756+
resolve('{"1":"","2":""}');
757+
}),
758+
abort: (): void => {},
759+
};
760+
},
761+
}),
762+
},
763+
},
764+
],
765+
});
766+
767+
expect(component.getDataCell(0, 3).getText()).toBe(EMPTY_CELL_TEXT);
768+
expect(component.getDataCell(1, 3).getText()).toBe(EMPTY_CELL_TEXT);
769+
770+
instance.columnOption('myColumn', 'ai.emptyText', 'Test - Empty Data');
771+
await Promise.resolve();
772+
773+
expect(component.getDataCell(0, 3).getText()).toBe('Test - Empty Data');
774+
expect(component.getDataCell(1, 3).getText()).toBe('Test - Empty Data');
775+
776+
instance.columnOption('myColumn', 'ai.prompt', 'Updated Prompt');
777+
instance.sendAIColumnRequest('myColumn');
778+
await Promise.resolve();
779+
780+
expect(component.getDataCell(0, 3).getText()).toBe(EMPTY_CELL_TEXT);
781+
expect(component.getDataCell(1, 3).getText()).toBe(EMPTY_CELL_TEXT);
782+
783+
instance.columnOption('myColumn', 'ai.noDataText', 'Test - No Data');
784+
instance.sendAIColumnRequest('myColumn');
785+
await Promise.resolve();
786+
787+
expect(component.getDataCell(0, 3).getText()).toBe('Test - No Data');
788+
expect(component.getDataCell(1, 3).getText()).toBe('Test - No Data');
789+
});
591790
});
592791

593792
describe('when the keyExpr is not set', () => {

packages/devextreme/js/__internal/grids/grid_core/data_controller/m_data_controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,9 @@ export class DataController extends DataHelperMixin(modules.Controller) {
499499
// B255430
500500
const updateItemsHandler = function (change) {
501501
that._columnsController.columnsChanged.remove(updateItemsHandler);
502+
const needsFullRepaint = optionNames.visible || that._columnsController.getVisibleColumns().some((col) => col.showEditorAlways);
502503
that.updateItems({
503-
repaintChangesOnly: false,
504+
repaintChangesOnly: needsFullRepaint ? false : that.option('repaintChangesOnly'),
504505
event: change?.changeTypes?.event,
505506
virtualColumnsScrolling: change?.changeTypes?.virtualColumnsScrolling,
506507
});
@@ -524,7 +525,6 @@ export class DataController extends DataHelperMixin(modules.Controller) {
524525
}
525526

526527
const excludedOptionNames = [
527-
'ai',
528528
'width',
529529
'visibleWidth',
530530
'filterValue',

packages/testcafe-models/dataGrid/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const CLASS = {
7171
dialogWrapper: 'dx-dialog-wrapper',
7272
summaryTotal: 'dx-datagrid-summary-item',
7373
scrollableContainer: 'dx-scrollable-container',
74+
scrollContainer: 'dx-datagrid-scroll-container',
7475
columnsSeparator: 'dx-datagrid-columns-separator',
7576
toast: 'dx-toast-wrapper',
7677
dragHeader: 'drag-header',
@@ -161,6 +162,10 @@ export default class DataGrid extends GridCore {
161162
return new Headers(this.element.find(`.${this.addWidgetPrefix(CLASS.headers)}`), this.getName());
162163
}
163164

165+
getHeadersScrollContainer(): Selector {
166+
return this.getHeadersContainer().find(`.${CLASS.scrollContainer}`);
167+
}
168+
164169
getRowsView(): Selector {
165170
return this.element.find(`.${this.addWidgetPrefix(CLASS.rowsView)}`);
166171
}

0 commit comments

Comments
 (0)