Skip to content

Commit e2763c8

Browse files
authored
Collections: add reorder support in the findChanges function (T968114, T953965, T1122158) (DevExpress#30243)
1 parent 65a87d2 commit e2763c8

File tree

5 files changed

+209
-43
lines changed

5 files changed

+209
-43
lines changed

packages/devextreme/js/__internal/core/utils/m_array_compare.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ export const isKeysEqual = function (oldKeys, newKeys) {
3131
return true;
3232
};
3333

34-
export const findChanges = function (oldItems, newItems, getKey, isItemEquals) {
34+
export const findChanges = function ({
35+
oldItems,
36+
newItems,
37+
getKey,
38+
isItemEquals,
39+
detectReorders = false,
40+
}) {
3541
const oldIndexByKey = {};
3642
const newIndexByKey = {};
3743
let addedCount = 0;
@@ -88,10 +94,33 @@ export const findChanges = function (oldItems, newItems, getKey, isItemEquals) {
8894
});
8995
}
9096
} else {
91-
return;
97+
if (!detectReorders) {
98+
return;
99+
}
100+
101+
result.push({
102+
type: 'remove',
103+
key: getKey(oldItem),
104+
index: oldIndex,
105+
oldItem,
106+
});
107+
result.push({
108+
type: 'insert',
109+
data: newItem,
110+
index,
111+
});
112+
addedCount++;
113+
removeCount++;
92114
}
93115
}
94116
}
95117

118+
if (detectReorders) {
119+
const removes = result.filter((r) => r.type === 'remove').sort((a, b) => b.index - a.index);
120+
const inserts = result.filter((i) => i.type === 'insert').sort((a, b) => a.index - b.index);
121+
const updates = result.filter((u) => u.type === 'update');
122+
return [...removes, ...inserts, ...updates];
123+
}
124+
96125
return result;
97126
};

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,12 @@ export class DataController extends DataHelperMixin(modules.Controller) {
10691069
item.rowIndex = index;
10701070
});
10711071

1072-
const result = findChanges(oldItems, change.items, getRowKey, isItemEquals);
1072+
const result = findChanges({
1073+
oldItems,
1074+
newItems: change.items,
1075+
getKey: getRowKey,
1076+
isItemEquals,
1077+
});
10731078

