diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example06.scss b/examples/vite-demo-vanilla-bundle/src/examples/example06.scss
index 41479f919..5a5b00cb0 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example06.scss
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example06.scss
@@ -2,6 +2,16 @@
@import '@slickgrid-universal/common/dist/styles/sass/sass-utilities.scss';
.grid6 {
+ .avg-total {
+ color: #ac76ff;
+ }
+ .bold {
+ font-weight: bold;
+ }
+ .total-suffix {
+ margin-left: 10px;
+ }
+
.mdi-file-pdf-outline {
/** 1. use `filter` color */
// filter: invert(62%) sepia(93%) saturate(5654%) hue-rotate(325deg) brightness(100%) contrast(90%);
diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example06.ts b/examples/vite-demo-vanilla-bundle/src/examples/example06.ts
index c9a217649..e2c9266fc 100644
--- a/examples/vite-demo-vanilla-bundle/src/examples/example06.ts
+++ b/examples/vite-demo-vanilla-bundle/src/examples/example06.ts
@@ -1,14 +1,20 @@
import {
+ Aggregators,
Column,
- GridOption,
+ decimalFormatted,
FieldType,
Filters,
findItemInTreeStructure,
Formatter,
Formatters,
+ GridOption,
+ isNumber,
SlickDataView,
+ // GroupTotalFormatters,
+ // italicFormatter,
} from '@slickgrid-universal/common';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
+import { TextExportService } from '@slickgrid-universal/text-export';
import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
import './example06.scss';
@@ -60,7 +66,50 @@ export default class Example6 {
type: FieldType.number, exportWithFormatter: true,
excelExportOptions: { autoDetectCellFormat: false },
filterable: true, filter: { model: Filters.compoundInputNumber },
- formatter: (_row, _cell, value) => isNaN(value) ? '' : `${value} MB`,
+
+ // Formatter option #1 (treeParseTotalFormatters)
+ // if you wish to use any of the GroupTotalFormatters (or even regular Formatters), we can do so with the code below
+ // use `treeTotalsFormatter` or `groupTotalsFormatter` to show totals in a Tree Data grid
+ // provide any regular formatters inside the params.formatters
+ // IMPORTANT: DO NOT USE Formatters.multiple (that will fail), Formatters.treeParseTotals already accepts multiple params.formatters and IT MUST BE the Formatter entry point
+
+ // formatter: Formatters.treeParseTotals,
+ // treeTotalsFormatter: GroupTotalFormatters.sumTotalsBold,
+ // // groupTotalsFormatter: GroupTotalFormatters.sumTotalsBold,
+ // params: {
+ // formatters: [
+ // // providing extra formatters for regular cell dataContext, it will only be used when `__treeTotals` is NOT detected (so when it's not a Tree Data total)
+ // (_row, _cell, value) => isNaN(value) ? '' : `${decimalFormatted(value, 0, 2)} MB`,
+ // italicFormatter,
+ // ],
+ // // we can also supply extra params for Formatters/GroupTotalFormatters like min/max decimals
+ // groupFormatterSuffix: ' MB',
+ // minDecimal: 0,
+ // maxDecimal: 2,
+ // },
+
+ // OR option #2 (custom Formatter)
+ formatter: (_row, _cell, value, column, dataContext) => {
+ // parent items will a "__treeTotals" property (when creating the Tree and running Aggregation, it mutates all items, all extra props starts with "__" prefix)
+ const fieldId = column.field;
+
+ // Tree Totals, if exists, will be found under `__treeTotals` prop
+ if (dataContext?.__treeTotals !== undefined) {
+ const treeLevel = dataContext[this.gridOptions?.treeDataOptions?.levelPropName || '__treeLevel'];
+ const sumVal = dataContext?.__treeTotals?.['sum'][fieldId];
+ const avgVal = dataContext?.__treeTotals?.['avg'][fieldId];
+
+ if (avgVal !== undefined && sumVal !== undefined) {
+ // when found Avg & Sum, we'll display both
+ return isNaN(sumVal) ? '' : `
sum: ${decimalFormatted(sumVal, 0, 2)} MB /
avg: ${decimalFormatted(avgVal, 0, 2)} MB (${treeLevel === 0 ? 'total' : 'sub-total'})`;
+ } else if (sumVal !== undefined) {
+ // or when only Sum is aggregated, then just show Sum
+ return isNaN(sumVal) ? '' : `
sum: ${decimalFormatted(sumVal, 0, 2)} MB (${treeLevel === 0 ? 'total' : 'sub-total'})`;
+ }
+ }
+ // reaching this line means it's a regular dataContext without totals, so regular formatter output will be used
+ return !isNumber(value) ? '' : `${value} MB`;
+ },
},
];
@@ -75,10 +124,15 @@ export default class Example6 {
exportWithFormatter: true,
sanitizeDataExport: true
},
+ enableTextExport: true,
+ textExportOptions: {
+ exportWithFormatter: true,
+ sanitizeDataExport: true
+ },
gridMenu: {
iconCssClass: 'mdi mdi-dots-grid',
},
- registerExternalResources: [new ExcelExportService()],
+ registerExternalResources: [new ExcelExportService(), new TextExportService()],
enableFiltering: true,
enableTreeData: true, // you must enable this flag for the filtering & sorting to work as expected
multiColumnSort: false, // multi-column sorting is not supported with Tree Data, so you need to disable it
@@ -96,7 +150,13 @@ export default class Example6 {
// initialSort: {
// columnId: 'file',
// direction: 'DESC'
- // }
+ // },
+
+ // Aggregators are also supported and must always be an array even when single one is provided
+ // Note: only 5 are currently supported: Avg, Sum, Min, Max and Count
+ // Note 2: also note that Avg Aggregator will automatically give you the "avg", "count" and "sum" so if you need these 3 then simply calling Avg will give you better perf
+ // aggregators: [new Aggregators.Sum('size')]
+ aggregators: [new Aggregators.Avg('size'), new Aggregators.Sum('size') /* , new Aggregators.Min('size'), new Aggregators.Max('size') */]
},
showCustomFooter: true,
@@ -148,32 +208,35 @@ export default class Example6 {
const identifierPropName = dataView.getIdPropertyName() || 'id';
const idx = dataView.getIdxById(dataContext[identifierPropName]) as number;
const prefix = this.getFileIcon(value);
+ const treeLevel = dataContext[treeLevelPropName];
value = value.replace(/&/g, '&').replace(//g, '>');
- const spacer = `
`;
+ const spacer = `
`;
- if (data[idx + 1] && data[idx + 1][treeLevelPropName] > data[idx][treeLevelPropName]) {
- const folderPrefix = `
`;
+ if (data[idx + 1]?.[treeLevelPropName] > data[idx][treeLevelPropName] || data[idx]['__hasChildren']) {
+ const folderPrefix = `
`;
if (dataContext.__collapsed) {
- return `${spacer}
${folderPrefix} ${prefix} ${value}`;
+ return `${spacer}
${folderPrefix} ${prefix} ${value}`;
} else {
- return `${spacer}
${folderPrefix} ${prefix} ${value}`;
+ return `${spacer}
${folderPrefix} ${prefix} ${value}`;
}
} else {
- return `${spacer}
${prefix} ${value}`;
+ return `${spacer}
${prefix} ${value}`;
}
};
getFileIcon(value: string) {
let prefix = '';
if (value.includes('.pdf')) {
- prefix = '
';
+ prefix = '
';
} else if (value.includes('.txt')) {
- prefix = '
';
- } else if (value.includes('.xls')) {
- prefix = '
';
+ prefix = '
';
+ } else if (value.includes('.csv') || value.includes('.xls')) {
+ prefix = '
';
} else if (value.includes('.mp3')) {
- prefix = '
';
+ prefix = '
';
+ } else if (value.includes('.')) {
+ prefix = '
';
}
return prefix;
}
@@ -183,7 +246,7 @@ export default class Example6 {
* After adding the item, it will sort by parent/child recursively
*/
addNewFile() {
- const newId = this.sgb.dataView!.getItemCount() + 100;
+ const newId = this.sgb.dataView!.getItemCount() + 50;
// find first parent object and add the new item as a child
const popItem = findItemInTreeStructure(this.datasetHierarchical, x => x.file === 'pop', 'files');
@@ -193,7 +256,7 @@ export default class Example6 {
id: newId,
file: `pop-${newId}.mp3`,
dateModified: new Date(),
- size: Math.floor(Math.random() * 100) + 50,
+ size: newId + 3,
});
// overwrite hierarchical dataset which will also trigger a grid sort and rendering
@@ -201,9 +264,9 @@ export default class Example6 {
// scroll into the position where the item was added with a delay since it needs to recreate the tree grid
setTimeout(() => {
- const rowIndex = this.sgb.dataView?.getRowById(popItem.id) as number;
+ const rowIndex = this.sgb.dataView?.getRowById(newId) as number;
this.sgb.slickGrid?.scrollRowIntoView(rowIndex + 3);
- }, 10);
+ }, 0);
}
}
@@ -234,12 +297,15 @@ export default class Example6 {
id: 4, file: 'pdf', files: [
{ id: 22, file: 'map2.pdf', dateModified: '2015-07-21T08:22:00.123Z', size: 2.9, },
{ id: 5, file: 'map.pdf', dateModified: '2015-05-21T10:22:00.123Z', size: 3.1, },
- { id: 6, file: 'internet-bill.pdf', dateModified: '2015-05-12T14:50:00.123Z', size: 1.4, },
- { id: 23, file: 'phone-bill.pdf', dateModified: '2015-05-01T07:50:00.123Z', size: 1.4, },
+ { id: 6, file: 'internet-bill.pdf', dateModified: '2015-05-12T14:50:00.123Z', size: 1.3, },
+ { id: 23, file: 'phone-bill.pdf', dateModified: '2015-05-01T07:50:00.123Z', size: 1.5, },
]
},
- { id: 9, file: 'misc', files: [{ id: 10, file: 'todo.txt', dateModified: '2015-02-26T16:50:00.123Z', size: 0.4, }] },
- { id: 7, file: 'xls', files: [{ id: 8, file: 'compilation.xls', description: 'movie compilation', dateModified: '2014-10-02T14:50:00.123Z', size: 2.3, }] },
+ { id: 9, file: 'misc', files: [{ id: 10, file: 'warranties.txt', dateModified: '2015-02-26T16:50:00.123Z', size: 0.4, }] },
+ { id: 7, file: 'xls', files: [{ id: 8, file: 'compilation.xls', dateModified: '2014-10-02T14:50:00.123Z', size: 2.3, }] },
+ { id: 55, file: 'unclassified.csv', dateModified: '2015-04-08T03:44:12.333Z', size: 0.25, },
+ { id: 56, file: 'unresolved.csv', dateModified: '2015-04-03T03:21:12.000Z', size: 0.79, },
+ { id: 57, file: 'zebra.dll', dateModified: '2016-12-08T13:22:12.432', size: 1.22, },
]
},
{
@@ -250,8 +316,9 @@ export default class Example6 {
id: 14, file: 'pop', files: [
{ id: 15, file: 'theme.mp3', description: 'Movie Theme Song', dateModified: '2015-03-01T17:05:00Z', size: 47, },
{ id: 25, file: 'song.mp3', description: 'it is a song...', dateModified: '2016-10-04T06:33:44Z', size: 6.3, }
- ]
+ ],
},
+ { id: 33, file: 'other', files: [] }
]
}]
},
@@ -264,4 +331,23 @@ export default class Example6 {
},
];
}
+
+ /**
+ * for test purposes only, we can dynamically change the loaded Aggregator(s) but we'll have to reload the dataset
+ * also note that it bypasses the grid preset which mean that "pdf" will not be collapsed when called this way
+ */
+ displaySumAggregatorOnly() {
+ this.sgb.slickGrid!.setOptions({
+ treeDataOptions: {
+ columnId: 'file',
+ childrenPropName: 'files',
+ excludeChildrenWhenFilteringTree: this.isExcludingChildWhenFiltering, // defaults to false
+ autoApproveParentItemWhenTreeColumnIsValid: this.isAutoApproveParentItemWhenTreeColumnIsValid,
+ aggregators: [new Aggregators.Sum('size')]
+ }
+ });
+
+ // reset dataset to clear all tree data stat mutations (basically recreate the grid entirely to start from scratch)
+ this.sgb.datasetHierarchical = this.mockDataset();
+ }
}
diff --git a/packages/common/src/aggregators/__tests__/avgAggregator.spec.ts b/packages/common/src/aggregators/__tests__/avgAggregator.spec.ts
index 75ca8e244..228080f04 100644
--- a/packages/common/src/aggregators/__tests__/avgAggregator.spec.ts
+++ b/packages/common/src/aggregators/__tests__/avgAggregator.spec.ts
@@ -2,58 +2,107 @@ import { AvgAggregator } from '../avgAggregator';
describe('avgAggregator', () => {
let aggregator: AvgAggregator;
- let dataset = [];
-
- beforeEach(() => {
- dataset = [
- { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
- { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
- { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
- { id: 3, title: 'Task 3', duration: '87', percentComplete: -2 },
- { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
- ] as any;
- });
+ let dataset: any[] = [];
- it('should return undefined when the field provided does not exist', () => {
- // arrange
- const fieldName = 'invalid';
- const groupTotals = {};
- aggregator = new AvgAggregator(fieldName);
- aggregator.init();
+ describe('Regular Group Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
+ { id: 3, title: 'Task 3', duration: '87', percentComplete: -2 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
+ ];
+ });
- // act
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ it('should return undefined when the field provided does not exist', () => {
+ // arrange
+ const fieldName = 'invalid';
+ const groupTotals = {};
+ aggregator = new AvgAggregator(fieldName);
+ aggregator.init();
- // assert
- expect(groupTotals['avg'][fieldName]).toBe(undefined);
- });
+ // act
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ // assert
+ expect(aggregator.isInitialized).toBeTruthy();
+ expect(groupTotals['avg'][fieldName]).toBe(undefined);
+ });
+
+ it('should calculate an average when the chosen field from the dataset contains only numbers', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { avg: {} };
+ aggregator = new AvgAggregator(fieldName);
+ aggregator.init();
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
- it('should calculate an average when the chosen field from the dataset contains only numbers', () => {
- const fieldName = 'percentComplete';
- const groupTotals = { avg: {} };
- aggregator = new AvgAggregator(fieldName);
- aggregator.init();
+ const avg = (55 + 87 + 60 + (-2) + 15) / 5;
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('avg');
+ expect(groupTotals.avg[fieldName]).toBe(avg);
+ });
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ it('should calculate an average with only the valid numbers when dataset contains numbers provided as string and other and invalid char', () => {
+ const fieldName = 'duration';
+ const groupTotals = { avg: {} };
+ aggregator = new AvgAggregator(fieldName);
+ aggregator.init();
- const avg = (55 + 87 + 60 + (-2) + 15) / 5;
- expect(aggregator.field).toBe(fieldName);
- expect(aggregator.type).toBe('avg');
- expect(groupTotals.avg[fieldName]).toBe(avg);
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ const avg = (58 + 14 + 87) / 3;
+ expect(groupTotals.avg[fieldName]).toBe(avg);
+ });
});
- it('should calculate an average with only the valid numbers when dataset contains numbers provided as tring and other and invalid char', () => {
- const fieldName = 'duration';
- const groupTotals = { avg: {} };
- aggregator = new AvgAggregator(fieldName);
- aggregator.init();
+ describe('Tree Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55, __treeLevel: 0 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87, __treeLevel: 1 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60, __treeLevel: 2 },
+ { id: 3, title: 'Task 3', duration: '897', percentComplete: -2, __treeLevel: 0 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15, __treeLevel: 0 },
+ ];
+ });
+
+ it('should return the tree data sum value when accumulating an child item', () => {
+ const fieldName = 'percentComplete';
+ aggregator = new AvgAggregator(fieldName);
+ aggregator.init({}, true);
+
+ // accumulate child to current groupTotals
+ const groupTotals = { avg: { percentComplete: 55 }, sum: { percentComplete: 200 }, count: { percentComplete: 4 } };
+ aggregator.accumulate(dataset[4]);
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('avg');
+ expect(groupTotals.count[fieldName]).toBe(5);
+ expect(groupTotals.sum[fieldName]).toBe(215); // 200 + last item 15 => 215
+ expect(groupTotals.avg[fieldName]).toBe(43); // 215 / 5 => 43
+ });
+
+ it('should return the current sum on the parent item that was accumulated so far', () => {
+ const fieldName = 'percentComplete';
+ aggregator = new AvgAggregator(fieldName);
+ aggregator.init({}, true);
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ // will not accumulate since it's a parent item
+ const groupTotals = { avg: { percentComplete: 55 }, sum: { percentComplete: 200 }, count: { percentComplete: 4 } };
+ aggregator.accumulate(dataset[4], true);
+ aggregator.storeResult(groupTotals);
- const avg = (58 + 14 + 87) / 3;
- expect(groupTotals.avg[fieldName]).toBe(avg);
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('avg');
+ expect(groupTotals.count[fieldName]).toBe(4);
+ expect(groupTotals.sum[fieldName]).toBe(200);
+ expect(groupTotals.avg[fieldName]).toBe(50); // 200 / 4 => 50
+ });
});
});
diff --git a/packages/common/src/aggregators/__tests__/cloneAggregator.spec.ts b/packages/common/src/aggregators/__tests__/cloneAggregator.spec.ts
index c707f12cc..3f5388b8d 100644
--- a/packages/common/src/aggregators/__tests__/cloneAggregator.spec.ts
+++ b/packages/common/src/aggregators/__tests__/cloneAggregator.spec.ts
@@ -2,45 +2,53 @@ import { CloneAggregator } from '../cloneAggregator';
describe('CloneAggregator', () => {
let aggregator: CloneAggregator;
- let dataset = [];
-
- beforeEach(() => {
- dataset = [
- { id: 0, title: 'Product 0', price: 58.5, productGroup: 'Sub-Cat1' },
- { id: 1, title: 'Product 1', price: 14, productGroup: 'Sub-Cat1' },
- { id: 2, title: 'Product 2', price: 2, productGroup: 'Sub-Cat2' },
- { id: 3, title: 'Product 3', price: 87, productGroup: 'Sub-Cat1' },
- { id: 4, title: 'Product 4', price: null, productGroup: 'Sub-Cat2' },
- ] as any;
+ let dataset: any[] = [];
+
+ describe('Regular Group Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Product 0', price: 58.5, productGroup: 'Sub-Cat1' },
+ { id: 1, title: 'Product 1', price: 14, productGroup: 'Sub-Cat1' },
+ { id: 2, title: 'Product 2', price: 2, productGroup: 'Sub-Cat2' },
+ { id: 3, title: 'Product 3', price: 87, productGroup: 'Sub-Cat1' },
+ { id: 4, title: 'Product 4', price: null, productGroup: 'Sub-Cat2' },
+ ];
+ });
+
+ it('should return empty string when the field provided does not exist', () => {
+ // arrange
+ const fieldName = 'invalid';
+ const groupTotals = {};
+ aggregator = new CloneAggregator(fieldName);
+ aggregator.init();
+
+ // act
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ // assert
+ expect(aggregator.isInitialized).toBeTruthy();
+ expect(groupTotals['clone'][fieldName]).toBe('');
+ });
+
+ it('should return last text analyzed by the aggregator when the chosen field is the product group', () => {
+ const fieldName = 'productGroup';
+ const lastGroupName = 'Sub-Cat2';
+ const groupTotals = { clone: {} };
+ aggregator = new CloneAggregator(fieldName);
+ aggregator.init();
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('clone');
+ expect(groupTotals.clone[fieldName]).toBe(lastGroupName);
+ });
});
- it('should return empty string when the field provided does not exist', () => {
- // arrange
- const fieldName = 'invalid';
- const groupTotals = {};
- aggregator = new CloneAggregator(fieldName);
- aggregator.init();
-
- // act
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
-
- // assert
- expect(groupTotals['clone'][fieldName]).toBe('');
- });
-
- it('should return last text analyzed by the aggregator when the chosen field is the product group', () => {
- const fieldName = 'productGroup';
- const lastGroupName = 'Sub-Cat2';
- const groupTotals = { clone: {} };
- aggregator = new CloneAggregator(fieldName);
- aggregator.init();
-
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
-
- expect(aggregator.field).toBe(fieldName);
- expect(aggregator.type).toBe('clone');
- expect(groupTotals.clone[fieldName]).toBe(lastGroupName);
+ describe('Tree Aggregator', () => {
+ aggregator = new CloneAggregator('title');
+ expect(() => aggregator.init({}, true)).toThrow('[Slickgrid-Universal] CloneAggregator is not currently supported for use with Tree Data');
});
});
diff --git a/packages/common/src/aggregators/__tests__/countAggregator.spec.ts b/packages/common/src/aggregators/__tests__/countAggregator.spec.ts
index 86311953a..d66b8ce0c 100644
--- a/packages/common/src/aggregators/__tests__/countAggregator.spec.ts
+++ b/packages/common/src/aggregators/__tests__/countAggregator.spec.ts
@@ -2,51 +2,96 @@ import { CountAggregator } from '../countAggregator';
describe('CountAggregator', () => {
let aggregator: CountAggregator;
- let dataset = [];
-
- beforeEach(() => {
- dataset = [
- { id: 0, title: 'Product 0', price: 58.5, productGroup: 'Sub-Cat1' },
- { id: 1, title: 'Product 1', price: 14, productGroup: 'Sub-Cat1' },
- { id: 2, title: 'Product 2', price: 2, productGroup: 'Sub-Cat2' },
- { id: 3, title: 'Product 3', price: 87, productGroup: 'Sub-Cat1' },
- { id: 4, title: 'Product 4', price: null, productGroup: 'Sub-Cat2' },
- ] as any;
- });
+ let dataset: any[] = [];
+
+ describe('Regular Group Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Product 0', price: 58.5, productGroup: 'Sub-Cat1' },
+ { id: 1, title: 'Product 1', price: 14, productGroup: 'Sub-Cat1' },
+ { id: 2, title: 'Product 2', price: 2, productGroup: 'Sub-Cat2' },
+ { id: 3, title: 'Product 3', price: 87, productGroup: 'Sub-Cat1' },
+ { id: 4, title: 'Product 4', price: null, productGroup: 'Sub-Cat2' },
+ ];
+ });
+
+ it('should return a length of 1 when the dataset found 1 item', () => {
+ // arrange
+ const fieldName = 'title';
+ const groupTotals = {
+ group: {
+ rows: dataset.filter((item) => item['title'] === 'Product 1')
+ }
+ };
+ aggregator = new CountAggregator(fieldName);
+ aggregator.init();
+
+ // act
+ aggregator.storeResult(groupTotals);
+
+ // assert
+ expect(aggregator.isInitialized).toBeTruthy();
+ expect(groupTotals['count'][fieldName]).toBe(1);
+ });
+
+ it('should return a count of the full dataset length when the group has all the same data', () => {
+ const fieldName = 'productGroup';
+ const groupTotals = {
+ count: {},
+ group: {
+ rows: dataset
+ }
+ };
+ aggregator = new CountAggregator(fieldName);
+ aggregator.init();
+
+ aggregator.storeResult(groupTotals);
- it('should return a length of 1 when the dataset found 1 item', () => {
- // arrange
- const fieldName = 'title';
- const groupTotals = {
- group: {
- rows: dataset.filter((item) => item['title'] === 'Product 1')
- }
- };
- aggregator = new CountAggregator(fieldName);
- aggregator.init();
-
- // act
- aggregator.storeResult(groupTotals);
-
- // assert
- expect(groupTotals['count'][fieldName]).toBe(1);
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('count');
+ expect(groupTotals.count[fieldName]).toBe(5);
+ });
});
- it('should return a count of the full dataset length when the group has all the same data', () => {
- const fieldName = 'productGroup';
- const groupTotals = {
- count: {},
- group: {
- rows: dataset
- }
- };
- aggregator = new CountAggregator(fieldName);
- aggregator.init();
-
- aggregator.storeResult(groupTotals);
-
- expect(aggregator.field).toBe(fieldName);
- expect(aggregator.type).toBe('count');
- expect(groupTotals.count[fieldName]).toBe(5);
+ describe('Tree Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55, __treeLevel: 0 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87, __treeLevel: 1 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60, __treeLevel: 2 },
+ { id: 3, title: 'Task 3', duration: '897', percentComplete: -2, __treeLevel: 0 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15, __treeLevel: 0 },
+ ];
+ });
+
+ it('should return the tree data count value when accumulating an child item', () => {
+ const fieldName = 'percentComplete';
+ aggregator = new CountAggregator(fieldName);
+ aggregator.init({}, true);
+
+ // accumulate child to current groupTotals
+ const groupTotals = { count: { percentComplete: 4 } };
+ aggregator.accumulate(dataset[4]);
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('count');
+ expect(groupTotals.count[fieldName]).toBe(5);
+ });
+
+ it('should return the current count on the parent item that was accumulated so far', () => {
+ const fieldName = 'percentComplete';
+ aggregator = new CountAggregator(fieldName);
+ aggregator.init({}, true);
+
+ // will not accumulate since it's a parent item
+ const groupTotals = { count: { percentComplete: 4 } };
+ aggregator.accumulate(dataset[4], true);
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('count');
+ expect(groupTotals.count[fieldName]).toBe(4);
+ });
});
});
diff --git a/packages/common/src/aggregators/__tests__/distinctAggregator.spec.ts b/packages/common/src/aggregators/__tests__/distinctAggregator.spec.ts
index 95a07eaf9..24d9b4025 100644
--- a/packages/common/src/aggregators/__tests__/distinctAggregator.spec.ts
+++ b/packages/common/src/aggregators/__tests__/distinctAggregator.spec.ts
@@ -2,58 +2,66 @@ import { DistinctAggregator } from '../distinctAggregator';
describe('disctinctAggregator', () => {
let aggregator: DistinctAggregator;
- let dataset = [];
-
- beforeEach(() => {
- dataset = [
- { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
- { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
- { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
- { id: 3, title: 'Task 3', duration: '58', percentComplete: 87 },
- { id: 4, title: 'Task 4', duration: null, percentComplete: 55 },
- { id: 4, title: 'Task 5', duration: 32, percentComplete: 52 },
- { id: 4, title: 'Task 6', duration: 58, percentComplete: 52 },
- ] as any;
- });
+ let dataset: any[] = [];
- it('should return empty array when the field provided does not exist', () => {
- // arrange
- const fieldName = 'invalid';
- const groupTotals = {};
- aggregator = new DistinctAggregator(fieldName);
- aggregator.init();
+ describe('Regular Group Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
+ { id: 3, title: 'Task 3', duration: '58', percentComplete: 87 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 55 },
+ { id: 4, title: 'Task 5', duration: 32, percentComplete: 52 },
+ { id: 4, title: 'Task 6', duration: 58, percentComplete: 52 },
+ ];
+ });
- // act
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ it('should return empty array when the field provided does not exist', () => {
+ // arrange
+ const fieldName = 'invalid';
+ const groupTotals = {};
+ aggregator = new DistinctAggregator(fieldName);
+ aggregator.init();
- // assert
- expect(groupTotals['distinct'][fieldName]).toEqual([]);
- });
+ // act
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
- it('should return the distinct number values when provided field property values are all numbers', () => {
- const fieldName = 'percentComplete';
- const groupTotals = { distinct: {} };
- aggregator = new DistinctAggregator(fieldName);
- aggregator.init();
+ // assert
+ expect(aggregator.isInitialized).toBeTruthy();
+ expect(groupTotals['distinct'][fieldName]).toEqual([]);
+ });
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ it('should return the distinct number values when provided field property values are all numbers', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { distinct: {} };
+ aggregator = new DistinctAggregator(fieldName);
+ aggregator.init();
- expect(aggregator.field).toBe(fieldName);
- expect(aggregator.type).toBe('distinct');
- expect(groupTotals.distinct[fieldName]).toEqual([55, 87, 60, 52]);
- });
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
- it('should return the distinct mixed values when provided field property values are all mixed types', () => {
- const fieldName = 'duration';
- const groupTotals = { distinct: {} };
- aggregator = new DistinctAggregator(fieldName);
- aggregator.init();
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('distinct');
+ expect(groupTotals.distinct[fieldName]).toEqual([55, 87, 60, 52]);
+ });
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ it('should return the distinct mixed values when provided field property values are all mixed types', () => {
+ const fieldName = 'duration';
+ const groupTotals = { distinct: {} };
+ aggregator = new DistinctAggregator(fieldName);
+ aggregator.init();
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ expect(groupTotals.distinct[fieldName]).toEqual(['58', '14', '', null, 32, 58]);
+ });
+ });
- expect(groupTotals.distinct[fieldName]).toEqual(['58', '14', '', null, 32, 58]);
+ describe('Tree Aggregator', () => {
+ aggregator = new DistinctAggregator('title');
+ expect(() => aggregator.init({}, true)).toThrow('[Slickgrid-Universal] CloneAggregator is not currently supported for use with Tree Data');
});
});
diff --git a/packages/common/src/aggregators/__tests__/maxAggregator.spec.ts b/packages/common/src/aggregators/__tests__/maxAggregator.spec.ts
index f9d8f01ce..bd5f91af9 100644
--- a/packages/common/src/aggregators/__tests__/maxAggregator.spec.ts
+++ b/packages/common/src/aggregators/__tests__/maxAggregator.spec.ts
@@ -2,56 +2,115 @@ import { MaxAggregator } from '../maxAggregator';
describe('maxAggregator', () => {
let aggregator: MaxAggregator;
- let dataset = [];
-
- beforeEach(() => {
- dataset = [
- { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
- { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
- { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
- { id: 3, title: 'Task 3', duration: '897', percentComplete: -2 },
- { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
- ] as any;
- });
+ let dataset: any[] = [];
- it('should return null when the field provided does not exist', () => {
- // arrange
- const fieldName = 'invalid';
- const groupTotals = {};
- aggregator = new MaxAggregator(fieldName);
- aggregator.init();
+ describe('Regular Group Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
+ { id: 3, title: 'Task 3', duration: '897', percentComplete: -2 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
+ ];
+ });
- // act
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ it('should return null when the field provided does not exist', () => {
+ // arrange
+ const fieldName = 'invalid';
+ const groupTotals = {};
+ aggregator = new MaxAggregator(fieldName);
+ aggregator.init();
- // assert
- expect(groupTotals['max'][fieldName]).toBe(null);
- });
+ // act
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ // assert
+ expect(groupTotals['max'][fieldName]).toBe(null);
+ expect(aggregator.isInitialized).toBeTruthy();
+ });
+
+ it('should return the maximum value when the chosen field from the dataset contains only numbers', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { max: {} };
+ aggregator = new MaxAggregator(fieldName);
+ aggregator.init();
- it('should return the maximum value when the chosen field from the dataset contains only numbers', () => {
- const fieldName = 'percentComplete';
- const groupTotals = { max: {} };
- aggregator = new MaxAggregator(fieldName);
- aggregator.init();
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('max');
+ expect(groupTotals.max[fieldName]).toBe(87);
+ });
- expect(aggregator.field).toBe(fieldName);
- expect(aggregator.type).toBe('max');
- expect(groupTotals.max[fieldName]).toBe(87);
+ it('should return the maximum valid number when dataset contains numbers provided as string and other and invalid char', () => {
+ const fieldName = 'duration';
+ const groupTotals = { max: {} };
+ aggregator = new MaxAggregator(fieldName);
+ aggregator.init();
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ expect(groupTotals.max[fieldName]).toBe(897);
+ });
});
- it('should return the maximum valid number when dataset contains numbers provided as tring and other and invalid char', () => {
- const fieldName = 'duration';
- const groupTotals = { max: {} };
- aggregator = new MaxAggregator(fieldName);
- aggregator.init();
+ describe('Tree Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55, __treeLevel: 0 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87, __treeLevel: 1 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60, __treeLevel: 2 },
+ { id: 3, title: 'Task 3', duration: '897', percentComplete: -2, __treeLevel: 0 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15, __treeLevel: 0 },
+ ];
+ });
+
+ it('should return the tree data maximum value when the chosen field from the dataset contains only numbers', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { max: {} };
+ aggregator = new MaxAggregator(fieldName);
+ aggregator.init({}, true);
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('max');
+ expect(groupTotals.max[fieldName]).toBe(87);
+ });
+
+ it('should return null when accumulating on a tree parent and no maximum appears yet in the datacontext parent item', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals: any = {};
+ aggregator = new MaxAggregator(fieldName);
+ aggregator.init({}, true);
+
+ dataset.forEach((row) => aggregator.accumulate(row, true));
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('max');
+ expect(groupTotals.max[fieldName]).toBe(null);
+ });
+
+ it('should return 99 which is the maximum value found in the Tree on a datacontext parent item', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { max: { percentComplete: 99 } };
+ aggregator = new MaxAggregator(fieldName);
+ aggregator.init({ __treeTotals: { max: { percentComplete: 22 } } }, true);
+ dataset[1].__treeTotals = { max: { percentComplete: 88 } };
+ dataset[2].__treeTotals = { max: { percentComplete: 77 } };
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ dataset.forEach((row) => aggregator.accumulate(row, true));
+ aggregator.storeResult(groupTotals);
- expect(groupTotals.max[fieldName]).toBe(897);
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('max');
+ expect(groupTotals.max[fieldName]).toBe(99);
+ });
});
});
diff --git a/packages/common/src/aggregators/__tests__/minAggregator.spec.ts b/packages/common/src/aggregators/__tests__/minAggregator.spec.ts
index 9db594dc2..c57428506 100644
--- a/packages/common/src/aggregators/__tests__/minAggregator.spec.ts
+++ b/packages/common/src/aggregators/__tests__/minAggregator.spec.ts
@@ -2,56 +2,115 @@ import { MinAggregator } from '../minAggregator';
describe('minAggregator', () => {
let aggregator: MinAggregator;
- let dataset = [];
-
- beforeEach(() => {
- dataset = [
- { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
- { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
- { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
- { id: 3, title: 'Task 3', duration: '91', percentComplete: -2 },
- { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
- ] as any;
- });
+ let dataset: any[] = [];
- it('should return null when the field provided does not exist', () => {
- // arrange
- const fieldName = 'invalid';
- const groupTotals = {};
- aggregator = new MinAggregator(fieldName);
- aggregator.init();
+ describe('Regular Group Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
+ { id: 3, title: 'Task 3', duration: '91', percentComplete: -2 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
+ ];
+ });
- // act
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ it('should return null when the field provided does not exist', () => {
+ // arrange
+ const fieldName = 'invalid';
+ const groupTotals = {};
+ aggregator = new MinAggregator(fieldName);
+ aggregator.init();
- // assert
- expect(groupTotals['min'][fieldName]).toBe(null);
- });
+ // act
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ // assert
+ expect(groupTotals['min'][fieldName]).toBe(null);
+ expect(aggregator.isInitialized).toBeTruthy();
+ });
+
+ it('should return the minimum value when the chosen field from the dataset contains only numbers', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { min: {} };
+ aggregator = new MinAggregator(fieldName);
+ aggregator.init();
- it('should return the minimum value when the chosen field from the dataset contains only numbers', () => {
- const fieldName = 'percentComplete';
- const groupTotals = { min: {} };
- aggregator = new MinAggregator(fieldName);
- aggregator.init();
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('min');
+ expect(groupTotals.min[fieldName]).toBe(-2);
+ });
- expect(aggregator.field).toBe(fieldName);
- expect(aggregator.type).toBe('min');
- expect(groupTotals.min[fieldName]).toBe(-2);
+ it('should return the minimum valid number when dataset contains numbers provided as string and other and invalid char', () => {
+ const fieldName = 'duration';
+ const groupTotals = { min: {} };
+ aggregator = new MinAggregator(fieldName);
+ aggregator.init();
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ expect(groupTotals.min[fieldName]).toBe(14);
+ });
});
- it('should return the minimum valid number when dataset contains numbers provided as tring and other and invalid char', () => {
- const fieldName = 'duration';
- const groupTotals = { min: {} };
- aggregator = new MinAggregator(fieldName);
- aggregator.init();
+ describe('Tree Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55, __treeLevel: 0 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87, __treeLevel: 1 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60, __treeLevel: 2 },
+ { id: 3, title: 'Task 3', duration: '91', percentComplete: -2, __treeLevel: 0 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15, __treeLevel: 0 },
+ ];
+ });
+
+ it('should return the tree data maximum value when the chosen field from the dataset contains only numbers', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { min: {} };
+ aggregator = new MinAggregator(fieldName);
+ aggregator.init({}, true);
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('min');
+ expect(groupTotals.min[fieldName]).toBe(-2);
+ });
+
+ it('should return null when accumulating on a tree parent and no minimum appears yet in the datacontext parent item', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals: any = {};
+ aggregator = new MinAggregator(fieldName);
+ aggregator.init({}, true);
+
+ dataset.forEach((row) => aggregator.accumulate(row, true));
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('min');
+ expect(groupTotals.min[fieldName]).toBe(null);
+ });
+
+ it('should return 88 which is the minimum value found in the Tree on a datacontext parent item', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { min: { percentComplete: 55 } };
+ aggregator = new MinAggregator(fieldName);
+ aggregator.init({ __treeTotals: { min: { percentComplete: 22 } } }, true);
+ dataset[1].__treeTotals = { min: { percentComplete: 99 } };
+ dataset[2].__treeTotals = { min: { percentComplete: 88 } };
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ dataset.forEach((row) => aggregator.accumulate(row, true));
+ aggregator.storeResult(groupTotals);
- expect(groupTotals.min[fieldName]).toBe(14);
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('min');
+ expect(groupTotals.min[fieldName]).toBe(55);
+ });
});
});
diff --git a/packages/common/src/aggregators/__tests__/sumAggregator.spec.ts b/packages/common/src/aggregators/__tests__/sumAggregator.spec.ts
index c02c69960..88838b47a 100644
--- a/packages/common/src/aggregators/__tests__/sumAggregator.spec.ts
+++ b/packages/common/src/aggregators/__tests__/sumAggregator.spec.ts
@@ -2,58 +2,105 @@ import { SumAggregator } from '../sumAggregator';
describe('sumAggregator', () => {
let aggregator: SumAggregator;
- let dataset = [];
-
- beforeEach(() => {
- dataset = [
- { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
- { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
- { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
- { id: 3, title: 'Task 3', duration: '87', percentComplete: -2 },
- { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
- ] as any;
- });
+ let dataset: any[] = [];
- it('should return null when the field provided does not exist', () => {
- // arrange
- const fieldName = 'invalid';
- const groupTotals = {};
- aggregator = new SumAggregator(fieldName);
- aggregator.init();
-
- // act
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
-
- // assert
- expect(aggregator.field).toBe(fieldName);
- expect(aggregator.type).toBe('sum');
- expect(groupTotals['sum'][fieldName]).toBe(0);
- });
+ describe('Regular Group Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60 },
+ { id: 3, title: 'Task 3', duration: '87', percentComplete: -2 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15 },
+ ];
+ });
+
+ it('should return null when the field provided does not exist', () => {
+ // arrange
+ const fieldName = 'invalid';
+ const groupTotals = {};
+ aggregator = new SumAggregator(fieldName);
+ aggregator.init();
+
+ // act
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ // assert
+ expect(aggregator.isInitialized).toBeTruthy();
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('sum');
+ expect(groupTotals['sum'][fieldName]).toBe(0);
+ });
+
+ it('should return the sum value when the chosen field from the dataset contains only numbers', () => {
+ const fieldName = 'percentComplete';
+ const groupTotals = { sum: {} };
+ aggregator = new SumAggregator(fieldName);
+ aggregator.init();
- it('should return the sum value when the chosen field from the dataset contains only numbers', () => {
- const fieldName = 'percentComplete';
- const groupTotals = { sum: {} };
- aggregator = new SumAggregator(fieldName);
- aggregator.init();
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ const total = (55 + 87 + 60 + (-2) + 15);
+ expect(groupTotals.sum[fieldName]).toBe(total);
+ });
- const avg = (55 + 87 + 60 + (-2) + 15);
- expect(groupTotals.sum[fieldName]).toBe(avg);
+ it('should return the sum valid number when dataset contains numbers provided as string and other and invalid char', () => {
+ const fieldName = 'duration';
+ const groupTotals = { sum: {} };
+ aggregator = new SumAggregator(fieldName);
+ aggregator.init();
+
+ dataset.forEach((row) => aggregator.accumulate(row));
+ aggregator.storeResult(groupTotals);
+
+ const total = 58 + 14 + 87;
+ expect(groupTotals.sum[fieldName]).toBe(total);
+ });
});
- it('should return the sum valid number when dataset contains numbers provided as tring and other and invalid char', () => {
- const fieldName = 'duration';
- const groupTotals = { sum: {} };
- aggregator = new SumAggregator(fieldName);
- aggregator.init();
+ describe('Tree Aggregator', () => {
+ beforeEach(() => {
+ dataset = [
+ { id: 0, title: 'Task 0', duration: '58', percentComplete: 55, __treeLevel: 0 },
+ { id: 1, title: 'Task 1', duration: '14', percentComplete: 87, __treeLevel: 1 },
+ { id: 2, title: 'Task 2', duration: '', percentComplete: 60, __treeLevel: 2 },
+ { id: 3, title: 'Task 3', duration: '897', percentComplete: -2, __treeLevel: 0 },
+ { id: 4, title: 'Task 4', duration: null, percentComplete: 15, __treeLevel: 0 },
+ ];
+ });
+
+ it('should return the tree data sum value when accumulating an child item', () => {
+ const fieldName = 'percentComplete';
+ aggregator = new SumAggregator(fieldName);
+ aggregator.init({}, true);
+
+ // accumulate child to current groupTotals
+ const groupTotals = { sum: { percentComplete: 200 }, count: { percentComplete: 4 } };
+ aggregator.accumulate(dataset[4]);
+ aggregator.storeResult(groupTotals);
+
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('sum');
+ expect(groupTotals.count[fieldName]).toBe(5);
+ expect(groupTotals.sum[fieldName]).toBe(215); // 200 + last item 15 => 215
+ });
+
+ it('should return the current sum on the parent item that was accumulated so far', () => {
+ const fieldName = 'percentComplete';
+ aggregator = new SumAggregator(fieldName);
+ aggregator.init({}, true);
- dataset.forEach((row) => aggregator.accumulate(row));
- aggregator.storeResult(groupTotals);
+ // will not accumulate since it's a parent item
+ const groupTotals = { sum: { percentComplete: 200 }, count: { percentComplete: 4 } };
+ aggregator.accumulate(dataset[4], true);
+ aggregator.storeResult(groupTotals);
- const avg = 58 + 14 + 87;
- expect(groupTotals.sum[fieldName]).toBe(avg);
+ expect(aggregator.field).toBe(fieldName);
+ expect(aggregator.type).toBe('sum');
+ expect(groupTotals.count[fieldName]).toBe(4);
+ expect(groupTotals.sum[fieldName]).toBe(200);
+ });
});
});
diff --git a/packages/common/src/aggregators/avgAggregator.ts b/packages/common/src/aggregators/avgAggregator.ts
index 9bc72f9b4..050744e22 100644
--- a/packages/common/src/aggregators/avgAggregator.ts
+++ b/packages/common/src/aggregators/avgAggregator.ts
@@ -1,6 +1,10 @@
+import { isNumber } from '@slickgrid-universal/utils';
+
import type { Aggregator } from './../interfaces/aggregator.interface';
export class AvgAggregator implements Aggregator {
+ private _isInitialized = false;
+ private _isTreeAggregator = false;
private _nonNullCount = 0;
private _sum = 0;
private _field: number | string;
@@ -14,29 +18,90 @@ export class AvgAggregator implements Aggregator {
return this._field;
}
+ get isInitialized() {
+ return this._isInitialized;
+ }
+
get type(): string {
return this._type;
}
- init(): void {
- this._nonNullCount = 0;
+ init(item?: any, isTreeAggregator = false) {
this._sum = 0;
+ this._nonNullCount = 0;
+ this._isInitialized = true;
+
+ // when dealing with Tree Data structure, we also need to keep sum & itemCount refs
+ // also while calculating Avg Aggregator, we could in theory skip completely SumAggregator because we kept the sum already for calculations
+ this._isTreeAggregator = isTreeAggregator;
+ if (isTreeAggregator) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ if (item.__treeTotals[this._type] === undefined) {
+ item.__treeTotals[this._type] = {};
+ item.__treeTotals.sum = {};
+ item.__treeTotals.count = {};
+ }
+ item.__treeTotals[this._type][this._field] = 0;
+ item.__treeTotals['count'][this._field] = 0;
+ item.__treeTotals['sum'][this._field] = 0;
+ }
}
- accumulate(item: any) {
- const val = (item && item.hasOwnProperty(this._field)) ? item[this._field] : null;
- if (val !== null && val !== '' && !isNaN(val)) {
- this._nonNullCount++;
- this._sum += parseFloat(val);
+ accumulate(item: any, isTreeParent = false) {
+ const val = item?.hasOwnProperty(this._field) ? item[this._field] : null;
+
+ // when dealing with Tree Data structure, we need keep only the new sum (without doing any addition)
+ if (!this._isTreeAggregator) {
+ // not a Tree structure, we'll do a regular summation
+ if (val !== null && val !== '' && !isNaN(val)) {
+ this._nonNullCount++;
+ this._sum += parseFloat(val);
+ }
+ } else {
+ if (isTreeParent) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ this.addGroupTotalPropertiesWhenNotExist(item.__treeTotals);
+ this._sum = parseFloat(item.__treeTotals['sum'][this._field] ?? 0);
+ this._nonNullCount = item.__treeTotals['count'][this._field] ?? 0;
+ } else if (isNumber(val)) {
+ this._sum = parseFloat(val);
+ this._nonNullCount = 1;
+ }
}
}
storeResult(groupTotals: any) {
- if (!groupTotals || groupTotals[this._type] === undefined) {
+ let sum = this._sum;
+ let itemCount = this._nonNullCount;
+ this.addGroupTotalPropertiesWhenNotExist(groupTotals);
+
+ // when dealing with Tree Data, we also need to take the parent's total and add it to the final sum
+ if (this._isTreeAggregator) {
+ sum += groupTotals['sum'][this._field];
+ itemCount += groupTotals['count'][this._field];
+
+ groupTotals['sum'][this._field] = sum;
+ groupTotals['count'][this._field] = itemCount;
+ }
+
+ if (itemCount !== 0) {
+ groupTotals[this._type][this._field] = itemCount === 0 ? sum : sum / itemCount;
+ }
+ }
+
+ protected addGroupTotalPropertiesWhenNotExist(groupTotals: any) {
+ if (groupTotals[this._type] === undefined) {
groupTotals[this._type] = {};
}
- if (this._nonNullCount !== 0) {
- groupTotals[this._type][this._field] = this._sum / this._nonNullCount;
+ if (this._isTreeAggregator && groupTotals['sum'] === undefined) {
+ groupTotals['sum'] = {};
+ }
+ if (this._isTreeAggregator && groupTotals['count'] === undefined) {
+ groupTotals['count'] = {};
}
}
}
diff --git a/packages/common/src/aggregators/cloneAggregator.ts b/packages/common/src/aggregators/cloneAggregator.ts
index d9755f312..a957c1a2f 100644
--- a/packages/common/src/aggregators/cloneAggregator.ts
+++ b/packages/common/src/aggregators/cloneAggregator.ts
@@ -1,6 +1,7 @@
import type { Aggregator } from './../interfaces/aggregator.interface';
export class CloneAggregator implements Aggregator {
+ private _isInitialized = false;
private _field: number | string;
private _data: any;
private _type = 'clone';
@@ -13,12 +14,20 @@ export class CloneAggregator implements Aggregator {
return this._field;
}
+ get isInitialized() {
+ return this._isInitialized;
+ }
+
get type(): string {
return this._type;
}
- init(): void {
+ init(_item?: any, isTreeAggregator = false): void {
this._data = '';
+ this._isInitialized = true;
+ if (isTreeAggregator) {
+ throw new Error('[Slickgrid-Universal] CloneAggregator is not currently supported for use with Tree Data');
+ }
}
accumulate(item: any) {
diff --git a/packages/common/src/aggregators/countAggregator.ts b/packages/common/src/aggregators/countAggregator.ts
index cb9c5b963..ab9a24c85 100644
--- a/packages/common/src/aggregators/countAggregator.ts
+++ b/packages/common/src/aggregators/countAggregator.ts
@@ -1,7 +1,12 @@
+import { isNumber } from '@slickgrid-universal/utils';
+
import type { Aggregator } from './../interfaces/aggregator.interface';
export class CountAggregator implements Aggregator {
+ private _isInitialized = false;
+ private _isTreeAggregator = false;
private _field: number | string;
+ private _count = 0;
private _type = 'count';
constructor(field: number | string) {
@@ -12,17 +17,62 @@ export class CountAggregator implements Aggregator {
return this._field;
}
+ get isInitialized() {
+ return this._isInitialized;
+ }
+
get type(): string {
return this._type;
}
- init(): void {
+ init(item?: any, isTreeAggregator = false) {
+ this._count = 0;
+ this._isInitialized = true;
+ this._isTreeAggregator = isTreeAggregator;
+
+ // when dealing with Tree Data structure, we also need to keep sum & itemCount refs
+ if (isTreeAggregator) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ if (item.__treeTotals[this._type] === undefined) {
+ item.__treeTotals[this._type] = {};
+ }
+ item.__treeTotals[this._type][this._field] = 0;
+ }
+ }
+
+ accumulate(item: any, isTreeParent = false) {
+ const val = item?.hasOwnProperty(this._field) ? item[this._field] : null;
+
+ // when dealing with Tree Data structure, we need keep only the new sum (without doing any addition)
+ if (this._isTreeAggregator) {
+ if (isTreeParent) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ if (item.__treeTotals[this._type] === undefined) {
+ item.__treeTotals[this._type] = {};
+ }
+ this._count = item.__treeTotals[this._type][this._field] ?? 0;
+ } else if (isNumber(val)) {
+ this._count = 1;
+ }
+ }
}
storeResult(groupTotals: any) {
if (!groupTotals || groupTotals[this._type] === undefined) {
groupTotals[this._type] = {};
}
- groupTotals[this._type][this._field] = groupTotals.group.rows.length;
+ let itemCount = this._count;
+
+ if (this._isTreeAggregator) {
+ // when dealing with Tree Data, we also need to take the parent's total and add it to the final count
+ itemCount += groupTotals[this._type][this._field];
+ } else {
+ itemCount = groupTotals.group.rows.length;
+ }
+ groupTotals[this._type][this._field] = itemCount;
}
}
diff --git a/packages/common/src/aggregators/distinctAggregator.ts b/packages/common/src/aggregators/distinctAggregator.ts
index 36e2e8636..1fe77a030 100644
--- a/packages/common/src/aggregators/distinctAggregator.ts
+++ b/packages/common/src/aggregators/distinctAggregator.ts
@@ -1,6 +1,7 @@
import type { Aggregator } from './../interfaces/aggregator.interface';
export class DistinctAggregator implements Aggregator {
+ private _isInitialized = false;
private _field: number | string;
private _distinctValues: any[] = [];
private _type = 'distinct';
@@ -13,12 +14,20 @@ export class DistinctAggregator implements Aggregator {
return this._field;
}
+ get isInitialized() {
+ return this._isInitialized;
+ }
+
get type(): string {
return this._type;
}
- init(): void {
+ init(_item?: any, isTreeAggregator = false): void {
this._distinctValues = [];
+ this._isInitialized = true;
+ if (isTreeAggregator) {
+ throw new Error('[Slickgrid-Universal] CloneAggregator is not currently supported for use with Tree Data');
+ }
}
accumulate(item: any) {
diff --git a/packages/common/src/aggregators/maxAggregator.ts b/packages/common/src/aggregators/maxAggregator.ts
index 0e2a52c72..005855519 100644
--- a/packages/common/src/aggregators/maxAggregator.ts
+++ b/packages/common/src/aggregators/maxAggregator.ts
@@ -1,6 +1,10 @@
+import { isNumber } from '@slickgrid-universal/utils';
+
import type { Aggregator } from './../interfaces/aggregator.interface';
export class MaxAggregator implements Aggregator {
+ private _isInitialized = false;
+ private _isTreeAggregator = false;
private _max: number | null = null;
private _field: number | string;
private _type = 'max';
@@ -13,27 +17,79 @@ export class MaxAggregator implements Aggregator {
return this._field;
}
+ get isInitialized() {
+ return this._isInitialized;
+ }
+
get type(): string {
return this._type;
}
- init(): void {
+ init(item?: any, isTreeAggregator = false) {
this._max = null;
+ this._isInitialized = true;
+
+ // when dealing with Tree Data structure, we also need to clear any parent totals
+ this._isTreeAggregator = isTreeAggregator;
+ if (isTreeAggregator) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ if (item.__treeTotals[this._type] === undefined) {
+ item.__treeTotals[this._type] = {};
+ }
+ item.__treeTotals[this._type][this._field] = null;
+ }
}
- accumulate(item: any) {
- const val = (item && item.hasOwnProperty(this._field)) ? item[this._field] : null;
- if (val !== null && val !== '' && !isNaN(val)) {
- if (this._max === null || val > this._max) {
- this._max = parseFloat(val);
+ accumulate(item: any, isTreeParent = false) {
+ const val = item?.hasOwnProperty(this._field) ? item[this._field] : null;
+
+ // when dealing with Tree Data structure, we need keep only the new max (without doing any addition)
+ if (!this._isTreeAggregator) {
+ // not a Tree structure, we'll do a regular maximation
+ this.keepMaxValueWhenFound(val);
+ } else {
+ if (isTreeParent) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ this.addGroupTotalPropertiesWhenNotExist(item.__treeTotals);
+ const parentMax = item.__treeTotals[this._type][this._field] !== null ? parseFloat(item.__treeTotals[this._type][this._field]) : null;
+ if (parentMax !== null && isNumber(parentMax) && (this._max === null || parentMax > this._max)) {
+ this._max = parentMax;
+ }
+ } else if (isNumber(val)) {
+ this.keepMaxValueWhenFound(val);
}
}
}
storeResult(groupTotals: any) {
- if (!groupTotals || groupTotals[this._type] === undefined) {
+ let max = this._max;
+ this.addGroupTotalPropertiesWhenNotExist(groupTotals);
+
+ // when dealing with Tree Data, we also need to take the parent's total and add it to the final max
+ if (this._isTreeAggregator && max !== null) {
+ const parentMax = groupTotals[this._type][this._field];
+ if (isNumber(parentMax) && parentMax > max) {
+ max = parentMax;
+ }
+ }
+ groupTotals[this._type][this._field] = max;
+ }
+
+ protected addGroupTotalPropertiesWhenNotExist(groupTotals: any) {
+ if (groupTotals[this._type] === undefined) {
groupTotals[this._type] = {};
}
- groupTotals[this._type][this._field] = this._max;
+ }
+
+ protected keepMaxValueWhenFound(val: any) {
+ if (val !== null && val !== '' && !isNaN(val)) {
+ if (this._max === null || val > this._max) {
+ this._max = parseFloat(val);
+ }
+ }
}
}
diff --git a/packages/common/src/aggregators/minAggregator.ts b/packages/common/src/aggregators/minAggregator.ts
index f32f5abf7..2340ef3b3 100644
--- a/packages/common/src/aggregators/minAggregator.ts
+++ b/packages/common/src/aggregators/minAggregator.ts
@@ -1,6 +1,10 @@
+import { isNumber } from '@slickgrid-universal/utils';
+
import type { Aggregator } from './../interfaces/aggregator.interface';
export class MinAggregator implements Aggregator {
+ private _isInitialized = false;
+ private _isTreeAggregator = false;
private _min: number | null = null;
private _field: number | string;
private _type = 'min';
@@ -13,27 +17,79 @@ export class MinAggregator implements Aggregator {
return this._field;
}
+ get isInitialized() {
+ return this._isInitialized;
+ }
+
get type(): string {
return this._type;
}
- init() {
+ init(item?: any, isTreeAggregator = false) {
this._min = null;
+ this._isInitialized = true;
+
+ // when dealing with Tree Data structure, we also need to clear any parent totals
+ this._isTreeAggregator = isTreeAggregator;
+ if (isTreeAggregator) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ if (item.__treeTotals[this._type] === undefined) {
+ item.__treeTotals[this._type] = {};
+ }
+ item.__treeTotals[this._type][this._field] = null;
+ }
}
- accumulate(item: any) {
- const val = (item && item.hasOwnProperty(this._field)) ? item[this._field] : null;
- if (val !== null && val !== '' && !isNaN(val)) {
- if (this._min === null || val < this._min) {
- this._min = parseFloat(val);
+ accumulate(item: any, isTreeParent = false) {
+ const val = item?.hasOwnProperty(this._field) ? item[this._field] : null;
+
+ // when dealing with Tree Data structure, we need keep only the new min (without doing any addition)
+ if (!this._isTreeAggregator) {
+ // not a Tree structure, we'll do a regular minimation
+ this.keepMinValueWhenFound(val);
+ } else {
+ if (isTreeParent) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ this.addGroupTotalPropertiesWhenNotExist(item.__treeTotals);
+ const parentMin = item.__treeTotals[this._type][this._field] !== null ? parseFloat(item.__treeTotals[this._type][this._field]) : null;
+ if (parentMin !== null && isNumber(parentMin) && (this._min === null || parentMin < this._min)) {
+ this._min = parentMin;
+ }
+ } else if (isNumber(val)) {
+ this.keepMinValueWhenFound(val);
}
}
}
storeResult(groupTotals: any) {
- if (!groupTotals || groupTotals[this._type] === undefined) {
+ let min = this._min;
+ this.addGroupTotalPropertiesWhenNotExist(groupTotals);
+
+ // when dealing with Tree Data, we also need to take the parent's total and add it to the final min
+ if (this._isTreeAggregator && min !== null) {
+ const parentMin = groupTotals[this._type][this._field];
+ if (isNumber(parentMin) && parentMin < min) {
+ min = parentMin;
+ }
+ }
+ groupTotals[this._type][this._field] = min;
+ }
+
+ protected addGroupTotalPropertiesWhenNotExist(groupTotals: any) {
+ if (groupTotals[this._type] === undefined) {
groupTotals[this._type] = {};
}
- groupTotals[this._type][this._field] = this._min;
+ }
+
+ protected keepMinValueWhenFound(val: any) {
+ if (val !== null && val !== '' && !isNaN(val)) {
+ if (this._min === null || val < this._min) {
+ this._min = parseFloat(val);
+ }
+ }
}
}
diff --git a/packages/common/src/aggregators/sumAggregator.ts b/packages/common/src/aggregators/sumAggregator.ts
index 5d19111d7..8ab7d65ac 100644
--- a/packages/common/src/aggregators/sumAggregator.ts
+++ b/packages/common/src/aggregators/sumAggregator.ts
@@ -1,7 +1,12 @@
+import { isNumber } from '@slickgrid-universal/utils';
+
import type { Aggregator } from './../interfaces/aggregator.interface';
export class SumAggregator implements Aggregator {
+ private _isInitialized = false;
+ private _isTreeAggregator = false;
private _sum = 0;
+ private _itemCount = 0;
private _field: number | string;
private _type = 'sum';
@@ -13,18 +18,55 @@ export class SumAggregator implements Aggregator {
return this._field;
}
+ get isInitialized() {
+ return this._isInitialized;
+ }
+
get type(): string {
return this._type;
}
- init() {
+ init(item?: any, isTreeAggregator = false) {
+ this._isTreeAggregator = isTreeAggregator;
+ this._isInitialized = true;
this._sum = 0;
+ this._itemCount = 0;
+
+ // when dealing with Tree Data structure, we also need to keep sum & itemCount refs
+ if (isTreeAggregator) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ if (item.__treeTotals[this._type] === undefined) {
+ item.__treeTotals[this._type] = {};
+ item.__treeTotals.count = {};
+ }
+ item.__treeTotals['count'][this._field] = 0;
+ item.__treeTotals[this._type][this._field] = 0;
+ }
}
- accumulate(item: any) {
- const val = (item && item.hasOwnProperty(this._field)) ? item[this._field] : null;
- if (val !== null && val !== '' && !isNaN(val)) {
- this._sum += parseFloat(val);
+ accumulate(item: any, isTreeParent = false) {
+ const val = item?.hasOwnProperty(this._field) ? item[this._field] : null;
+
+ // when dealing with Tree Data structure, we need keep only the new sum (without doing any addition)
+ if (!this._isTreeAggregator) {
+ // not a Tree structure, we'll do a regular summation
+ if (val !== null && val !== '' && !isNaN(val)) {
+ this._sum += parseFloat(val);
+ }
+ } else {
+ if (isTreeParent) {
+ if (!item.__treeTotals) {
+ item.__treeTotals = {};
+ }
+ this.addGroupTotalPropertiesWhenNotExist(item.__treeTotals);
+ this._sum = parseFloat(item.__treeTotals[this._type][this._field] ?? 0);
+ this._itemCount = item.__treeTotals['count'][this._field] ?? 0;
+ } else if (isNumber(val)) {
+ this._sum = parseFloat(val);
+ this._itemCount = 1;
+ }
}
}
@@ -32,6 +74,25 @@ export class SumAggregator implements Aggregator {
if (!groupTotals || groupTotals[this._type] === undefined) {
groupTotals[this._type] = {};
}
- groupTotals[this._type][this._field] = this._sum;
+ this.addGroupTotalPropertiesWhenNotExist(groupTotals);
+ let sum = this._sum;
+ let itemCount = this._itemCount;
+
+ // when dealing with Tree Data, we also need to take the parent's total and add it to the final sum
+ if (this._isTreeAggregator) {
+ sum += groupTotals[this._type][this._field];
+ itemCount += groupTotals['count'][this._field];
+ groupTotals['count'][this._field] = itemCount;
+ }
+ groupTotals[this._type][this._field] = sum;
+ }
+
+ protected addGroupTotalPropertiesWhenNotExist(groupTotals: any) {
+ if (groupTotals[this._type] === undefined) {
+ groupTotals[this._type] = {};
+ }
+ if (this._isTreeAggregator && groupTotals['count'] === undefined) {
+ groupTotals['count'] = {};
+ }
}
}
diff --git a/packages/common/src/formatters/__tests__/treeParseTotalFormatters.spec.ts b/packages/common/src/formatters/__tests__/treeParseTotalFormatters.spec.ts
new file mode 100644
index 000000000..9ab9a0a8c
--- /dev/null
+++ b/packages/common/src/formatters/__tests__/treeParseTotalFormatters.spec.ts
@@ -0,0 +1,65 @@
+import { Column, GridOption, SlickGrid } from '../../interfaces/index';
+import { italicFormatter } from '../italicFormatter';
+import { GroupTotalFormatters } from '../../grouping-formatters';
+import { treeParseTotalsFormatter } from '../treeParseTotalsFormatter';
+import { dollarFormatter } from '../dollarFormatter';
+
+const gridStub = {
+ getData: jest.fn(),
+ getOptions: jest.fn(),
+} as unknown as SlickGrid;
+
+describe('TreeParseTotalFormatters', () => {
+ let mockGridOptions: GridOption;
+ const colFieldName = 'fileSize';
+
+ beforeEach(() => {
+ mockGridOptions = {
+ treeDataOptions: { levelPropName: 'indent' }
+ } as GridOption;
+ jest.spyOn(gridStub, 'getOptions').mockReturnValue(mockGridOptions);
+ });
+
+ it('should return expected output of groupTotalsFormatter when detecting the dataContext has tree children and a "__treeTotals" prop', () => {
+ const cellValue = 2.1;
+ const sumTotal = 12.33;
+ const params = { formatters: [italicFormatter] };
+ const result = treeParseTotalsFormatter(0, 0, cellValue, { field: colFieldName, groupTotalsFormatter: GroupTotalFormatters.sumTotalsBold, params } as Column, { __hasChildren: true, __treeTotals: { sum: { [colFieldName]: sumTotal } } }, gridStub);
+ expect(result).toBe(`
${sumTotal}`);
+ });
+
+ it('should return expected output of treeTotalsFormatter when detecting the dataContext has tree children and a "__treeTotals" prop', () => {
+ const cellValue = 2.1;
+ const sumTotal = 12.33;
+ const params = { formatters: [italicFormatter] };
+ const result = treeParseTotalsFormatter(0, 0, cellValue, { field: colFieldName, treeTotalsFormatter: GroupTotalFormatters.sumTotalsBold, params } as Column, { __hasChildren: true, __treeTotals: { sum: { [colFieldName]: sumTotal } } }, gridStub);
+ expect(result).toBe(`
${sumTotal}`);
+ });
+
+ it('should return expected output of italic formatter when detecting the dataContext does not has tree children, neither a "__treeTotals" prop', () => {
+ const cellValue = 2.1;
+ const params = { formatters: [italicFormatter] };
+ const result = treeParseTotalsFormatter(0, 0, cellValue, { field: colFieldName, treeTotalsFormatter: GroupTotalFormatters.sumTotalsBold, params } as Column, {}, gridStub);
+ expect(result).toBe(`
${cellValue}`);
+ });
+
+ it('should return expected output of when multiple formatters (uppercase & italic) are provided and dataContext does not has tree children, neither a "__treeTotals" prop', () => {
+ const cellValue = 2.1;
+ const params = { formatters: [dollarFormatter, italicFormatter] };
+ const result = treeParseTotalsFormatter(0, 0, cellValue, { field: colFieldName, treeTotalsFormatter: GroupTotalFormatters.sumTotalsBold, params } as Column, {}, gridStub);
+ expect(result).toBe(`
$2.10`);
+ });
+
+ it('should return same value as input when dataContext is not a tree total and params.formatters is not provided', () => {
+ const cellValue = 2.1;
+ const params = {};
+ const result = treeParseTotalsFormatter(0, 0, cellValue, { field: colFieldName, treeTotalsFormatter: GroupTotalFormatters.sumTotalsBold, params } as Column, {}, gridStub);
+ expect(result).toBe(cellValue);
+ });
+
+ it('should throw an error when this formatter is used without groupTotalsFormatter or treeTotalsFormatter', () => {
+ const cellValue = 2.1;
+ expect(() => treeParseTotalsFormatter(1, 1, cellValue, {} as Column, {}, gridStub))
+ .toThrowError('[Slickgrid-Universal] When using Formatters.treeParseTotals, you must provide a total formatter via "groupTotalsFormatter" or "treeTotalsFormatter".');
+ });
+});
diff --git a/packages/common/src/formatters/formatters.index.ts b/packages/common/src/formatters/formatters.index.ts
index d6b37b6bd..429743aca 100644
--- a/packages/common/src/formatters/formatters.index.ts
+++ b/packages/common/src/formatters/formatters.index.ts
@@ -35,6 +35,7 @@ import { progressBarFormatter } from './progressBarFormatter';
import { translateFormatter } from './translateFormatter';
import { treeExportFormatter } from './treeExportFormatter';
import { treeFormatter } from './treeFormatter';
+import { treeParseTotalsFormatter } from './treeParseTotalsFormatter';
import { translateBooleanFormatter } from './translateBooleanFormatter';
import { uppercaseFormatter } from './uppercaseFormatter';
import { yesNoFormatter } from './yesNoFormatter';
@@ -284,6 +285,17 @@ export const Formatters = {
/** Formatter that must be use with a Tree Data column */
tree: treeFormatter,
+ /**
+ * Formatter that can be use to parse Tree Data totals and display totals using GroupTotalFormatters.
+ * This formatter works with both regular `Formatters` or `GroupTotalFormatters`,
+ * it will auto-detect if the current data context has a `__treeTotals` prop,
+ * then it will use the `GroupTotalFormatters`, if not then it will try to use regular `Formatters`.
+ *
+ * This mean that you can provide an array of `Formatters` & `GroupTotalFormatters` and it will use the correct formatter
+ * by detecting if the current data context has a `__treeTotals` prop (`GroupTotalFormatters`) or not (regular `Formatter`)
+ */
+ treeParseTotals: treeParseTotalsFormatter,
+
/** Formatter that must be use with a Tree Data column for Exporting the data */
treeExport: treeExportFormatter,
diff --git a/packages/common/src/formatters/index.ts b/packages/common/src/formatters/index.ts
index 8fae8b29c..164eff73b 100644
--- a/packages/common/src/formatters/index.ts
+++ b/packages/common/src/formatters/index.ts
@@ -34,5 +34,6 @@ export * from './translateFormatter';
export * from './translateBooleanFormatter';
export * from './treeExportFormatter';
export * from './treeFormatter';
+export * from './treeParseTotalsFormatter';
export * from './uppercaseFormatter';
export * from './yesNoFormatter';
diff --git a/packages/common/src/formatters/multipleFormatter.ts b/packages/common/src/formatters/multipleFormatter.ts
index a80b07438..f0990835e 100644
--- a/packages/common/src/formatters/multipleFormatter.ts
+++ b/packages/common/src/formatters/multipleFormatter.ts
@@ -18,7 +18,7 @@ export const multipleFormatter: Formatter = (row, cell, value, columnDef, dataCo
// they are piped and executed in sequences
let currentValue = value;
for (const formatter of formatters) {
- currentValue = formatter(row, cell, currentValue, columnDef, dataContext, grid);
+ currentValue = formatter.call(this, row, cell, currentValue, columnDef, dataContext, grid);
}
return currentValue;
};
diff --git a/packages/common/src/formatters/treeParseTotalsFormatter.ts b/packages/common/src/formatters/treeParseTotalsFormatter.ts
new file mode 100644
index 000000000..a7cb244d2
--- /dev/null
+++ b/packages/common/src/formatters/treeParseTotalsFormatter.ts
@@ -0,0 +1,32 @@
+import { Constants } from '../constants';
+import { Formatter, GridOption, GroupTotalsFormatter } from '../interfaces/index';
+
+export const treeParseTotalsFormatter: Formatter = (row, cell, value, columnDef, dataContext, grid) => {
+ const gridOptions = grid.getOptions() as GridOption;
+ const hasChildrenPropName = gridOptions?.treeDataOptions?.hasChildrenPropName ?? Constants.treeDataProperties.HAS_CHILDREN_PROP;
+ const { groupTotalsFormatter, treeTotalsFormatter, params } = columnDef;
+
+ // make sure that the user provided a total formatter or else it won't work
+ if (!groupTotalsFormatter && !treeTotalsFormatter) {
+ throw new Error('[Slickgrid-Universal] When using Formatters.treeParseTotals, you must provide a total formatter via "groupTotalsFormatter" or "treeTotalsFormatter".');
+ }
+
+ // treeParseTotalsFormatter will auto-detect if it should execute GroupTotalsFormatter or a list of regular Formatters (it has to be either/or, never both at same time)
+ if (dataContext[hasChildrenPropName] && dataContext?.__treeTotals && (groupTotalsFormatter || treeTotalsFormatter)) {
+ const totalFormatter = (treeTotalsFormatter ?? groupTotalsFormatter) as GroupTotalsFormatter;
+ return totalFormatter(dataContext?.__treeTotals, columnDef, grid);
+ } else if (params.formatters) {
+ // loop through all Formatters, the value of 1st formatter will be used by 2nd formatter and so on.
+ // they are piped and executed in sequences
+ let currentValue = value;
+ for (const formatter of params.formatters) {
+ if (!dataContext[hasChildrenPropName] && !dataContext?.__treeTotals) {
+ currentValue = (formatter as Formatter)(row, cell, currentValue, columnDef, dataContext, grid) || value;
+ }
+ }
+ return currentValue;
+ }
+
+ // falling here means dataContext doesn't include any tree totals and user didn't provide any regular formatters
+ return value;
+};
diff --git a/packages/common/src/interfaces/aggregator.interface.ts b/packages/common/src/interfaces/aggregator.interface.ts
index df76e2665..92bc54b4f 100644
--- a/packages/common/src/interfaces/aggregator.interface.ts
+++ b/packages/common/src/interfaces/aggregator.interface.ts
@@ -2,14 +2,17 @@ export interface Aggregator {
/** Column definition field Id of the associated Aggregator */
field: number | string;
+ /** Was the Aggregator already initialized? */
+ isInitialized?: boolean;
+
/** Type of Aggregator (sum, avg, ...) */
type: string;
/** Aggregator initialize method */
- init: () => void;
+ init: (item?: any, isTreeAggregator?: boolean) => void;
/** Method to accumulate the result which will be different for each Aggregator type */
- accumulate?: (item: any) => void;
+ accumulate?: (item: any, isTreeParent?: boolean) => void;
/** Method to store the result into the given group total object provided as argument */
storeResult: (groupTotals: any | undefined) => void;
diff --git a/packages/common/src/interfaces/column.interface.ts b/packages/common/src/interfaces/column.interface.ts
index b05fa0795..79b651c17 100644
--- a/packages/common/src/interfaces/column.interface.ts
+++ b/packages/common/src/interfaces/column.interface.ts
@@ -332,6 +332,12 @@ export interface Column
{
/** Custom Tooltip that can ben shown to the column */
toolTip?: string;
+ /**
+ * @alias `groupTotalsFormatter` Tree Totals Formatter function that can be used to add tree totals in the,
+ * user can provide any `GroupTotalsFormatter` and/or use `groupTotalsFormatter` which will do the same
+ */
+ treeTotalsFormatter?: GroupTotalsFormatter;
+
/** What is the Field Type, this can be used in the Formatters/Editors/Filters/... */
type?: typeof FieldType[keyof typeof FieldType];
diff --git a/packages/common/src/interfaces/treeDataOption.interface.ts b/packages/common/src/interfaces/treeDataOption.interface.ts
index 0f9c8fd0a..adf76eff9 100644
--- a/packages/common/src/interfaces/treeDataOption.interface.ts
+++ b/packages/common/src/interfaces/treeDataOption.interface.ts
@@ -1,5 +1,6 @@
// import { Aggregator } from './aggregator.interface';
import type { SortDirection, SortDirectionString } from '../enums/index';
+import type { Aggregator } from './aggregator.interface';
import type { Formatter } from './formatter.interface';
export interface TreeDataOption {
@@ -26,8 +27,7 @@ export interface TreeDataOption {
excludeChildrenWhenFilteringTree?: boolean;
/** Grouping Aggregators array */
- // NOT YET IMPLEMENTED
- // aggregators?: Aggregator[];
+ aggregators?: Aggregator[];
/** Optionally define the initial sort column and direction */
initialSort?: {
diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts
index 35dfe82d6..168650609 100644
--- a/packages/common/src/services/__tests__/utilities.spec.ts
+++ b/packages/common/src/services/__tests__/utilities.spec.ts
@@ -7,6 +7,7 @@ import { Column, GridOption } from '../../interfaces/index';
import { RxJsResourceStub } from '../../../../../test/rxjsResourceStub';
import {
addTreeLevelByMutation,
+ addTreeLevelAndAggregatorsByMutation,
cancellablePromise,
CancelledException,
castObservableToPromise,
@@ -29,6 +30,7 @@ import {
thousandSeparatorFormatted,
unsubscribeAll,
} from '../utilities';
+import { SumAggregator } from '../../aggregators';
describe('Service/Utilies', () => {
describe('unflattenParentChildArrayToTree method', () => {
@@ -57,6 +59,32 @@ describe('Service/Utilies', () => {
{ id: 18, __treeLevel: 0, parentId: null, file: 'something.txt', dateModified: '2015-03-03', size: 90, },
]);
});
+
+ it('should take a parent/child array with aggregators and return a hierarchical array structure', () => {
+ const input = [
+ { id: 18, size: 90, dateModified: '2015-03-03', file: 'something.txt', parentId: null, },
+ { id: 11, file: 'Music', parentId: null, __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, },
+ { id: 12, file: 'mp3', parentId: 11, __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, },
+ { id: 16, file: 'rock', parentId: 12, __treeTotals: { count: { size: 2, }, sum: { size: 98, }, }, },
+ { id: 17, dateModified: '2015-05-13', file: 'soft.mp3', size: 98, parentId: 16, },
+ { id: 14, file: 'pop', parentId: 12, __treeTotals: { count: { size: 2, }, sum: { size: 85, }, }, },
+ { id: 15, dateModified: '2015-03-01', file: 'theme.mp3', size: 85, parentId: 14, },
+ ];
+
+ const output = unflattenParentChildArrayToTree(input, { aggregators: [new SumAggregator('size')], parentPropName: 'parentId', childrenPropName: 'files' });
+
+ expect(output).toEqual([
+ {
+ id: 11, __treeLevel: 0, __collapsed: false, parentId: null, file: 'Music', __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, files: [{
+ id: 12, __treeLevel: 1, __collapsed: false, parentId: 11, file: 'mp3', __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, files: [
+ { id: 14, __treeLevel: 2, __collapsed: false, parentId: 12, file: 'pop', __treeTotals: { count: { size: 1, }, sum: { size: 85, }, }, files: [{ id: 15, __treeLevel: 3, parentId: 14, file: 'theme.mp3', dateModified: '2015-03-01', size: 85, }] },
+ { id: 16, __treeLevel: 2, __collapsed: false, parentId: 12, file: 'rock', __treeTotals: { count: { size: 1, }, sum: { size: 98, }, }, files: [{ id: 17, __treeLevel: 3, parentId: 16, file: 'soft.mp3', dateModified: '2015-05-13', size: 98, }] },
+ ]
+ }]
+ },
+ { id: 18, __treeLevel: 0, parentId: null, file: 'something.txt', dateModified: '2015-03-03', size: 90, },
+ ]);
+ });
});
describe('cancellablePromise method', () => {
@@ -151,6 +179,36 @@ describe('Service/Utilies', () => {
{ id: 15, __treeLevel: 3, dateModified: '2015-03-01', file: 'theme.mp3', size: 85, __parentId: 14, __hasChildren: false },
]);
});
+
+ it('should return a flat array from a hierarchical structure and tree totals aggregation', () => {
+ const aggregator = new SumAggregator('size');
+ addTreeLevelAndAggregatorsByMutation(mockTreeArray, { aggregator, childrenPropName: 'files', levelPropName: '__treeLevel' });
+ const output = flattenToParentChildArray(mockTreeArray, { childrenPropName: 'files' });
+ expect(output).toEqual([
+ { id: 18, size: 90, __treeLevel: 0, dateModified: '2015-03-03', file: 'something.txt', __parentId: null, __hasChildren: false },
+ { id: 11, __treeLevel: 0, file: 'Music', __parentId: null, __hasChildren: true, __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, },
+ { id: 12, __treeLevel: 1, file: 'mp3', __parentId: 11, __hasChildren: true, __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, },
+ { id: 16, __treeLevel: 2, file: 'rock', __parentId: 12, __hasChildren: true, __treeTotals: { count: { size: 1, }, sum: { size: 98, }, }, },
+ { id: 17, __treeLevel: 3, dateModified: '2015-05-13', file: 'soft.mp3', size: 98, __parentId: 16, __hasChildren: false },
+ { id: 14, __treeLevel: 2, file: 'pop', __parentId: 12, __hasChildren: true, __treeTotals: { count: { size: 1, }, sum: { size: 85, }, } },
+ { id: 15, __treeLevel: 3, dateModified: '2015-03-01', file: 'theme.mp3', size: 85, __parentId: 14, __hasChildren: false },
+ ]);
+ });
+
+ it('should return a flat array from a hierarchical structure and tree totals aggregation and tree level number as well', () => {
+ const aggregator = new SumAggregator('size');
+ addTreeLevelAndAggregatorsByMutation(mockTreeArray, { aggregator, childrenPropName: 'files', levelPropName: '__treeLevel' });
+ const output = flattenToParentChildArray(mockTreeArray, { childrenPropName: 'files', shouldAddTreeLevelNumber: true, aggregators: [aggregator] });
+ expect(output).toEqual([
+ { id: 18, size: 90, __treeLevel: 0, dateModified: '2015-03-03', file: 'something.txt', __parentId: null, __hasChildren: false },
+ { id: 11, __treeLevel: 0, file: 'Music', __parentId: null, __hasChildren: true, __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, },
+ { id: 12, __treeLevel: 1, file: 'mp3', __parentId: 11, __hasChildren: true, __treeTotals: { count: { size: 2, }, sum: { size: (98 + 85), }, }, },
+ { id: 16, __treeLevel: 2, file: 'rock', __parentId: 12, __hasChildren: true, __treeTotals: { count: { size: 1, }, sum: { size: 98, }, }, },
+ { id: 17, __treeLevel: 3, dateModified: '2015-05-13', file: 'soft.mp3', size: 98, __parentId: 16, __hasChildren: false },
+ { id: 14, __treeLevel: 2, file: 'pop', __parentId: 12, __hasChildren: true, __treeTotals: { count: { size: 1, }, sum: { size: 85, }, } },
+ { id: 15, __treeLevel: 3, dateModified: '2015-03-01', file: 'theme.mp3', size: 85, __parentId: 14, __hasChildren: false },
+ ]);
+ });
});
describe('findItemInTreeStructure method', () => {
diff --git a/packages/common/src/services/filter.service.ts b/packages/common/src/services/filter.service.ts
index 32cba288f..63aa740f6 100644
--- a/packages/common/src/services/filter.service.ts
+++ b/packages/common/src/services/filter.service.ts
@@ -858,7 +858,7 @@ export class FilterService {
* you can change the sorting icons separately by passing an array of columnId/sortAsc and that will change ONLY the icons
* @param sortColumns
*/
- setSortColumnIcons(sortColumns: { columnId: string, sortAsc: boolean }[]) {
+ setSortColumnIcons(sortColumns: { columnId: string, sortAsc: boolean; }[]) {
if (this._grid && Array.isArray(sortColumns)) {
this._grid.setSortColumns(sortColumns);
}
@@ -1056,7 +1056,7 @@ export class FilterService {
// -------------------
/** Add all created filters (from their template) to the header row section area */
- protected addFilterTemplateToHeaderRow(args: { column: Column; grid: SlickGrid; node: HTMLElement }, isFilterFirstRender = true) {
+ protected addFilterTemplateToHeaderRow(args: { column: Column; grid: SlickGrid; node: HTMLElement; }, isFilterFirstRender = true) {
const columnDef = args.column;
const columnId = columnDef?.id ?? '';
diff --git a/packages/common/src/services/grid.service.ts b/packages/common/src/services/grid.service.ts
index 96a7ff510..4d4931980 100644
--- a/packages/common/src/services/grid.service.ts
+++ b/packages/common/src/services/grid.service.ts
@@ -908,6 +908,7 @@ export class GridService {
this.sharedService.hierarchicalDataset = sortedDatasetResult.hierarchical;
this.filterService.refreshTreeDataFilters(items);
this._dataView.setItems(sortedDatasetResult.flat);
+ this._grid.invalidate();
}
}
diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts
index e46b0041a..e6b8a0343 100644
--- a/packages/common/src/services/utilities.ts
+++ b/packages/common/src/services/utilities.ts
@@ -5,7 +5,7 @@ const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "m
import { Constants } from '../constants';
import { FieldType, type OperatorString, OperatorType } from '../enums/index';
-import type { CancellablePromiseWrapper, Column, GridOption, } from '../interfaces/index';
+import type { Aggregator, CancellablePromiseWrapper, Column, GridOption, } from '../interfaces/index';
import type { Observable, RxJsFacade, Subject, Subscription } from './rxjsFacade';
/** Cancelled Extension that can be only be thrown by the `cancellablePromise()` function */
@@ -72,9 +72,9 @@ export function addTreeLevelByMutation(treeArray: T[], options: { childrenPro
if (Array.isArray(treeArray)) {
for (const item of treeArray) {
if (item) {
- if (Array.isArray(item[childrenPropName]) && (item[childrenPropName] as unknown as Array).length > 0) {
+ if (Array.isArray(item[childrenPropName]) && (item[childrenPropName] as Array).length > 0) {
treeLevel++;
- addTreeLevelByMutation(item[childrenPropName] as unknown as Array, options, treeLevel);
+ addTreeLevelByMutation(item[childrenPropName] as Array, options, treeLevel);
treeLevel--;
}
(item as any)[options.levelPropName] = treeLevel;
@@ -83,13 +83,39 @@ export function addTreeLevelByMutation(treeArray: T[], options: { childrenPro
}
}
+export function addTreeLevelAndAggregatorsByMutation(treeArray: T[], options: { aggregator: Aggregator; childrenPropName: string; levelPropName: string; }, treeLevel = 0, parent: T = null as any) {
+ const childrenPropName = (options?.childrenPropName ?? Constants.treeDataProperties.CHILDREN_PROP) as keyof T;
+ const { aggregator } = options;
+
+ if (Array.isArray(treeArray)) {
+ for (const item of treeArray) {
+ if (item) {
+ const isParent = Array.isArray(item[childrenPropName]);
+
+ if (Array.isArray(item[childrenPropName]) && (item[childrenPropName] as Array).length > 0) {
+ aggregator.init(item, true);
+ treeLevel++;
+ addTreeLevelAndAggregatorsByMutation(item[childrenPropName] as Array, options, treeLevel, item);
+ treeLevel--;
+ }
+
+ if (parent && aggregator.isInitialized && typeof aggregator.accumulate === 'function') {
+ aggregator.accumulate(item, isParent);
+ aggregator.storeResult((parent as any).__treeTotals);
+ }
+ (item as any)[options.levelPropName] = treeLevel;
+ }
+ }
+ }
+}
+
/**
* Convert a hierarchical (tree) array (with children) into a flat array structure array (where the children are pushed as next indexed item in the array)
* @param {Array