Skip to content

Commit

Permalink
Merge pull request #2954 from IgniteUI/mvenkov/tree-grid-batch-update
Browse files Browse the repository at this point in the history
igxTreeGrid batch update, #2921
  • Loading branch information
bazal4o authored Dec 7, 2018
2 parents 5b82664 + 6a9d607 commit cb75b44
Show file tree
Hide file tree
Showing 25 changed files with 1,424 additions and 318 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable changes for each version of this project will be documented in this
- `displayValuePipe` input property is provided that allows developers to additionally transform the value on blur;
- `focusedValuePipe` input property is provided that allows developers to additionally transform the value on focus;
- `IgxTreeGrid`:
- Batch editing - an injectable transaction provider accumulates pending changes, which are not directly applied to the grid's data source. Those can later be inspected, manipulated and submitted at once. Changes are collected for individual cells or rows, depending on editing mode, and accumulated per data row/record.
- You can now export the tree grid both to CSV and Excel.
- The hierarchy and the records' expanded states would be reflected in the exported Excel worksheet.

Expand Down
6 changes: 2 additions & 4 deletions projects/igniteui-angular/src/lib/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ export function cloneHierarchicalArray(array: any[], childDataKey: any): any[] {
}

for (const item of array) {
const clonedItem = cloneValue(item);
if (Array.isArray(item[childDataKey])) {
const clonedItem = cloneValue(item);
clonedItem[childDataKey] = cloneHierarchicalArray(clonedItem[childDataKey], childDataKey);
result.push(clonedItem);
} else {
result.push(item);
}
result.push(clonedItem);
}
return result;
}
Expand Down
145 changes: 143 additions & 2 deletions projects/igniteui-angular/src/lib/data-operations/data-util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import { FilteringStrategy } from './filtering-strategy';
import { IFilteringExpressionsTree, FilteringExpressionsTree } from './filtering-expressions-tree';
import { IFilteringState } from './filtering-state.interface';
import { FilteringLogic } from './filtering-expression.interface';
import { IgxNumberFilteringOperand,
import {
IgxNumberFilteringOperand,
IgxStringFilteringOperand,
IgxDateFilteringOperand,
IgxBooleanFilteringOperand } from './filtering-condition';
IgxBooleanFilteringOperand
} from './filtering-condition';
import { IPagingState, PagingError } from './paging-state.interface';
import { SampleTestData } from '../test-utils/sample-test-data.spec';
import { Transaction, TransactionType, HierarchicalTransaction } from '../services';

/* Test sorting */
function testSort() {
Expand Down Expand Up @@ -266,6 +270,7 @@ function testGroupBy() {
});
}
/* //Test sorting */

/* Test filtering */
class CustomFilteringStrategy extends FilteringStrategy {
public filter<T>(data: T[], expressionsTree: IFilteringExpressionsTree): T[] {
Expand Down Expand Up @@ -387,6 +392,7 @@ function testFilter() {
});
}
/* //Test filtering */

/* Test paging */
function testPage() {
const dataGenerator: DataGenerator = new DataGenerator();
Expand Down Expand Up @@ -426,9 +432,144 @@ function testPage() {
});
}
/* //Test paging */