10741079
if (!result) {
10751080
this._applyChangeFull(change);

packages/devextreme/js/__internal/ui/collection/m_collection_widget.live_update.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,13 @@ class CollectionWidgetLiveUpdate<
125125
}
126126
return this.keyOf(data);
127127
};
128-
const result = findChanges(this._itemsCache, this._editStrategy.itemsGetter(), keyOf, this._isItemStrictEquals.bind(this));
128+
const result = findChanges({
129+
oldItems: this._itemsCache,
130+
newItems: this._editStrategy.itemsGetter(),
131+
getKey: keyOf,
132+
isItemEquals: this._isItemStrictEquals.bind(this),
133+
detectReorders: true,
134+
});
129135
if (result && this._itemsCache.length && !this._shouldAddNewGroup(result, this._itemsCache)) {
130136
this._modifyByChanges(result, true);
131137
this._renderEmptyMessage();
Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,137 @@
11
import { findChanges } from 'core/utils/array_compare';
22
import { extend } from 'core/utils/extend';
3+
import { applyBatch } from 'common/data/array_utils';
4+
5+
const ITEMS_ARRAY_LENGTH = 4;
6+
const createItems = (length = ITEMS_ARRAY_LENGTH) => Array.from({ length }, (_, i) => ({ a: `Item ${i}`, id: i }));
37

48
QUnit.module('findChanges', {
59
beforeEach: function() {
610
const isItemEquals = (item1, item2) => JSON.stringify(item1) === JSON.stringify(item2);
7-
this.oldItems = [{ a: 'Item 0', id: 0 }, { a: 'Item 1', id: 1 }];
11+
const keyOf = item => item.id;
12+
const keyInfo = {
13+
key: () => 'id',
14+
keyOf,
15+
};
16+
this.oldItems = createItems();
817
this.newItems = extend(true, [], this.oldItems);
9-
this.findChanges = () => findChanges(this.oldItems, this.newItems, item => item.id, isItemEquals);
18+
this.findChanges = () => findChanges({
19+
oldItems: this.oldItems,
20+
newItems: this.newItems,
21+
getKey: item => item.id,
22+
isItemEquals,
23+
detectReorders: true,
24+
});
25+
26+
this.checkChanges = (assert) => {
27+
const changes = this.findChanges();
28+
29+
const output = applyBatch({
30+
keyInfo,
31+
useInsertIndex: true,
32+
immutable: true,
33+
data: this.oldItems,
34+
changes,
35+
});
36+
37+
assert.deepEqual(this.newItems, output, 'changes applied correctly');
38+
};
1039
}
1140
}, function() {
1241
QUnit.test('add item in the beginning', function(assert) {
13-
this.newItems.unshift({ a: 'Item 2', id: 2 });
14-
15-
const changes = this.findChanges();
16-
17-
assert.equal(changes.length, 1);
18-
assert.equal(changes[0].type, 'insert');
19-
assert.equal(changes[0].data.id, 2);
42+
this.newItems.unshift({ a: 'Item 4', id: 4 });
43+
this.checkChanges(assert);
2044
});
2145

2246
QUnit.test('remove item from the beginning', function(assert) {
2347
this.newItems.shift();
24-
25-
const changes = this.findChanges();
26-
27-
assert.equal(changes.length, 1);
28-
assert.equal(changes[0].type, 'remove');
29-
assert.equal(changes[0].key, 0);
48+
this.checkChanges(assert);
3049
});
3150

3251
QUnit.test('remove(beginning), insert(end), update', function(assert) {
3352
this.newItems.shift();
34-
this.newItems.push({ a: 'Item 2', id: 2 });
53+
this.newItems.push({ a: 'Item 4', id: 4 });
3554
this.newItems[0].a = 'Item 1 updated';
3655

37-
const changes = this.findChanges();
38-
39-
assert.equal(changes.length, 3);
40-
assert.equal(changes[0].type, 'remove');
41-
assert.equal(changes[0].key, 0);
42-
assert.equal(changes[1].type, 'update');
43-
assert.equal(changes[1].data.id, 1);
44-
assert.equal(changes[2].type, 'insert');
45-
assert.equal(changes[2].data.id, 2);
56+
this.checkChanges(assert);
4657
});
4758

4859
QUnit.test('remove(end), insert(beginning), update', function(assert) {
4960
this.newItems.pop();
50-
this.newItems.unshift({ a: 'Item 2', id: 2 });
61+
this.newItems.unshift({ a: 'Item 4', id: 4 });
5162
this.newItems[1].a = 'Item 0 updated';
5263

53-
const changes = this.findChanges();
54-
55-
assert.equal(changes[0].type, 'insert');
56-
assert.equal(changes[0].data.id, 2);
57-
assert.equal(changes[1].type, 'update');
58-
assert.equal(changes[1].data.id, 0);
59-
assert.equal(changes[2].type, 'remove');
60-
assert.equal(changes[2].key, 1);
64+
this.checkChanges(assert);
6165
});
6266

6367
QUnit.test('remove(end), update(beginning)', function(assert) {
6468
this.newItems.pop();
6569
this.newItems[0].a = 'Item 0 updated';
6670

67-
const changes = this.findChanges();
71+
this.checkChanges(assert);
72+
});
73+
74+
QUnit.module('reorder 1-1', function() {
75+
for(let from = 0; from < ITEMS_ARRAY_LENGTH; from++) {
76+
for(let to = 0; to < ITEMS_ARRAY_LENGTH; to++) {
77+
if(from === to) continue;
78+
79+
QUnit.test(`move item from index ${from} to ${to}`, function(assert) {
80+
const [itemToMove] = this.newItems.splice(from, 1);
81+
this.newItems.splice(to, 0, itemToMove);
82+
83+
this.checkChanges(assert);
84+
});
85+
}
86+
}
87+
});
88+
89+
QUnit.test('reorder of several elements', function(assert) {
90+
this.oldItems = createItems(5);
91+
this.newItems = [...this.oldItems].reverse();
92+
93+
this.checkChanges(assert);
94+
});
95+
96+
QUnit.test('reorder + insert', function(assert) {
97+
const [itemToMove] = this.newItems.splice(3, 1);
98+
this.newItems.splice(2, 0, itemToMove);
99+
this.newItems.push({ a: 'Item 4', id: 4 });
100+
101+
this.checkChanges(assert);
102+
});
103+
104+
QUnit.test('reorder + remove', function(assert) {
105+
const [itemToMove] = this.newItems.splice(3, 1);
106+
this.newItems.splice(2, 0, itemToMove);
107+
this.newItems.shift();
108+
109+
this.checkChanges(assert);
110+
});
111+
112+
QUnit.test('reorder + update', function(assert) {
113+
const [itemToMove] = this.newItems.splice(3, 1);
114+
this.newItems.splice(2, 0, itemToMove);
115+
this.newItems[0].a = 'Item 0 updated';
116+
117+
this.checkChanges(assert);
118+
});
119+
120+
QUnit.test('should return undefined when reordering if detectReorders=false', function(assert) {
121+
const isItemEquals = (item1, item2) => JSON.stringify(item1) === JSON.stringify(item2);
122+
const findChangesWithoutReorders = () => findChanges({
123+
oldItems: this.oldItems,
124+
newItems: this.newItems,
125+
getKey: item => item.id,
126+
isItemEquals,
127+
detectReorders: false,
128+
});
129+
130+
this.oldItems = createItems(5);
131+
this.newItems = [...this.oldItems].reverse();
132+
133+
const result = findChangesWithoutReorders();
68134

69-
assert.equal(changes[0].type, 'update');
70-
assert.equal(changes[0].data.id, 0);
71-
assert.equal(changes[1].type, 'remove');
72-
assert.equal(changes[1].key, 1);
135+
assert.strictEqual(result, undefined);
73136
});
74137
});

packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/editingUITests.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3114,6 +3114,69 @@ QUnit.module('reordering decorator', {
31143114
assert.ok(updateSpy.lastCall.calledAfter(onItemRenderedSpy.lastCall), 'update is called after items change');
31153115
assert.deepEqual(onItemRenderedSpy.callCount, 4, 'rendered item count');
31163116
});
3117+
3118+
const ITEMS_ARRAY_LENGTH = 4;
3119+
QUnit.module('reorder rerenders', {
3120+
beforeEach: function() {
3121+
this.itemRenderedSpy = sinon.spy();
3122+
3123+
const items = Array.from({ length: ITEMS_ARRAY_LENGTH }, (_, i) => ({ id: i, text: `Item ${i}` }));
3124+
3125+
this.createList = () => {
3126+
return $('#list').dxList({
3127+
dataSource: items,
3128+
repaintChangesOnly: true,
3129+
itemTemplate: (data) => {
3130+
this.itemRenderedSpy();
3131+
return data.text;
3132+
},
3133+
itemDragging: {
3134+
allowReordering: true,
3135+
data: items,
3136+
onReorder({ fromIndex, toIndex, fromData, component }) {
3137+
const [item] = fromData.splice(fromIndex, 1);
3138+
fromData.splice(toIndex, 0, item);
3139+
component.reload();
3140+
}
3141+
}
3142+
});
3143+
};
3144+
3145+
this.performReorder = (list, fromIndex, toIndex) => {
3146+
const $items = list.find(`.${LIST_ITEM_CLASS}`);
3147+
const pointer = reorderingPointerMock($items.eq(fromIndex), this.clock);
3148+
3149+
const offset = toIndex - fromIndex;
3150+
const adjustment = offset >= 0 ? 0 : -0.1;
3151+
3152+
pointer.dragStart(0.5).drag(offset + adjustment);
3153+
this.clock.tick();
3154+
pointer.dragEnd();
3155+
};
3156+
},
3157+
}, () => {
3158+
for(let from = 0; from < ITEMS_ARRAY_LENGTH; from++) {
3159+
for(let to = 0; to < ITEMS_ARRAY_LENGTH; to++) {
3160+
if(from === to) continue;
3161+
3162+
QUnit.test(`reorder from ${from} to ${to} should rerender only affected items if repaintChangesOnly=true`, function(assert) {
3163+
const list = this.createList();
3164+
3165+
this.itemRenderedSpy.resetHistory();
3166+
3167+
this.performReorder(list, from, to);
3168+
3169+
const affectedItemsCount = Math.abs(to - from) + 1;
3170+
3171+
assert.strictEqual(
3172+
this.itemRenderedSpy.callCount,
3173+
affectedItemsCount,
3174+
`reordered from ${from} to ${to} - expected ${affectedItemsCount} rerenders`
3175+
);
3176+
});
3177+
}
3178+
}
3179+
});
31173180
});
31183181

31193182
if(QUnit.urlParams['nojquery'] && QUnit.urlParams['shadowDom']) {

0 commit comments

Comments
 (0)