@mui/x-data-grid 6.19.10 → 6.20.0
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/CHANGELOG.md +76 -0
- package/hooks/features/clipboard/useGridClipboard.js +8 -5
- package/hooks/features/export/serializers/csvSerializer.d.ts +3 -6
- package/hooks/features/export/serializers/csvSerializer.js +28 -32
- package/hooks/features/export/useGridCsvExport.js +9 -6
- package/hooks/features/virtualization/useGridVirtualScroller.js +27 -28
- package/index.js +1 -1
- package/legacy/hooks/features/clipboard/useGridClipboard.js +8 -5
- package/legacy/hooks/features/export/serializers/csvSerializer.js +28 -32
- package/legacy/hooks/features/export/useGridCsvExport.js +9 -6
- package/legacy/hooks/features/virtualization/useGridVirtualScroller.js +27 -28
- package/legacy/index.js +1 -1
- package/models/gridExport.d.ts +7 -0
- package/modern/hooks/features/clipboard/useGridClipboard.js +8 -5
- package/modern/hooks/features/export/serializers/csvSerializer.js +28 -32
- package/modern/hooks/features/export/useGridCsvExport.js +8 -5
- package/modern/hooks/features/virtualization/useGridVirtualScroller.js +24 -24
- package/modern/index.js +1 -1
- package/node/hooks/features/clipboard/useGridClipboard.js +8 -5
- package/node/hooks/features/export/serializers/csvSerializer.js +28 -32
- package/node/hooks/features/export/useGridCsvExport.js +8 -5
- package/node/hooks/features/virtualization/useGridVirtualScroller.js +24 -24
- package/node/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,82 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## 6.20.0
|
|
7
|
+
|
|
8
|
+
_May 24, 2024_
|
|
9
|
+
|
|
10
|
+
We'd like to offer a big thanks to the 2 contributors who made this release possible. Here are some highlights ✨:
|
|
11
|
+
|
|
12
|
+
- 🐞 Bugfixes
|
|
13
|
+
|
|
14
|
+
### Data Grid
|
|
15
|
+
|
|
16
|
+
#### `@mui/x-data-grid@6.20.0`
|
|
17
|
+
|
|
18
|
+
- [DataGrid] Escape formulas in CSV and Excel export (#13190) @cherniavskii
|
|
19
|
+
|
|
20
|
+
#### `@mui/x-data-grid-pro@6.20.0` [](https://mui.com/r/x-pro-svg-link 'Pro plan')
|
|
21
|
+
|
|
22
|
+
Same changes as in `@mui/x-data-grid@6.20.0`.
|
|
23
|
+
|
|
24
|
+
#### `@mui/x-data-grid-premium@6.20.0` [](https://mui.com/r/x-premium-svg-link 'Premium plan')
|
|
25
|
+
|
|
26
|
+
Same changes as in `@mui/x-data-grid-pro@6.20.0`.
|
|
27
|
+
|
|
28
|
+
### Date Pickers
|
|
29
|
+
|
|
30
|
+
#### `@mui/x-date-pickers@6.20.0`
|
|
31
|
+
|
|
32
|
+
- [pickers] Fix `disableOpenPicker` prop behavior (#13221) @LukasTy
|
|
33
|
+
|
|
34
|
+
#### `@mui/x-date-pickers-pro@6.20.0` [](https://mui.com/r/x-pro-svg-link 'Pro plan')
|
|
35
|
+
|
|
36
|
+
Same changes as in `@mui/x-date-pickers@6.20.0`.
|
|
37
|
+
|
|
38
|
+
## 6.19.12
|
|
39
|
+
|
|
40
|
+
_May 17, 2024_
|
|
41
|
+
|
|
42
|
+
We'd like to offer a big thanks to the 2 contributors who made this release possible. Here are some highlights ✨:
|
|
43
|
+
|
|
44
|
+
- 🐞 Bugfixes
|
|
45
|
+
|
|
46
|
+
### Date Pickers
|
|
47
|
+
|
|
48
|
+
#### `@mui/x-date-pickers@6.19.12`
|
|
49
|
+
|
|
50
|
+
- [pickers] Fix `AdapterMomentJalaali` regression (#13150) @LukasTy
|
|
51
|
+
|
|
52
|
+
#### `@mui/x-date-pickers-pro@6.19.12` [](https://mui.com/r/x-pro-svg-link 'Pro plan')
|
|
53
|
+
|
|
54
|
+
Same changes as in `@mui/x-date-pickers@6.19.12`.
|
|
55
|
+
|
|
56
|
+
### Docs
|
|
57
|
+
|
|
58
|
+
- [docs] Use MUI X v6 in Codesandbox and Stackblitz demos (#12838) @cherniavskii
|
|
59
|
+
|
|
60
|
+
## 6.19.11
|
|
61
|
+
|
|
62
|
+
_Apr 18, 2024_
|
|
63
|
+
|
|
64
|
+
We'd like to offer a big thanks to the 1 contributor who made this release possible. Here are some highlights ✨:
|
|
65
|
+
|
|
66
|
+
- 🐞 Bugfixes
|
|
67
|
+
|
|
68
|
+
### Data Grid
|
|
69
|
+
|
|
70
|
+
#### `@mui/x-data-grid@6.19.11`
|
|
71
|
+
|
|
72
|
+
- [DataGrid] Fix virtualization memory leak (#12812) @romgrk
|
|
73
|
+
|
|
74
|
+
#### `@mui/x-data-grid-pro@6.19.11` [](https://mui.com/r/x-pro-svg-link 'Pro plan')
|
|
75
|
+
|
|
76
|
+
Same changes as in `@mui/x-data-grid@6.19.11`.
|
|
77
|
+
|
|
78
|
+
#### `@mui/x-data-grid-premium@6.19.11` [](https://mui.com/r/x-premium-svg-link 'Premium plan')
|
|
79
|
+
|
|
80
|
+
Same changes as in `@mui/x-data-grid-pro@6.19.11`.
|
|
81
|
+
|
|
6
82
|
## 6.19.10
|
|
7
83
|
|
|
8
84
|
_Apr 12, 2024_
|
|
@@ -67,18 +67,21 @@ export const useGridClipboard = (apiRef, props) => {
|
|
|
67
67
|
if (selectedRows.size > 0) {
|
|
68
68
|
textToCopy = apiRef.current.getDataAsCsv({
|
|
69
69
|
includeHeaders: false,
|
|
70
|
-
// TODO: make it configurable
|
|
71
70
|
delimiter: clipboardCopyCellDelimiter,
|
|
72
|
-
shouldAppendQuotes: false
|
|
71
|
+
shouldAppendQuotes: false,
|
|
72
|
+
escapeFormulas: false
|
|
73
73
|
});
|
|
74
74
|
} else {
|
|
75
75
|
const focusedCell = gridFocusCellSelector(apiRef);
|
|
76
76
|
if (focusedCell) {
|
|
77
77
|
const cellParams = apiRef.current.getCellParams(focusedCell.id, focusedCell.field);
|
|
78
78
|
textToCopy = serializeCellValue(cellParams, {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
csvOptions: {
|
|
80
|
+
delimiter: clipboardCopyCellDelimiter,
|
|
81
|
+
shouldAppendQuotes: false,
|
|
82
|
+
escapeFormulas: false
|
|
83
|
+
},
|
|
84
|
+
ignoreValueFormatter
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
}
|
|
@@ -4,19 +4,16 @@ import type { GridCellParams } from '../../../../models/params/gridCellParams';
|
|
|
4
4
|
import type { GridStateColDef } from '../../../../models/colDef/gridColDef';
|
|
5
5
|
import type { GridApiCommunity } from '../../../../models/api/gridApiCommunity';
|
|
6
6
|
export declare const serializeCellValue: (cellParams: GridCellParams, options: {
|
|
7
|
-
|
|
7
|
+
csvOptions: CSVOptions;
|
|
8
8
|
ignoreValueFormatter: boolean;
|
|
9
|
-
shouldAppendQuotes: boolean;
|
|
10
9
|
}) => any;
|
|
10
|
+
type CSVOptions = Required<Pick<GridCsvExportOptions, 'delimiter' | 'shouldAppendQuotes' | 'escapeFormulas'>>;
|
|
11
11
|
interface BuildCSVOptions {
|
|
12
12
|
columns: GridStateColDef[];
|
|
13
13
|
rowIds: GridRowId[];
|
|
14
|
-
|
|
15
|
-
includeHeaders: NonNullable<GridCsvExportOptions['includeHeaders']>;
|
|
16
|
-
includeColumnGroupsHeaders: NonNullable<GridCsvExportOptions['includeColumnGroupsHeaders']>;
|
|
14
|
+
csvOptions: Required<Pick<GridCsvExportOptions, 'delimiter' | 'includeColumnGroupsHeaders' | 'includeHeaders' | 'shouldAppendQuotes' | 'escapeFormulas'>>;
|
|
17
15
|
ignoreValueFormatter: boolean;
|
|
18
16
|
apiRef: React.MutableRefObject<GridApiCommunity>;
|
|
19
|
-
shouldAppendQuotes: boolean;
|
|
20
17
|
}
|
|
21
18
|
export declare function buildCSV(options: BuildCSVOptions): string;
|
|
22
19
|
export {};
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../../../../colDef';
|
|
2
2
|
import { buildWarning } from '../../../../utils/warning';
|
|
3
|
-
function sanitizeCellValue(value,
|
|
3
|
+
function sanitizeCellValue(value, csvOptions) {
|
|
4
4
|
if (typeof value === 'string') {
|
|
5
|
-
if (shouldAppendQuotes) {
|
|
5
|
+
if (csvOptions.shouldAppendQuotes || csvOptions.escapeFormulas) {
|
|
6
6
|
const escapedValue = value.replace(/"/g, '""');
|
|
7
|
-
// Make sure value containing delimiter or line break won't be split into multiple
|
|
8
|
-
if ([
|
|
7
|
+
// Make sure value containing delimiter or line break won't be split into multiple cells
|
|
8
|
+
if ([csvOptions.delimiter, '\n', '\r', '"'].some(delimiter => value.includes(delimiter))) {
|
|
9
9
|
return `"${escapedValue}"`;
|
|
10
10
|
}
|
|
11
|
+
if (csvOptions.escapeFormulas) {
|
|
12
|
+
// See https://owasp.org/www-community/attacks/CSV_Injection
|
|
13
|
+
if (['=', '+', '-', '@', '\t', '\r'].includes(escapedValue[0])) {
|
|
14
|
+
return `'${escapedValue}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
11
17
|
return escapedValue;
|
|
12
18
|
}
|
|
13
19
|
return value;
|
|
@@ -16,9 +22,8 @@ function sanitizeCellValue(value, delimiterCharacter, shouldAppendQuotes) {
|
|
|
16
22
|
}
|
|
17
23
|
export const serializeCellValue = (cellParams, options) => {
|
|
18
24
|
const {
|
|
19
|
-
|
|
20
|
-
ignoreValueFormatter
|
|
21
|
-
shouldAppendQuotes
|
|
25
|
+
csvOptions,
|
|
26
|
+
ignoreValueFormatter
|
|
22
27
|
} = options;
|
|
23
28
|
let value;
|
|
24
29
|
if (ignoreValueFormatter) {
|
|
@@ -37,7 +42,7 @@ export const serializeCellValue = (cellParams, options) => {
|
|
|
37
42
|
} else {
|
|
38
43
|
value = cellParams.formattedValue;
|
|
39
44
|
}
|
|
40
|
-
return sanitizeCellValue(value,
|
|
45
|
+
return sanitizeCellValue(value, csvOptions);
|
|
41
46
|
};
|
|
42
47
|
const objectFormattedValueWarning = buildWarning(['MUI: When the value of a field is an object or a `renderCell` is provided, the CSV export might not display the value correctly.', 'You can provide a `valueFormatter` with a string representation to be used.']);
|
|
43
48
|
class CSVRow {
|
|
@@ -49,12 +54,12 @@ class CSVRow {
|
|
|
49
54
|
}
|
|
50
55
|
addValue(value) {
|
|
51
56
|
if (!this.isEmpty) {
|
|
52
|
-
this.rowString += this.options.
|
|
57
|
+
this.rowString += this.options.csvOptions.delimiter;
|
|
53
58
|
}
|
|
54
59
|
if (value === null || value === undefined) {
|
|
55
60
|
this.rowString += '';
|
|
56
61
|
} else if (typeof this.options.sanitizeCellValue === 'function') {
|
|
57
|
-
this.rowString += this.options.sanitizeCellValue(value, this.options.
|
|
62
|
+
this.rowString += this.options.sanitizeCellValue(value, this.options.csvOptions);
|
|
58
63
|
} else {
|
|
59
64
|
this.rowString += value;
|
|
60
65
|
}
|
|
@@ -68,13 +73,11 @@ const serializeRow = ({
|
|
|
68
73
|
id,
|
|
69
74
|
columns,
|
|
70
75
|
getCellParams,
|
|
71
|
-
|
|
72
|
-
ignoreValueFormatter
|
|
73
|
-
shouldAppendQuotes
|
|
76
|
+
csvOptions,
|
|
77
|
+
ignoreValueFormatter
|
|
74
78
|
}) => {
|
|
75
79
|
const row = new CSVRow({
|
|
76
|
-
|
|
77
|
-
shouldAppendQuotes
|
|
80
|
+
csvOptions
|
|
78
81
|
});
|
|
79
82
|
columns.forEach(column => {
|
|
80
83
|
const cellParams = getCellParams(id, column.field);
|
|
@@ -84,9 +87,8 @@ const serializeRow = ({
|
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
row.addValue(serializeCellValue(cellParams, {
|
|
87
|
-
delimiterCharacter,
|
|
88
90
|
ignoreValueFormatter,
|
|
89
|
-
|
|
91
|
+
csvOptions
|
|
90
92
|
}));
|
|
91
93
|
});
|
|
92
94
|
return row.getRowString();
|
|
@@ -95,27 +97,23 @@ export function buildCSV(options) {
|
|
|
95
97
|
const {
|
|
96
98
|
columns,
|
|
97
99
|
rowIds,
|
|
98
|
-
|
|
99
|
-
includeHeaders,
|
|
100
|
-
includeColumnGroupsHeaders,
|
|
100
|
+
csvOptions,
|
|
101
101
|
ignoreValueFormatter,
|
|
102
|
-
apiRef
|
|
103
|
-
shouldAppendQuotes
|
|
102
|
+
apiRef
|
|
104
103
|
} = options;
|
|
105
104
|
const CSVBody = rowIds.reduce((acc, id) => `${acc}${serializeRow({
|
|
106
105
|
id,
|
|
107
106
|
columns,
|
|
108
107
|
getCellParams: apiRef.current.getCellParams,
|
|
109
|
-
delimiterCharacter,
|
|
110
108
|
ignoreValueFormatter,
|
|
111
|
-
|
|
109
|
+
csvOptions
|
|
112
110
|
})}\r\n`, '').trim();
|
|
113
|
-
if (!includeHeaders) {
|
|
111
|
+
if (!csvOptions.includeHeaders) {
|
|
114
112
|
return CSVBody;
|
|
115
113
|
}
|
|
116
114
|
const filteredColumns = columns.filter(column => column.field !== GRID_CHECKBOX_SELECTION_COL_DEF.field);
|
|
117
115
|
const headerRows = [];
|
|
118
|
-
if (includeColumnGroupsHeaders) {
|
|
116
|
+
if (csvOptions.includeColumnGroupsHeaders) {
|
|
119
117
|
const columnGroupLookup = apiRef.current.unstable_getAllGroupDetails();
|
|
120
118
|
let maxColumnGroupsDepth = 0;
|
|
121
119
|
const columnGroupPathsLookup = filteredColumns.reduce((acc, column) => {
|
|
@@ -126,9 +124,8 @@ export function buildCSV(options) {
|
|
|
126
124
|
}, {});
|
|
127
125
|
for (let i = 0; i < maxColumnGroupsDepth; i += 1) {
|
|
128
126
|
const headerGroupRow = new CSVRow({
|
|
129
|
-
|
|
130
|
-
sanitizeCellValue
|
|
131
|
-
shouldAppendQuotes
|
|
127
|
+
csvOptions,
|
|
128
|
+
sanitizeCellValue
|
|
132
129
|
});
|
|
133
130
|
headerRows.push(headerGroupRow);
|
|
134
131
|
filteredColumns.forEach(column => {
|
|
@@ -139,9 +136,8 @@ export function buildCSV(options) {
|
|
|
139
136
|
}
|
|
140
137
|
}
|
|
141
138
|
const mainHeaderRow = new CSVRow({
|
|
142
|
-
|
|
143
|
-
sanitizeCellValue
|
|
144
|
-
shouldAppendQuotes
|
|
139
|
+
csvOptions,
|
|
140
|
+
sanitizeCellValue
|
|
145
141
|
});
|
|
146
142
|
filteredColumns.forEach(column => {
|
|
147
143
|
mainHeaderRow.addValue(column.headerName || column.field);
|
|
@@ -19,7 +19,7 @@ export const useGridCsvExport = (apiRef, props) => {
|
|
|
19
19
|
const ignoreValueFormatterProp = props.unstable_ignoreValueFormatterDuringExport;
|
|
20
20
|
const ignoreValueFormatter = (typeof ignoreValueFormatterProp === 'object' ? ignoreValueFormatterProp == null ? void 0 : ignoreValueFormatterProp.csvExport : ignoreValueFormatterProp) || false;
|
|
21
21
|
const getDataAsCsv = React.useCallback((options = {}) => {
|
|
22
|
-
var _options$getRowsToExp, _options$includeHeade, _options$includeColum, _options$
|
|
22
|
+
var _options$getRowsToExp, _options$shouldAppend, _options$includeHeade, _options$includeColum, _options$escapeFormul;
|
|
23
23
|
logger.debug(`Get data as CSV`);
|
|
24
24
|
const exportedColumns = getColumnsToExport({
|
|
25
25
|
apiRef,
|
|
@@ -32,12 +32,15 @@ export const useGridCsvExport = (apiRef, props) => {
|
|
|
32
32
|
return buildCSV({
|
|
33
33
|
columns: exportedColumns,
|
|
34
34
|
rowIds: exportedRowIds,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
csvOptions: {
|
|
36
|
+
delimiter: options.delimiter || ',',
|
|
37
|
+
shouldAppendQuotes: (_options$shouldAppend = options.shouldAppendQuotes) != null ? _options$shouldAppend : true,
|
|
38
|
+
includeHeaders: (_options$includeHeade = options.includeHeaders) != null ? _options$includeHeade : true,
|
|
39
|
+
includeColumnGroupsHeaders: (_options$includeColum = options.includeColumnGroupsHeaders) != null ? _options$includeColum : true,
|
|
40
|
+
escapeFormulas: (_options$escapeFormul = options.escapeFormulas) != null ? _options$escapeFormul : true
|
|
41
|
+
},
|
|
38
42
|
ignoreValueFormatter,
|
|
39
|
-
apiRef
|
|
40
|
-
shouldAppendQuotes: (_options$shouldAppend = options.shouldAppendQuotes) != null ? _options$shouldAppend : true
|
|
43
|
+
apiRef
|
|
41
44
|
});
|
|
42
45
|
}, [logger, apiRef, ignoreValueFormatter]);
|
|
43
46
|
const exportDataAsCsv = React.useCallback(options => {
|
|
@@ -63,7 +63,6 @@ const MEMOIZE_OPTIONS = {
|
|
|
63
63
|
maxSize: 3
|
|
64
64
|
};
|
|
65
65
|
export const useGridVirtualScroller = props => {
|
|
66
|
-
var _currentPage$range3, _currentPage$range4;
|
|
67
66
|
const apiRef = useGridPrivateApiContext();
|
|
68
67
|
const rootProps = useGridRootProps();
|
|
69
68
|
const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
|
|
@@ -134,27 +133,6 @@ export const useGridVirtualScroller = props => {
|
|
|
134
133
|
}
|
|
135
134
|
return -1;
|
|
136
135
|
}, [cellFocus, visibleColumns]);
|
|
137
|
-
const getNearestIndexToRender = React.useCallback(offset => {
|
|
138
|
-
var _currentPage$range, _currentPage$range2;
|
|
139
|
-
const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
|
|
140
|
-
let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
|
|
141
|
-
if ((_currentPage$range = currentPage.range) != null && _currentPage$range.lastRowIndex && !allRowsMeasured) {
|
|
142
|
-
// Check if all rows in this page are already measured
|
|
143
|
-
allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
|
|
144
|
-
}
|
|
145
|
-
const lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (((_currentPage$range2 = currentPage.range) == null ? void 0 : _currentPage$range2.firstRowIndex) || 0), 0, rowsMeta.positions.length);
|
|
146
|
-
if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
|
|
147
|
-
// If all rows were measured (when no row has "auto" as height) or all rows before the offset
|
|
148
|
-
// were measured, then use a binary search because it's faster.
|
|
149
|
-
return binarySearch(offset, rowsMeta.positions);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Otherwise, use an exponential search.
|
|
153
|
-
// If rows have "auto" as height, their positions will be based on estimated heights.
|
|
154
|
-
// In this case, we can skip several steps until we find a position higher than the offset.
|
|
155
|
-
// Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
|
|
156
|
-
return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
|
|
157
|
-
}, [apiRef, (_currentPage$range3 = currentPage.range) == null ? void 0 : _currentPage$range3.firstRowIndex, (_currentPage$range4 = currentPage.range) == null ? void 0 : _currentPage$range4.lastRowIndex, rowsMeta.positions]);
|
|
158
136
|
const computeRenderContext = React.useCallback(() => {
|
|
159
137
|
if (!enabled) {
|
|
160
138
|
return {
|
|
@@ -171,8 +149,8 @@ export const useGridVirtualScroller = props => {
|
|
|
171
149
|
|
|
172
150
|
// Clamp the value because the search may return an index out of bounds.
|
|
173
151
|
// In the last index, this is not needed because Array.slice doesn't include it.
|
|
174
|
-
const firstRowIndex = Math.min(getNearestIndexToRender(top), rowsMeta.positions.length - 1);
|
|
175
|
-
const lastRowIndex = rootProps.autoHeight ? firstRowIndex + currentPage.rows.length : getNearestIndexToRender(top + containerDimensions.height);
|
|
152
|
+
const firstRowIndex = Math.min(getNearestIndexToRender(apiRef, currentPage, rowsMeta, top), rowsMeta.positions.length - 1);
|
|
153
|
+
const lastRowIndex = rootProps.autoHeight ? firstRowIndex + currentPage.rows.length : getNearestIndexToRender(apiRef, currentPage, rowsMeta, top + containerDimensions.height);
|
|
176
154
|
let firstColumnIndex = 0;
|
|
177
155
|
let lastColumnIndex = columnPositions.length;
|
|
178
156
|
if (enabledForColumns) {
|
|
@@ -199,7 +177,7 @@ export const useGridVirtualScroller = props => {
|
|
|
199
177
|
firstColumnIndex,
|
|
200
178
|
lastColumnIndex
|
|
201
179
|
};
|
|
202
|
-
}, [enabled, enabledForColumns,
|
|
180
|
+
}, [enabled, enabledForColumns, rowsMeta, rootProps.autoHeight, rootProps.rowBuffer, currentPage, columnPositions, visibleColumns.length, apiRef, containerDimensions]);
|
|
203
181
|
useEnhancedEffect(() => {
|
|
204
182
|
if (enabled) {
|
|
205
183
|
// TODO a scroll reset should not be necessary
|
|
@@ -470,7 +448,7 @@ export const useGridVirtualScroller = props => {
|
|
|
470
448
|
const rows = [];
|
|
471
449
|
let isRowWithFocusedCellRendered = false;
|
|
472
450
|
for (let i = 0; i < renderedRows.length; i += 1) {
|
|
473
|
-
var _currentPage$
|
|
451
|
+
var _currentPage$range;
|
|
474
452
|
const {
|
|
475
453
|
id,
|
|
476
454
|
model
|
|
@@ -504,7 +482,7 @@ export const useGridVirtualScroller = props => {
|
|
|
504
482
|
const style = _extends({}, rowStyle, rootRowStyle);
|
|
505
483
|
rowStyleCache.current[id] = style;
|
|
506
484
|
}
|
|
507
|
-
let index = rowIndexOffset + ((currentPage == null || (_currentPage$
|
|
485
|
+
let index = rowIndexOffset + ((currentPage == null || (_currentPage$range = currentPage.range) == null ? void 0 : _currentPage$range.firstRowIndex) || 0) + firstRowToRender + i;
|
|
508
486
|
if (isRowWithFocusedCellNotInRange && (cellFocus == null ? void 0 : cellFocus.id) === id) {
|
|
509
487
|
index = indexOfRowWithFocusedCell;
|
|
510
488
|
isRowWithFocusedCellRendered = true;
|
|
@@ -597,4 +575,25 @@ export const useGridVirtualScroller = props => {
|
|
|
597
575
|
role: 'rowgroup'
|
|
598
576
|
})
|
|
599
577
|
};
|
|
600
|
-
};
|
|
578
|
+
};
|
|
579
|
+
function getNearestIndexToRender(apiRef, currentPage, rowsMeta, offset) {
|
|
580
|
+
var _currentPage$range2, _currentPage$range3;
|
|
581
|
+
const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
|
|
582
|
+
let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
|
|
583
|
+
if ((_currentPage$range2 = currentPage.range) != null && _currentPage$range2.lastRowIndex && !allRowsMeasured) {
|
|
584
|
+
// Check if all rows in this page are already measured
|
|
585
|
+
allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
|
|
586
|
+
}
|
|
587
|
+
const lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (((_currentPage$range3 = currentPage.range) == null ? void 0 : _currentPage$range3.firstRowIndex) || 0), 0, rowsMeta.positions.length);
|
|
588
|
+
if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
|
|
589
|
+
// If all rows were measured (when no row has "auto" as height) or all rows before the offset
|
|
590
|
+
// were measured, then use a binary search because it's faster.
|
|
591
|
+
return binarySearch(offset, rowsMeta.positions);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Otherwise, use an exponential search.
|
|
595
|
+
// If rows have "auto" as height, their positions will be based on estimated heights.
|
|
596
|
+
// In this case, we can skip several steps until we find a position higher than the offset.
|
|
597
|
+
// Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
|
|
598
|
+
return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
|
|
599
|
+
}
|
package/index.js
CHANGED
|
@@ -68,18 +68,21 @@ export var useGridClipboard = function useGridClipboard(apiRef, props) {
|
|
|
68
68
|
if (selectedRows.size > 0) {
|
|
69
69
|
textToCopy = apiRef.current.getDataAsCsv({
|
|
70
70
|
includeHeaders: false,
|
|
71
|
-
// TODO: make it configurable
|
|
72
71
|
delimiter: clipboardCopyCellDelimiter,
|
|
73
|
-
shouldAppendQuotes: false
|
|
72
|
+
shouldAppendQuotes: false,
|
|
73
|
+
escapeFormulas: false
|
|
74
74
|
});
|
|
75
75
|
} else {
|
|
76
76
|
var focusedCell = gridFocusCellSelector(apiRef);
|
|
77
77
|
if (focusedCell) {
|
|
78
78
|
var cellParams = apiRef.current.getCellParams(focusedCell.id, focusedCell.field);
|
|
79
79
|
textToCopy = serializeCellValue(cellParams, {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
csvOptions: {
|
|
81
|
+
delimiter: clipboardCopyCellDelimiter,
|
|
82
|
+
shouldAppendQuotes: false,
|
|
83
|
+
escapeFormulas: false
|
|
84
|
+
},
|
|
85
|
+
ignoreValueFormatter: ignoreValueFormatter
|
|
83
86
|
});
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -2,16 +2,22 @@ import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck";
|
|
|
2
2
|
import _createClass from "@babel/runtime/helpers/esm/createClass";
|
|
3
3
|
import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../../../../colDef';
|
|
4
4
|
import { buildWarning } from '../../../../utils/warning';
|
|
5
|
-
function sanitizeCellValue(value,
|
|
5
|
+
function sanitizeCellValue(value, csvOptions) {
|
|
6
6
|
if (typeof value === 'string') {
|
|
7
|
-
if (shouldAppendQuotes) {
|
|
7
|
+
if (csvOptions.shouldAppendQuotes || csvOptions.escapeFormulas) {
|
|
8
8
|
var escapedValue = value.replace(/"/g, '""');
|
|
9
|
-
// Make sure value containing delimiter or line break won't be split into multiple
|
|
10
|
-
if ([
|
|
9
|
+
// Make sure value containing delimiter or line break won't be split into multiple cells
|
|
10
|
+
if ([csvOptions.delimiter, '\n', '\r', '"'].some(function (delimiter) {
|
|
11
11
|
return value.includes(delimiter);
|
|
12
12
|
})) {
|
|
13
13
|
return "\"".concat(escapedValue, "\"");
|
|
14
14
|
}
|
|
15
|
+
if (csvOptions.escapeFormulas) {
|
|
16
|
+
// See https://owasp.org/www-community/attacks/CSV_Injection
|
|
17
|
+
if (['=', '+', '-', '@', '\t', '\r'].includes(escapedValue[0])) {
|
|
18
|
+
return "'".concat(escapedValue);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
15
21
|
return escapedValue;
|
|
16
22
|
}
|
|
17
23
|
return value;
|
|
@@ -19,9 +25,8 @@ function sanitizeCellValue(value, delimiterCharacter, shouldAppendQuotes) {
|
|
|
19
25
|
return value;
|
|
20
26
|
}
|
|
21
27
|
export var serializeCellValue = function serializeCellValue(cellParams, options) {
|
|
22
|
-
var
|
|
23
|
-
ignoreValueFormatter = options.ignoreValueFormatter
|
|
24
|
-
shouldAppendQuotes = options.shouldAppendQuotes;
|
|
28
|
+
var csvOptions = options.csvOptions,
|
|
29
|
+
ignoreValueFormatter = options.ignoreValueFormatter;
|
|
25
30
|
var value;
|
|
26
31
|
if (ignoreValueFormatter) {
|
|
27
32
|
var _cellParams$value2;
|
|
@@ -39,7 +44,7 @@ export var serializeCellValue = function serializeCellValue(cellParams, options)
|
|
|
39
44
|
} else {
|
|
40
45
|
value = cellParams.formattedValue;
|
|
41
46
|
}
|
|
42
|
-
return sanitizeCellValue(value,
|
|
47
|
+
return sanitizeCellValue(value, csvOptions);
|
|
43
48
|
};
|
|
44
49
|
var objectFormattedValueWarning = buildWarning(['MUI: When the value of a field is an object or a `renderCell` is provided, the CSV export might not display the value correctly.', 'You can provide a `valueFormatter` with a string representation to be used.']);
|
|
45
50
|
var CSVRow = /*#__PURE__*/function () {
|
|
@@ -54,12 +59,12 @@ var CSVRow = /*#__PURE__*/function () {
|
|
|
54
59
|
key: "addValue",
|
|
55
60
|
value: function addValue(value) {
|
|
56
61
|
if (!this.isEmpty) {
|
|
57
|
-
this.rowString += this.options.
|
|
62
|
+
this.rowString += this.options.csvOptions.delimiter;
|
|
58
63
|
}
|
|
59
64
|
if (value === null || value === undefined) {
|
|
60
65
|
this.rowString += '';
|
|
61
66
|
} else if (typeof this.options.sanitizeCellValue === 'function') {
|
|
62
|
-
this.rowString += this.options.sanitizeCellValue(value, this.options.
|
|
67
|
+
this.rowString += this.options.sanitizeCellValue(value, this.options.csvOptions);
|
|
63
68
|
} else {
|
|
64
69
|
this.rowString += value;
|
|
65
70
|
}
|
|
@@ -77,12 +82,10 @@ var serializeRow = function serializeRow(_ref) {
|
|
|
77
82
|
var id = _ref.id,
|
|
78
83
|
columns = _ref.columns,
|
|
79
84
|
getCellParams = _ref.getCellParams,
|
|
80
|
-
|
|
81
|
-
ignoreValueFormatter = _ref.ignoreValueFormatter
|
|
82
|
-
shouldAppendQuotes = _ref.shouldAppendQuotes;
|
|
85
|
+
csvOptions = _ref.csvOptions,
|
|
86
|
+
ignoreValueFormatter = _ref.ignoreValueFormatter;
|
|
83
87
|
var row = new CSVRow({
|
|
84
|
-
|
|
85
|
-
shouldAppendQuotes: shouldAppendQuotes
|
|
88
|
+
csvOptions: csvOptions
|
|
86
89
|
});
|
|
87
90
|
columns.forEach(function (column) {
|
|
88
91
|
var cellParams = getCellParams(id, column.field);
|
|
@@ -92,9 +95,8 @@ var serializeRow = function serializeRow(_ref) {
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
row.addValue(serializeCellValue(cellParams, {
|
|
95
|
-
delimiterCharacter: delimiterCharacter,
|
|
96
98
|
ignoreValueFormatter: ignoreValueFormatter,
|
|
97
|
-
|
|
99
|
+
csvOptions: csvOptions
|
|
98
100
|
}));
|
|
99
101
|
});
|
|
100
102
|
return row.getRowString();
|
|
@@ -102,30 +104,26 @@ var serializeRow = function serializeRow(_ref) {
|
|
|
102
104
|
export function buildCSV(options) {
|
|
103
105
|
var columns = options.columns,
|
|
104
106
|
rowIds = options.rowIds,
|
|
105
|
-
|
|
106
|
-
includeHeaders = options.includeHeaders,
|
|
107
|
-
includeColumnGroupsHeaders = options.includeColumnGroupsHeaders,
|
|
107
|
+
csvOptions = options.csvOptions,
|
|
108
108
|
ignoreValueFormatter = options.ignoreValueFormatter,
|
|
109
|
-
apiRef = options.apiRef
|
|
110
|
-
shouldAppendQuotes = options.shouldAppendQuotes;
|
|
109
|
+
apiRef = options.apiRef;
|
|
111
110
|
var CSVBody = rowIds.reduce(function (acc, id) {
|
|
112
111
|
return "".concat(acc).concat(serializeRow({
|
|
113
112
|
id: id,
|
|
114
113
|
columns: columns,
|
|
115
114
|
getCellParams: apiRef.current.getCellParams,
|
|
116
|
-
delimiterCharacter: delimiterCharacter,
|
|
117
115
|
ignoreValueFormatter: ignoreValueFormatter,
|
|
118
|
-
|
|
116
|
+
csvOptions: csvOptions
|
|
119
117
|
}), "\r\n");
|
|
120
118
|
}, '').trim();
|
|
121
|
-
if (!includeHeaders) {
|
|
119
|
+
if (!csvOptions.includeHeaders) {
|
|
122
120
|
return CSVBody;
|
|
123
121
|
}
|
|
124
122
|
var filteredColumns = columns.filter(function (column) {
|
|
125
123
|
return column.field !== GRID_CHECKBOX_SELECTION_COL_DEF.field;
|
|
126
124
|
});
|
|
127
125
|
var headerRows = [];
|
|
128
|
-
if (includeColumnGroupsHeaders) {
|
|
126
|
+
if (csvOptions.includeColumnGroupsHeaders) {
|
|
129
127
|
var columnGroupLookup = apiRef.current.unstable_getAllGroupDetails();
|
|
130
128
|
var maxColumnGroupsDepth = 0;
|
|
131
129
|
var columnGroupPathsLookup = filteredColumns.reduce(function (acc, column) {
|
|
@@ -136,9 +134,8 @@ export function buildCSV(options) {
|
|
|
136
134
|
}, {});
|
|
137
135
|
var _loop = function _loop(i) {
|
|
138
136
|
var headerGroupRow = new CSVRow({
|
|
139
|
-
|
|
140
|
-
sanitizeCellValue: sanitizeCellValue
|
|
141
|
-
shouldAppendQuotes: shouldAppendQuotes
|
|
137
|
+
csvOptions: csvOptions,
|
|
138
|
+
sanitizeCellValue: sanitizeCellValue
|
|
142
139
|
});
|
|
143
140
|
headerRows.push(headerGroupRow);
|
|
144
141
|
filteredColumns.forEach(function (column) {
|
|
@@ -152,9 +149,8 @@ export function buildCSV(options) {
|
|
|
152
149
|
}
|
|
153
150
|
}
|
|
154
151
|
var mainHeaderRow = new CSVRow({
|
|
155
|
-
|
|
156
|
-
sanitizeCellValue: sanitizeCellValue
|
|
157
|
-
shouldAppendQuotes: shouldAppendQuotes
|
|
152
|
+
csvOptions: csvOptions,
|
|
153
|
+
sanitizeCellValue: sanitizeCellValue
|
|
158
154
|
});
|
|
159
155
|
filteredColumns.forEach(function (column) {
|
|
160
156
|
mainHeaderRow.addValue(column.headerName || column.field);
|
|
@@ -21,7 +21,7 @@ export var useGridCsvExport = function useGridCsvExport(apiRef, props) {
|
|
|
21
21
|
var ignoreValueFormatterProp = props.unstable_ignoreValueFormatterDuringExport;
|
|
22
22
|
var ignoreValueFormatter = (_typeof(ignoreValueFormatterProp) === 'object' ? ignoreValueFormatterProp == null ? void 0 : ignoreValueFormatterProp.csvExport : ignoreValueFormatterProp) || false;
|
|
23
23
|
var getDataAsCsv = React.useCallback(function () {
|
|
24
|
-
var _options$getRowsToExp, _options$includeHeade, _options$includeColum, _options$
|
|
24
|
+
var _options$getRowsToExp, _options$shouldAppend, _options$includeHeade, _options$includeColum, _options$escapeFormul;
|
|
25
25
|
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
26
26
|
logger.debug("Get data as CSV");
|
|
27
27
|
var exportedColumns = getColumnsToExport({
|
|
@@ -35,12 +35,15 @@ export var useGridCsvExport = function useGridCsvExport(apiRef, props) {
|
|
|
35
35
|
return buildCSV({
|
|
36
36
|
columns: exportedColumns,
|
|
37
37
|
rowIds: exportedRowIds,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
csvOptions: {
|
|
39
|
+
delimiter: options.delimiter || ',',
|
|
40
|
+
shouldAppendQuotes: (_options$shouldAppend = options.shouldAppendQuotes) != null ? _options$shouldAppend : true,
|
|
41
|
+
includeHeaders: (_options$includeHeade = options.includeHeaders) != null ? _options$includeHeade : true,
|
|
42
|
+
includeColumnGroupsHeaders: (_options$includeColum = options.includeColumnGroupsHeaders) != null ? _options$includeColum : true,
|
|
43
|
+
escapeFormulas: (_options$escapeFormul = options.escapeFormulas) != null ? _options$escapeFormul : true
|
|
44
|
+
},
|
|
41
45
|
ignoreValueFormatter: ignoreValueFormatter,
|
|
42
|
-
apiRef: apiRef
|
|
43
|
-
shouldAppendQuotes: (_options$shouldAppend = options.shouldAppendQuotes) != null ? _options$shouldAppend : true
|
|
46
|
+
apiRef: apiRef
|
|
44
47
|
});
|
|
45
48
|
}, [logger, apiRef, ignoreValueFormatter]);
|
|
46
49
|
var exportDataAsCsv = React.useCallback(function (options) {
|