/* Test merging */
function testMerging() {
describe('Test merging', () => {
it('Should merge add transactions correctly', () => {
const data = SampleTestData.personIDNameData();
const addRow4 = { ID: 4, Name: 'Peter' };
const addRow5 = { ID: 5, Name: 'Mimi' };
const addRow6 = { ID: 6, Name: 'Pedro' };
const transactions: Transaction[] = [
{ id: addRow4.ID, newValue: addRow4, type: TransactionType.ADD },
{ id: addRow5.ID, newValue: addRow5, type: TransactionType.ADD },
{ id: addRow6.ID, newValue: addRow6, type: TransactionType.ADD },
];

DataUtil.mergeTransactions(data, transactions, 'ID');
expect(data.length).toBe(6);
expect(data[3]).toBe(addRow4);
expect(data[4]).toBe(addRow5);
expect(data[5]).toBe(addRow6);
});

it('Should merge update transactions correctly', () => {
const data = SampleTestData.personIDNameData();
const transactions: Transaction[] = [
{ id: 1, newValue: { Name: 'Peter' }, type: TransactionType.UPDATE },
{ id: 3, newValue: { Name: 'Mimi' }, type: TransactionType.UPDATE },
];

DataUtil.mergeTransactions(data, transactions, 'ID');
expect(data.length).toBe(3);
expect(data[0].Name).toBe('Peter');
expect(data[2].Name).toBe('Mimi');
});

it('Should merge delete transactions correctly', () => {
const data = SampleTestData.personIDNameData();
const secondRow = data[1];
const transactions: Transaction[] = [
{ id: 1, newValue: null, type: TransactionType.DELETE },
{ id: 3, newValue: null, type: TransactionType.DELETE },
];

DataUtil.mergeTransactions(data, transactions, 'ID', true);
expect(data.length).toBe(1);
expect(data[0]).toEqual(secondRow);
});

it('Should merge add hierarchical transactions correctly', () => {
const data = SampleTestData.employeeSmallTreeData();
const addRootRow = { ID: 1000, Name: 'Pit Peter', HireDate: new Date(2008, 3, 20), Age: 55 };
const addChildRow1 = { ID: 1001, Name: 'Marry May', HireDate: new Date(2018, 4, 1), Age: 102 };
const addChildRow2 = { ID: 1002, Name: 'April Alison', HireDate: new Date(2021, 5, 10), Age: 4 };
const transactions: HierarchicalTransaction[] = [
{ id: addRootRow.ID, newValue: addRootRow, type: TransactionType.ADD, path: [] },
{ id: addChildRow1.ID, newValue: addChildRow1, type: TransactionType.ADD, path: [data[0].ID, data[0].Employees[1].ID] },
{ id: addChildRow2.ID, newValue: addChildRow2, type: TransactionType.ADD, path: [addRootRow.ID] },
];

DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', false);
expect(data.length).toBe(4);

expect(data[3].Age).toBe(addRootRow.Age);
expect(data[3].Employees.length).toBe(1);
expect(data[3].HireDate).toBe(addRootRow.HireDate);
expect(data[3].ID).toBe(addRootRow.ID);
expect(data[3].Name).toBe(addRootRow.Name);

expect((data[0].Employees[1] as any).Employees.length).toBe(1);
expect((data[0].Employees[1] as any).Employees[0]).toBe(addChildRow1);

expect(data[3].Employees[0]).toBe(addChildRow2);
});

it('Should merge update hierarchical transactions correctly', () => {
const data = SampleTestData.employeeSmallTreeData();
const updateRootRow = { Name: 'May Peter', Age: 13 };
const updateChildRow1 = { HireDate: new Date(2100, 1, 12), Age: 1300 };
const updateChildRow2 = { HireDate: new Date(2100, 1, 12), Name: 'Santa Claus' };

const transactions: HierarchicalTransaction[] = [
{
id: data[1].ID,
newValue: updateRootRow,
type: TransactionType.UPDATE,
path: []
},
{
id: data[2].Employees[0].ID,
newValue: updateChildRow1,
type: TransactionType.UPDATE,
path: [data[2].ID]
},
{
id: (data[0].Employees[2] as any).Employees[0].ID,
newValue: updateChildRow2,
type: TransactionType.UPDATE,
path: [data[0].ID, data[0].Employees[2].ID]
},
];

DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', false);
expect(data[1].Name).toBe(updateRootRow.Name);
expect(data[1].Age).toBe(updateRootRow.Age);

expect(data[2].Employees[0].HireDate.getTime()).toBe(updateChildRow1.HireDate.getTime());
expect(data[2].Employees[0].Age).toBe(updateChildRow1.Age);

expect((data[0].Employees[2] as any).Employees[0].Name).toBe(updateChildRow2.Name);
expect((data[0].Employees[2] as any).Employees[0].HireDate.getTime()).toBe(updateChildRow2.HireDate.getTime());
});

it('Should merge delete hierarchical transactions correctly', () => {
const data = SampleTestData.employeeSmallTreeData();
const transactions: HierarchicalTransaction[] = [
// root row with no children
{ id: data[1].ID, newValue: null, type: TransactionType.DELETE, path: [] },
// root row with children
{ id: data[2].ID, newValue: null, type: TransactionType.DELETE, path: [] },
// child row with no children
{ id: data[0].Employees[0].ID, newValue: null, type: TransactionType.DELETE, path: [data[0].ID] },
// child row with children
{ id: data[0].Employees[2].ID, newValue: null, type: TransactionType.DELETE, path: [data[0].ID] }
];

DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', true);

expect(data.length).toBe(1);
expect(data[0].Employees.length).toBe(1);
});
});
}
/* //Test merging */

