@open-dcp/grid 0.0.0-stage → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/main.yml +42 -0
- package/CLA.md +22 -0
- package/LICENSE +190 -0
- package/NOTICE.md +31 -0
- package/README.md +39 -3
- package/SECURITY.md +27 -0
- package/dist/GridService.d.ts +83 -0
- package/dist/GridService.js +410 -0
- package/dist/__tests__/GridServices.test.d.ts +1 -0
- package/dist/__tests__/GridServices.test.js +288 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/services/FilteringService.d.ts +34 -0
- package/dist/services/FilteringService.js +278 -0
- package/dist/services/FormattingService.d.ts +17 -0
- package/dist/services/FormattingService.js +179 -0
- package/dist/services/RowHeightService.d.ts +15 -0
- package/dist/services/RowHeightService.js +63 -0
- package/dist/services/SelectionService.d.ts +14 -0
- package/dist/services/SelectionService.js +55 -0
- package/dist/services/SortingService.d.ts +15 -0
- package/dist/services/SortingService.js +82 -0
- package/dist/services/__tests__/FilteringService.test.d.ts +1 -0
- package/dist/services/__tests__/FilteringService.test.js +140 -0
- package/dist/services/__tests__/FormattingService.test.d.ts +1 -0
- package/dist/services/__tests__/FormattingService.test.js +122 -0
- package/dist/types/grid.d.ts +71 -0
- package/dist/types/grid.js +1 -0
- package/dist/types/sorting.d.ts +8 -0
- package/dist/types/sorting.js +1 -0
- package/jest.config.js +16 -0
- package/package.json +43 -4
- package/reports/junit.xml +3 -0
- package/src/GridService.ts +561 -0
- package/src/__tests__/GridServices.test.ts +348 -0
- package/src/index.ts +17 -0
- package/src/services/FilteringService.ts +347 -0
- package/src/services/FormattingService.ts +205 -0
- package/src/services/RowHeightService.ts +81 -0
- package/src/services/SelectionService.ts +71 -0
- package/src/services/SortingService.ts +105 -0
- package/src/services/__tests__/FilteringService.test.ts +173 -0
- package/src/services/__tests__/FormattingService.test.ts +157 -0
- package/src/types/grid.ts +84 -0
- package/src/types/sorting.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { format } from 'date-fns';
|
|
2
|
+
import { toZonedTime } from 'date-fns-tz';
|
|
3
|
+
import { logger } from "@open-dcp/utilities";
|
|
4
|
+
function formatInTimeZone(timestamp, timeZone, formatString) {
|
|
5
|
+
const date = typeof timestamp === "number" ? new Date(timestamp) : timestamp;
|
|
6
|
+
const zonedDate = toZonedTime(date, timeZone);
|
|
7
|
+
return format(zonedDate, formatString);
|
|
8
|
+
}
|
|
9
|
+
export class FormattingService {
|
|
10
|
+
constructor(gridService) {
|
|
11
|
+
this.gridService = gridService;
|
|
12
|
+
}
|
|
13
|
+
applyFormattingToRow(row) {
|
|
14
|
+
const columns = this.gridService.getColumns();
|
|
15
|
+
const formattedRow = Object.assign({}, row); // Clone the row to keep it immutable
|
|
16
|
+
if (columns && columns.length > 0) {
|
|
17
|
+
// Columns are ready, proceed with formatting
|
|
18
|
+
columns.forEach((column) => {
|
|
19
|
+
var _a;
|
|
20
|
+
const cellData = (_a = row[column.key]) !== null && _a !== void 0 ? _a : null;
|
|
21
|
+
const rawValue = cellData && typeof cellData === 'object' && 'raw' in cellData
|
|
22
|
+
? cellData.raw
|
|
23
|
+
: cellData;
|
|
24
|
+
const value = this.formatCellValue(row, column);
|
|
25
|
+
// Store raw and value
|
|
26
|
+
formattedRow[column.key] = {
|
|
27
|
+
raw: rawValue, // Ensure raw value stays the same
|
|
28
|
+
value: value, // Store transformed value for sorting/filtering
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
Object.keys(row).forEach(key => {
|
|
34
|
+
formattedRow[key] = {
|
|
35
|
+
raw: row[key], // Use original value as raw
|
|
36
|
+
value: null, // No transformation yet
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
//logger.warn('Columns are not available yet. Row formatting is deferred.');
|
|
40
|
+
}
|
|
41
|
+
return formattedRow;
|
|
42
|
+
}
|
|
43
|
+
formatCustomCell(cellValue, row, column) {
|
|
44
|
+
let displayValue = undefined;
|
|
45
|
+
let renderedElement = undefined;
|
|
46
|
+
if (typeof column.renderCell === 'function') {
|
|
47
|
+
try {
|
|
48
|
+
const functionReturn = column.renderCell(cellValue, row);
|
|
49
|
+
if (functionReturn instanceof HTMLAnchorElement) {
|
|
50
|
+
renderedElement = functionReturn;
|
|
51
|
+
}
|
|
52
|
+
else if (typeof functionReturn == "string") {
|
|
53
|
+
displayValue = functionReturn;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.error(`Error in custom renderCell function for column ${column.key}:`, error);
|
|
58
|
+
displayValue = 'Error';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
logger.warn(`Custom column type requires a renderCell function.`);
|
|
63
|
+
displayValue = 'N/A';
|
|
64
|
+
}
|
|
65
|
+
return { display: displayValue, renderedElement };
|
|
66
|
+
}
|
|
67
|
+
formatDateCell(cellValue, column) {
|
|
68
|
+
const date = new Date(cellValue);
|
|
69
|
+
let dateFormatString = this.gridService.displayFormatSubject.getValue().dateFormat;
|
|
70
|
+
if (column.type === 'datetime') {
|
|
71
|
+
dateFormatString += ` ${this.gridService.displayFormatSubject.getValue().timeFormat}`;
|
|
72
|
+
}
|
|
73
|
+
return formatInTimeZone(date, this.gridService.displayFormatSubject.getValue().timeZone, dateFormatString);
|
|
74
|
+
}
|
|
75
|
+
formatCellDisplay(row, column) {
|
|
76
|
+
var _a, _b;
|
|
77
|
+
const cellValue = (_b = (_a = row[column.key]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : null;
|
|
78
|
+
if (cellValue === null) {
|
|
79
|
+
return { display: null, renderedElement: undefined };
|
|
80
|
+
}
|
|
81
|
+
let displayValue = undefined;
|
|
82
|
+
let renderedElement = undefined;
|
|
83
|
+
if (typeof column.transformer === 'function') {
|
|
84
|
+
try {
|
|
85
|
+
const transformed = column.transformer(cellValue);
|
|
86
|
+
displayValue = transformed != null ? transformed.toString() : '';
|
|
87
|
+
return { display: displayValue, renderedElement };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error(`Error in transformer for column ${column.key}:`, error);
|
|
91
|
+
return { display: 'Error', renderedElement };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
switch (column.type) {
|
|
96
|
+
case 'component':
|
|
97
|
+
break;
|
|
98
|
+
case 'custom':
|
|
99
|
+
return this.formatCustomCell(cellValue, row, column);
|
|
100
|
+
case 'integer':
|
|
101
|
+
case 'number':
|
|
102
|
+
displayValue = cellValue.toString();
|
|
103
|
+
break;
|
|
104
|
+
case 'date':
|
|
105
|
+
case 'datetime':
|
|
106
|
+
displayValue = this.formatDateCell(cellValue, column);
|
|
107
|
+
break;
|
|
108
|
+
case 'duration':
|
|
109
|
+
displayValue = `${Math.floor(cellValue / 3600).toString().padStart(2, '0')}:${Math.floor((cellValue % 3600) / 60).toString().padStart(2, '0')}:${(cellValue % 60).toString().padStart(2, '0')}`;
|
|
110
|
+
break;
|
|
111
|
+
case 'string':
|
|
112
|
+
default:
|
|
113
|
+
displayValue = cellValue.toString();
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error(`Error formatting cell for column ${column.key}:`, error);
|
|
119
|
+
displayValue = 'Error';
|
|
120
|
+
}
|
|
121
|
+
return { display: displayValue, renderedElement };
|
|
122
|
+
}
|
|
123
|
+
transformCustomOrComponent(rawValue, column) {
|
|
124
|
+
if (typeof column.transformer === 'function') {
|
|
125
|
+
try {
|
|
126
|
+
return column.transformer(rawValue);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
logger.error(`Error in custom transformer function for column ${column.key}:`, error);
|
|
130
|
+
return 'Error';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return rawValue;
|
|
134
|
+
}
|
|
135
|
+
transformInteger(rawValue) {
|
|
136
|
+
return rawValue !== null ? parseInt(rawValue, 10) : null;
|
|
137
|
+
}
|
|
138
|
+
transformNumber(rawValue) {
|
|
139
|
+
return rawValue !== null ? parseFloat(rawValue) : null;
|
|
140
|
+
}
|
|
141
|
+
transformDate(rawValue) {
|
|
142
|
+
if (rawValue !== null) {
|
|
143
|
+
const date = new Date(rawValue);
|
|
144
|
+
return isNaN(date.getTime()) ? null : date.getTime();
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
transformDuration(rawValue) {
|
|
149
|
+
return rawValue !== null ? parseInt(rawValue, 10) : null;
|
|
150
|
+
}
|
|
151
|
+
transformString(rawValue) {
|
|
152
|
+
return rawValue !== null ? rawValue.toString() : null;
|
|
153
|
+
}
|
|
154
|
+
formatCellValue(row, column) {
|
|
155
|
+
var _a;
|
|
156
|
+
// Safely retrieve the raw value
|
|
157
|
+
const cellData = (_a = row[column.key]) !== null && _a !== void 0 ? _a : null;
|
|
158
|
+
const rawValue = cellData && typeof cellData === 'object' && 'raw' in cellData
|
|
159
|
+
? cellData.raw
|
|
160
|
+
: cellData;
|
|
161
|
+
switch (column.type) {
|
|
162
|
+
case 'component':
|
|
163
|
+
case 'custom':
|
|
164
|
+
return this.transformCustomOrComponent(rawValue, column);
|
|
165
|
+
case 'integer':
|
|
166
|
+
return this.transformInteger(rawValue);
|
|
167
|
+
case 'number':
|
|
168
|
+
return this.transformNumber(rawValue);
|
|
169
|
+
case 'date':
|
|
170
|
+
case 'datetime':
|
|
171
|
+
return this.transformDate(rawValue);
|
|
172
|
+
case 'duration':
|
|
173
|
+
return this.transformDuration(rawValue);
|
|
174
|
+
case 'string':
|
|
175
|
+
default:
|
|
176
|
+
return this.transformString(rawValue);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GridService } from "../GridService";
|
|
2
|
+
export declare class RowHeightService {
|
|
3
|
+
private rowHeights;
|
|
4
|
+
private heightCache;
|
|
5
|
+
private readonly gridService;
|
|
6
|
+
constructor(gridService: GridService);
|
|
7
|
+
initRowHeights(numberOfRows: number): void;
|
|
8
|
+
getRowHeight(index: number): number;
|
|
9
|
+
getHeightCache(): number[];
|
|
10
|
+
updateRowHeight(index: number, height: number): void;
|
|
11
|
+
recalculateHeights(): void;
|
|
12
|
+
updateRowHeights(): void;
|
|
13
|
+
findStartIndex(scrollTop: number): number;
|
|
14
|
+
findEndIndex(scrollBottom: number): number;
|
|
15
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export class RowHeightService {
|
|
2
|
+
constructor(gridService) {
|
|
3
|
+
this.rowHeights = new Map();
|
|
4
|
+
this.heightCache = [];
|
|
5
|
+
this.gridService = gridService;
|
|
6
|
+
}
|
|
7
|
+
initRowHeights(numberOfRows) {
|
|
8
|
+
this.rowHeights = new Map(Array.from({ length: numberOfRows }, (_, index) => [index, this.gridService.elementHeight]));
|
|
9
|
+
}
|
|
10
|
+
getRowHeight(index) {
|
|
11
|
+
var _a;
|
|
12
|
+
return (_a = this.rowHeights.get(index)) !== null && _a !== void 0 ? _a : this.gridService.elementHeight;
|
|
13
|
+
}
|
|
14
|
+
getHeightCache() {
|
|
15
|
+
return this.heightCache;
|
|
16
|
+
}
|
|
17
|
+
updateRowHeight(index, height) {
|
|
18
|
+
this.rowHeights.set(index, height);
|
|
19
|
+
this.recalculateHeights();
|
|
20
|
+
}
|
|
21
|
+
recalculateHeights() {
|
|
22
|
+
let filteredRows = this.gridService.getFilterIndexes();
|
|
23
|
+
this.heightCache = [];
|
|
24
|
+
let sum = 0;
|
|
25
|
+
for (let i = 0; i < this.gridService.rowCount(); i++) {
|
|
26
|
+
let rowIndex = filteredRows ? filteredRows[i] : i;
|
|
27
|
+
sum += this.getRowHeight(rowIndex);
|
|
28
|
+
this.heightCache.push(sum);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
updateRowHeights() {
|
|
32
|
+
let container = this.gridService.getContainer();
|
|
33
|
+
if (!container)
|
|
34
|
+
return;
|
|
35
|
+
const rows = container.querySelectorAll('tbody > tr:not(.firstRow):not(.lastRow)'); // Adjust the selector as needed
|
|
36
|
+
let hasUpdated = false;
|
|
37
|
+
rows.forEach((row, index) => {
|
|
38
|
+
const rowHeight = row.getBoundingClientRect().height; // Get the actual row height
|
|
39
|
+
const rowIndex = index + this.gridService.startIndex;
|
|
40
|
+
if (this.getRowHeight(rowIndex) !== rowHeight) {
|
|
41
|
+
this.updateRowHeight(rowIndex, rowHeight);
|
|
42
|
+
hasUpdated = true;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (hasUpdated) {
|
|
46
|
+
this.gridService.updateRenderedRows();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
findStartIndex(scrollTop) {
|
|
50
|
+
let index = 0;
|
|
51
|
+
while (index < this.heightCache.length && this.heightCache[index] < scrollTop) {
|
|
52
|
+
index++;
|
|
53
|
+
}
|
|
54
|
+
return Math.max(0, index);
|
|
55
|
+
}
|
|
56
|
+
findEndIndex(scrollBottom) {
|
|
57
|
+
let index = 0;
|
|
58
|
+
while (index < this.heightCache.length && this.heightCache[index] < scrollBottom) {
|
|
59
|
+
index++;
|
|
60
|
+
}
|
|
61
|
+
return Math.min(index, this.heightCache.length - 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class SelectionService {
|
|
2
|
+
private readonly selectedIndexes$;
|
|
3
|
+
private readonly multiSelect;
|
|
4
|
+
constructor(selections?: Set<number>, multiSelect?: boolean);
|
|
5
|
+
get selected$(): import("rxjs").Observable<Set<number>>;
|
|
6
|
+
selectRow(index: number): void;
|
|
7
|
+
deselectRow(index: number): void;
|
|
8
|
+
toggleRowSelection(index: number): void;
|
|
9
|
+
clearSelection(): void;
|
|
10
|
+
selectAll(indices: number[]): void;
|
|
11
|
+
deselectAll(): void;
|
|
12
|
+
shiftSelect(startIndex: number, endIndex: number): void;
|
|
13
|
+
isSelected(index: number): boolean;
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BehaviorSubject } from 'rxjs';
|
|
2
|
+
export class SelectionService {
|
|
3
|
+
constructor(selections = new Set(), multiSelect = true) {
|
|
4
|
+
this.multiSelect = multiSelect;
|
|
5
|
+
this.selectedIndexes$ = new BehaviorSubject(selections);
|
|
6
|
+
}
|
|
7
|
+
get selected$() {
|
|
8
|
+
return this.selectedIndexes$.asObservable();
|
|
9
|
+
}
|
|
10
|
+
selectRow(index) {
|
|
11
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
12
|
+
selected.add(index);
|
|
13
|
+
this.selectedIndexes$.next(selected);
|
|
14
|
+
}
|
|
15
|
+
deselectRow(index) {
|
|
16
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
17
|
+
selected.delete(index);
|
|
18
|
+
this.selectedIndexes$.next(selected);
|
|
19
|
+
}
|
|
20
|
+
toggleRowSelection(index) {
|
|
21
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
22
|
+
if (selected.has(index)) {
|
|
23
|
+
selected.delete(index);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
if (!this.multiSelect) {
|
|
27
|
+
selected.clear();
|
|
28
|
+
}
|
|
29
|
+
selected.add(index);
|
|
30
|
+
}
|
|
31
|
+
this.selectedIndexes$.next(selected);
|
|
32
|
+
}
|
|
33
|
+
clearSelection() {
|
|
34
|
+
this.selectedIndexes$.next(new Set());
|
|
35
|
+
}
|
|
36
|
+
selectAll(indices) {
|
|
37
|
+
this.selectedIndexes$.next(new Set(indices));
|
|
38
|
+
}
|
|
39
|
+
deselectAll() {
|
|
40
|
+
this.clearSelection();
|
|
41
|
+
}
|
|
42
|
+
shiftSelect(startIndex, endIndex) {
|
|
43
|
+
if (!this.multiSelect)
|
|
44
|
+
return;
|
|
45
|
+
const selected = new Set(this.selectedIndexes$.value);
|
|
46
|
+
const [start, end] = [Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)];
|
|
47
|
+
for (let i = start; i <= end; i++) {
|
|
48
|
+
selected.add(i);
|
|
49
|
+
}
|
|
50
|
+
this.selectedIndexes$.next(selected);
|
|
51
|
+
}
|
|
52
|
+
isSelected(index) {
|
|
53
|
+
return this.selectedIndexes$.value.has(index);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GridService } from "../GridService";
|
|
2
|
+
import type { Row } from "../types/grid";
|
|
3
|
+
import { SortingCondition } from '../types/sorting';
|
|
4
|
+
export declare class SortingService {
|
|
5
|
+
private readonly gridService;
|
|
6
|
+
private readonly sortConditions;
|
|
7
|
+
private sortedIndexes;
|
|
8
|
+
constructor(gridService: GridService, sortConditions?: SortingCondition[]);
|
|
9
|
+
getSorts(): SortingCondition[];
|
|
10
|
+
getSortCondition(columnKey: string | number): SortingCondition[];
|
|
11
|
+
toggleSort(columnKey: string | number): void;
|
|
12
|
+
applySorting(rows: Row[]): Row[];
|
|
13
|
+
getSortedIndexes(): number[] | null;
|
|
14
|
+
resetSorting(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export class SortingService {
|
|
2
|
+
constructor(gridService, sortConditions = []) {
|
|
3
|
+
this.sortedIndexes = null;
|
|
4
|
+
this.gridService = gridService;
|
|
5
|
+
this.sortConditions = sortConditions;
|
|
6
|
+
}
|
|
7
|
+
getSorts() {
|
|
8
|
+
return this.sortConditions;
|
|
9
|
+
}
|
|
10
|
+
getSortCondition(columnKey) {
|
|
11
|
+
return this.sortConditions.filter(condition => columnKey === condition.columnKey);
|
|
12
|
+
}
|
|
13
|
+
toggleSort(columnKey) {
|
|
14
|
+
const existingIndex = this.sortConditions.findIndex(condition => condition.columnKey === columnKey);
|
|
15
|
+
if (existingIndex === -1) {
|
|
16
|
+
this.sortConditions.push({ columnKey, direction: 'asc' });
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
switch (this.sortConditions[existingIndex].direction) {
|
|
20
|
+
case 'asc':
|
|
21
|
+
this.sortConditions[existingIndex].direction = 'desc';
|
|
22
|
+
break;
|
|
23
|
+
case 'desc':
|
|
24
|
+
this.sortConditions.splice(existingIndex, 1);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
this.sortedIndexes = null;
|
|
29
|
+
}
|
|
30
|
+
applySorting(rows) {
|
|
31
|
+
var _a;
|
|
32
|
+
if (this.sortConditions.length === 0) {
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
if (this.sortedIndexes &&
|
|
36
|
+
this.sortedIndexes.length === rows.length &&
|
|
37
|
+
this.sortedIndexes.every(index => index >= 0 && index < rows.length)) {
|
|
38
|
+
return this.sortedIndexes.map(index => rows[index]); // Apply cached order
|
|
39
|
+
}
|
|
40
|
+
const indexedRows = rows.map((row, index) => ({ row, index }));
|
|
41
|
+
indexedRows.sort((a, b) => {
|
|
42
|
+
for (const { columnKey, direction } of this.sortConditions) {
|
|
43
|
+
const valueA = a.row[columnKey].value;
|
|
44
|
+
const valueB = b.row[columnKey].value;
|
|
45
|
+
let comparison = 0;
|
|
46
|
+
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
|
47
|
+
comparison = valueA.localeCompare(valueB);
|
|
48
|
+
}
|
|
49
|
+
else if (typeof valueA === 'number' && typeof valueB === 'number') {
|
|
50
|
+
comparison = valueA - valueB;
|
|
51
|
+
}
|
|
52
|
+
else if (valueA instanceof Date && valueB instanceof Date) {
|
|
53
|
+
comparison = valueA.getTime() - valueB.getTime();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
comparison = String(valueA).localeCompare(String(valueB));
|
|
57
|
+
}
|
|
58
|
+
if (direction === 'desc') {
|
|
59
|
+
comparison = -comparison;
|
|
60
|
+
}
|
|
61
|
+
if (comparison !== 0) {
|
|
62
|
+
return comparison;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return 0; // Keep stable sorting
|
|
66
|
+
});
|
|
67
|
+
const filteredIndexes = (_a = this.gridService.getFilterIndexes()) !== null && _a !== void 0 ? _a : this.gridService.getRows().map((_, i) => i);
|
|
68
|
+
;
|
|
69
|
+
this.gridService.clearIndexMap(); // Clear the existing mapping
|
|
70
|
+
indexedRows.forEach((indexedRow, i) => {
|
|
71
|
+
this.gridService.setIndexMappingValue(i, filteredIndexes[indexedRow.index]); // Map the sorted index to the original index
|
|
72
|
+
});
|
|
73
|
+
this.sortedIndexes = indexedRows.map(item => item.index);
|
|
74
|
+
return indexedRows.map(item => item.row); // Return sorted rows
|
|
75
|
+
}
|
|
76
|
+
getSortedIndexes() {
|
|
77
|
+
return this.sortedIndexes;
|
|
78
|
+
}
|
|
79
|
+
resetSorting() {
|
|
80
|
+
this.sortedIndexes = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { GridService } from '../../GridService';
|
|
2
|
+
describe('FilteringService', () => {
|
|
3
|
+
let gridService;
|
|
4
|
+
let filteringService;
|
|
5
|
+
let mockRows;
|
|
6
|
+
let mockColumns;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockColumns = [
|
|
9
|
+
{ key: 'id', label: 'ID', type: 'integer' },
|
|
10
|
+
{ key: 'name', label: 'Name', type: 'string' },
|
|
11
|
+
{ key: 'age', label: 'Age', type: 'number' },
|
|
12
|
+
{ key: 'status', label: 'Status', type: 'string' },
|
|
13
|
+
];
|
|
14
|
+
mockRows = [
|
|
15
|
+
{ id: { raw: 1, value: 1 }, name: { raw: 'Alice', value: 'Alice' }, age: { raw: 30, value: 30 }, status: { raw: 'active', value: 'active' } },
|
|
16
|
+
{ id: { raw: 2, value: 2 }, name: { raw: 'Bob', value: 'Bob' }, age: { raw: 25, value: 25 }, status: { raw: 'inactive', value: 'inactive' } },
|
|
17
|
+
{ id: { raw: 3, value: 3 }, name: { raw: 'Charlie', value: 'Charlie' }, age: { raw: 35, value: 35 }, status: { raw: 'active', value: 'active' } },
|
|
18
|
+
];
|
|
19
|
+
gridService = new GridService(mockRows, mockColumns);
|
|
20
|
+
filteringService = gridService['filteringService'];
|
|
21
|
+
});
|
|
22
|
+
describe('Basic Operations', () => {
|
|
23
|
+
it('should initialize with no filter', () => {
|
|
24
|
+
expect(filteringService.getFilterTree()).toBeNull();
|
|
25
|
+
expect(filteringService.getFilterIndexes()).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
it('should filter by equals condition', () => {
|
|
28
|
+
const filter = {
|
|
29
|
+
type: 'condition',
|
|
30
|
+
columnKey: 'name',
|
|
31
|
+
operator: '=',
|
|
32
|
+
value: 'Alice'
|
|
33
|
+
};
|
|
34
|
+
filteringService.setFilterTree(filter);
|
|
35
|
+
expect(filteringService.getFilterIndexes()).toEqual([0]);
|
|
36
|
+
});
|
|
37
|
+
it('should filter by greater than condition', () => {
|
|
38
|
+
const filter = {
|
|
39
|
+
type: 'condition',
|
|
40
|
+
columnKey: 'age',
|
|
41
|
+
operator: '>',
|
|
42
|
+
value: 30
|
|
43
|
+
};
|
|
44
|
+
filteringService.setFilterTree(filter);
|
|
45
|
+
expect(filteringService.getFilterIndexes()).toEqual([2]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('Group Filters', () => {
|
|
49
|
+
it('should apply AND group filter', () => {
|
|
50
|
+
const filter = {
|
|
51
|
+
type: 'group',
|
|
52
|
+
operator: 'and',
|
|
53
|
+
filters: [
|
|
54
|
+
{ type: 'condition', columnKey: 'status', operator: '=', value: 'active' },
|
|
55
|
+
{ type: 'condition', columnKey: 'age', operator: '>=', value: 30 }
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
filteringService.setFilterTree(filter);
|
|
59
|
+
expect(filteringService.getFilterIndexes()).toEqual([0, 2]);
|
|
60
|
+
});
|
|
61
|
+
it('should apply OR group filter', () => {
|
|
62
|
+
const filter = {
|
|
63
|
+
type: 'group',
|
|
64
|
+
operator: 'or',
|
|
65
|
+
filters: [
|
|
66
|
+
{ type: 'condition', columnKey: 'name', operator: '=', value: 'Alice' },
|
|
67
|
+
{ type: 'condition', columnKey: 'name', operator: '=', value: 'Bob' }
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
filteringService.setFilterTree(filter);
|
|
71
|
+
expect(filteringService.getFilterIndexes()).toEqual([0, 1]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('Add and Reset Filters', () => {
|
|
75
|
+
it('should add filter and replace existing for same column', () => {
|
|
76
|
+
const filter1 = { type: 'condition', columnKey: 'age', operator: '>', value: 25 };
|
|
77
|
+
const filter2 = { type: 'condition', columnKey: 'age', operator: '<', value: 35 };
|
|
78
|
+
filteringService.addFilter(filter1);
|
|
79
|
+
filteringService.addFilter(filter2);
|
|
80
|
+
const tree = filteringService.getFilterTree();
|
|
81
|
+
expect(tree === null || tree === void 0 ? void 0 : tree.type).toBe('group');
|
|
82
|
+
if ((tree === null || tree === void 0 ? void 0 : tree.type) === 'group') {
|
|
83
|
+
expect(tree.filters.length).toBe(1);
|
|
84
|
+
expect(tree.filters[0]).toEqual(filter2);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it('should reset all filters', () => {
|
|
88
|
+
filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
|
|
89
|
+
filteringService.resetFilters();
|
|
90
|
+
expect(filteringService.getFilterTree()).toBeNull();
|
|
91
|
+
expect(filteringService.getFilterIndexes()).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
it('should reset specific column filter', () => {
|
|
94
|
+
const filter = {
|
|
95
|
+
type: 'group',
|
|
96
|
+
operator: 'and',
|
|
97
|
+
filters: [
|
|
98
|
+
{ type: 'condition', columnKey: 'status', operator: '=', value: 'active' },
|
|
99
|
+
{ type: 'condition', columnKey: 'age', operator: '>', value: 25 }
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
filteringService.setFilterTree(filter);
|
|
103
|
+
filteringService.resetFilter('status');
|
|
104
|
+
const tree = filteringService.getFilterTree();
|
|
105
|
+
if ((tree === null || tree === void 0 ? void 0 : tree.type) === 'group') {
|
|
106
|
+
expect(tree.filters.length).toBe(1);
|
|
107
|
+
expect(tree.filters[0].columnKey).toBe('age');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('Helper Methods', () => {
|
|
112
|
+
it('should check if column has filter', () => {
|
|
113
|
+
filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
|
|
114
|
+
expect(filteringService.hasFilter('status')).toBe(true);
|
|
115
|
+
expect(filteringService.hasFilter('age')).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
it('should apply filter to rows', () => {
|
|
118
|
+
filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
|
|
119
|
+
const filtered = filteringService.applyFilter(mockRows);
|
|
120
|
+
expect(filtered.length).toBe(2);
|
|
121
|
+
expect(filtered[0].name.value).toBe('Alice');
|
|
122
|
+
expect(filtered[1].name.value).toBe('Charlie');
|
|
123
|
+
});
|
|
124
|
+
it('should combine global search with filter tree using AND semantics', () => {
|
|
125
|
+
filteringService.setFilterTree({ type: 'condition', columnKey: 'status', operator: '=', value: 'active' });
|
|
126
|
+
filteringService.setGlobalSearch('char', { caseInsensitive: true });
|
|
127
|
+
const filtered = filteringService.applyFilter(mockRows);
|
|
128
|
+
expect(filtered.length).toBe(1);
|
|
129
|
+
expect(filtered[0].name.value).toBe('Charlie');
|
|
130
|
+
});
|
|
131
|
+
it('should use grid case-insensitive option when global search option is not set', () => {
|
|
132
|
+
const caseInsensitiveGridService = new GridService(mockRows, mockColumns, { caseInsensitiveFiltering: true });
|
|
133
|
+
const caseInsensitiveFilteringService = caseInsensitiveGridService['filteringService'];
|
|
134
|
+
caseInsensitiveFilteringService.setGlobalSearch('ALICE');
|
|
135
|
+
const filtered = caseInsensitiveFilteringService.applyFilter(mockRows);
|
|
136
|
+
expect(filtered.length).toBe(1);
|
|
137
|
+
expect(filtered[0].name.value).toBe('Alice');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|