describe('DataUtil', () => {
testSort();
testGroupBy();
testFilter();
testPage();
testMerging();
});
111 changes: 75 additions & 36 deletions projects/igniteui-angular/src/lib/data-operations/data-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ export enum DataType {
* @hidden
*/
export class DataUtil {
public static sort<T>(data: T[], expressions: ISortingExpression [], sorting: IgxSorting = new IgxSorting()): T[] {
public static sort<T>(data: T[], expressions: ISortingExpression[], sorting: IgxSorting = new IgxSorting()): T[] {
return sorting.sort(data, expressions);
}

public static treeGridSort(hierarchicalData: ITreeGridRecord[],
expressions: ISortingExpression [],
parent?: ITreeGridRecord): ITreeGridRecord[] {
expressions: ISortingExpression[],
parent?: ITreeGridRecord): ITreeGridRecord[] {
let res: ITreeGridRecord[] = [];
hierarchicalData.forEach((hr: ITreeGridRecord) => {
const rec: ITreeGridRecord = DataUtil.cloneTreeGridRecord(hr);
Expand All @@ -58,8 +58,7 @@ export class DataUtil {
children: hierarchicalRecord.children,
isFilteredOutParent: hierarchicalRecord.isFilteredOutParent,
level: hierarchicalRecord.level,
expanded: hierarchicalRecord.expanded,
path: [...hierarchicalRecord.path]
expanded: hierarchicalRecord.expanded
};
return rec;
}
Expand Down Expand Up @@ -189,60 +188,100 @@ export class DataUtil {
* @param data Collection to merge
* @param transactions Transactions to merge into data
* @param primaryKey Primary key of the collection, if any
* @param deleteRows Should delete rows with DELETE transaction type from data
* @returns Provided data collections updated with all provided transactions
*/
public static mergeTransactions<T>(data: T[], transactions: Transaction[], primaryKey?: any): T[] {
public static mergeTransactions<T>(data: T[], transactions: Transaction[], primaryKey?: any, deleteRows: boolean = false): T[] {
data.forEach((item: any, index: number) => {
const rowId = primaryKey ? item[primaryKey] : item;
const transaction = transactions.find(t => t.id === rowId);
if (Array.isArray(item.children)) {
this.mergeTransactions(item.children, transactions, primaryKey);
}
if (transaction && transaction.type === TransactionType.UPDATE) {
data[index] = transaction.newValue;
}
});

if (deleteRows) {
transactions
.filter(t => t.type === TransactionType.DELETE)
.forEach(t => {
const index = primaryKey ? data.findIndex(d => d[primaryKey] === t.id) : data.findIndex(d => d === t.id);
if (0 <= index && index < data.length) {
data.splice(index, 1);
}
});
}

data.push(...transactions
.filter(t => t.type === TransactionType.ADD)
.map(t => t.newValue));

return data;
}

// TODO: optimize addition of added rows. Should not filter transaction in each recursion!!!
/** @experimental @hidden */
/**
* Merges all changes from provided transactions into provided hierarchical data collection
* @param data Collection to merge
* @param transactions Transactions to merge into data
* @param childDataKey Data key of child collections
* @param primaryKey Primary key of the collection, if any
* @param deleteRows Should delete rows with DELETE transaction type from data
* @returns Provided data collections updated with all provided transactions
*/
public static mergeHierarchicalTransactions(
data: any[],
transactions: HierarchicalTransaction[],
childDataKey: any,
primaryKey?: any,
parentKey?: any): any[] {

for (let index = 0; index < data.length; index++) {
const dataItem = data[index];
const rowId = primaryKey ? dataItem[primaryKey] : dataItem;
const updateTransaction = transactions.filter(t => t.type === TransactionType.UPDATE).find(t => t.id === rowId);
const addedTransactions = transactions.filter(t => t.type === TransactionType.ADD).filter(t => t.parentId === rowId);
if (updateTransaction || addedTransactions.length > 0) {
data[index] = mergeObjects(cloneValue(dataItem), updateTransaction && updateTransaction.newValue);
}
if (addedTransactions.length > 0) {
if (!data[index][childDataKey]) {
data[index][childDataKey] = [];
}
for (const addedTransaction of addedTransactions) {
data[index][childDataKey].push(addedTransaction.newValue);
deleteRows: boolean = false): any[] {

for (const transaction of transactions) {
if (transaction.path) {
const parent = this.findParentFromPath(data, primaryKey, childDataKey, transaction.path);
let collection: any[] = parent ? parent[childDataKey] : data;
switch (transaction.type) {
case TransactionType.ADD:
// if there is no parent this is ADD row at root level
if (parent && !parent[childDataKey]) {
parent[childDataKey] = collection = [];
}
collection.push(transaction.newValue);
break;
case TransactionType.UPDATE:
const updateIndex = collection.findIndex(x => x[primaryKey] === transaction.id);
if (updateIndex !== -1) {
collection[updateIndex] = mergeObjects(cloneValue(collection[updateIndex]), transaction.newValue);
}
break;
case TransactionType.DELETE:
if (deleteRows) {
const deleteIndex = collection.findIndex(r => r[primaryKey] === transaction.id);
if (deleteIndex !== -1) {
collection.splice(deleteIndex, 1);
}
}
break;
}
}
if (data[index][childDataKey]) {
data[index][childDataKey] = this.mergeHierarchicalTransactions(
data[index][childDataKey],
transactions,
childDataKey,
primaryKey,
rowId
);
} else {
// if there is no path this is ADD row in root. Push the newValue to data
data.push(transaction.newValue);
}
}
return data;
}

private static findParentFromPath(data: any[], primaryKey: any, childDataKey: any, path: any[]): any {
let collection: any[] = data;
let result: any;

for (const id of path) {
result = collection && collection.find(x => x[primaryKey] === id);
if (!result) {
break;
}

collection = result[childDataKey];
}

return result;
}
}
Loading

0 comments on commit cb75b44

Please sign in to comment.