@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.
@@ -66,7 +66,6 @@ var MEMOIZE_OPTIONS = {
66
66
  maxSize: 3
67
67
  };
68
68
  export var useGridVirtualScroller = function useGridVirtualScroller(props) {
69
- var _currentPage$range3, _currentPage$range4;
70
69
  var apiRef = useGridPrivateApiContext();
71
70
  var rootProps = useGridRootProps();
72
71
  var visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
@@ -148,27 +147,6 @@ export var useGridVirtualScroller = function useGridVirtualScroller(props) {
148
147
  }
149
148
  return -1;
150
149
  }, [cellFocus, visibleColumns]);
151
- var getNearestIndexToRender = React.useCallback(function (offset) {
152
- var _currentPage$range, _currentPage$range2;
153
- var lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
154
- var allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
155
- if ((_currentPage$range = currentPage.range) != null && _currentPage$range.lastRowIndex && !allRowsMeasured) {
156
- // Check if all rows in this page are already measured
157
- allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
158
- }
159
- var lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (((_currentPage$range2 = currentPage.range) == null ? void 0 : _currentPage$range2.firstRowIndex) || 0), 0, rowsMeta.positions.length);
160
- if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
161
- // If all rows were measured (when no row has "auto" as height) or all rows before the offset
162
- // were measured, then use a binary search because it's faster.
163
- return binarySearch(offset, rowsMeta.positions);
164
- }
165
-
166
- // Otherwise, use an exponential search.
167
- // If rows have "auto" as height, their positions will be based on estimated heights.
168
- // In this case, we can skip several steps until we find a position higher than the offset.
169
- // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
170
- return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
171
- }, [apiRef, (_currentPage$range3 = currentPage.range) == null ? void 0 : _currentPage$range3.firstRowIndex, (_currentPage$range4 = currentPage.range) == null ? void 0 : _currentPage$range4.lastRowIndex, rowsMeta.positions]);
172
150
  var computeRenderContext = React.useCallback(function () {
173
151
  if (!enabled) {
174
152
  return {
@@ -184,8 +162,8 @@ export var useGridVirtualScroller = function useGridVirtualScroller(props) {
184
162
 
185
163
  // Clamp the value because the search may return an index out of bounds.
186
164
  // In the last index, this is not needed because Array.slice doesn't include it.
187
- var firstRowIndex = Math.min(getNearestIndexToRender(top), rowsMeta.positions.length - 1);
188
- var lastRowIndex = rootProps.autoHeight ? firstRowIndex + currentPage.rows.length : getNearestIndexToRender(top + containerDimensions.height);
165
+ var firstRowIndex = Math.min(getNearestIndexToRender(apiRef, currentPage, rowsMeta, top), rowsMeta.positions.length - 1);
166
+ var lastRowIndex = rootProps.autoHeight ? firstRowIndex + currentPage.rows.length : getNearestIndexToRender(apiRef, currentPage, rowsMeta, top + containerDimensions.height);
189
167
  var firstColumnIndex = 0;
190
168
  var lastColumnIndex = columnPositions.length;
191
169
  if (enabledForColumns) {
@@ -215,7 +193,7 @@ export var useGridVirtualScroller = function useGridVirtualScroller(props) {
215
193
  firstColumnIndex: firstColumnIndex,
216
194
  lastColumnIndex: lastColumnIndex
217
195
  };
218
- }, [enabled, enabledForColumns, getNearestIndexToRender, rowsMeta.positions.length, rootProps.autoHeight, rootProps.rowBuffer, currentPage.rows, columnPositions, visibleColumns.length, apiRef, containerDimensions]);
196
+ }, [enabled, enabledForColumns, rowsMeta, rootProps.autoHeight, rootProps.rowBuffer, currentPage, columnPositions, visibleColumns.length, apiRef, containerDimensions]);
219
197
  useEnhancedEffect(function () {
220
198
  if (enabled) {
221
199
  // TODO a scroll reset should not be necessary
@@ -503,7 +481,7 @@ export var useGridVirtualScroller = function useGridVirtualScroller(props) {
503
481
  var rows = [];
504
482
  var isRowWithFocusedCellRendered = false;
505
483
  for (var _i = 0; _i < renderedRows.length; _i += 1) {
506
- var _currentPage$range5;
484
+ var _currentPage$range;
507
485
  var _renderedRows$_i = renderedRows[_i],
508
486
  _id = _renderedRows$_i.id,
509
487
  _model = _renderedRows$_i.model;
@@ -534,7 +512,7 @@ export var useGridVirtualScroller = function useGridVirtualScroller(props) {
534
512
  var style = _extends({}, rowStyle, rootRowStyle);
535
513
  rowStyleCache.current[_id] = style;
536
514
  }
537
- var index = rowIndexOffset + ((currentPage == null || (_currentPage$range5 = currentPage.range) == null ? void 0 : _currentPage$range5.firstRowIndex) || 0) + firstRowToRender + _i;
515
+ var index = rowIndexOffset + ((currentPage == null || (_currentPage$range = currentPage.range) == null ? void 0 : _currentPage$range.firstRowIndex) || 0) + firstRowToRender + _i;
538
516
  if (isRowWithFocusedCellNotInRange && (cellFocus == null ? void 0 : cellFocus.id) === _id) {
539
517
  index = indexOfRowWithFocusedCell;
540
518
  isRowWithFocusedCellRendered = true;
@@ -634,4 +612,25 @@ export var useGridVirtualScroller = function useGridVirtualScroller(props) {
634
612
  };
635
613
  }
636
614
  };
637
- };
615
+ };
616
+ function getNearestIndexToRender(apiRef, currentPage, rowsMeta, offset) {
617
+ var _currentPage$range2, _currentPage$range3;
618
+ var lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
619
+ var allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
620
+ if ((_currentPage$range2 = currentPage.range) != null && _currentPage$range2.lastRowIndex && !allRowsMeasured) {
621
+ // Check if all rows in this page are already measured
622
+ allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
623
+ }
624
+ var lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (((_currentPage$range3 = currentPage.range) == null ? void 0 : _currentPage$range3.firstRowIndex) || 0), 0, rowsMeta.positions.length);
625
+ if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
626
+ // If all rows were measured (when no row has "auto" as height) or all rows before the offset
627
+ // were measured, then use a binary search because it's faster.
628
+ return binarySearch(offset, rowsMeta.positions);
629
+ }
630
+
631
+ // Otherwise, use an exponential search.
632
+ // If rows have "auto" as height, their positions will be based on estimated heights.
633
+ // In this case, we can skip several steps until we find a position higher than the offset.
634
+ // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
635
+ return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
636
+ }
package/legacy/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v6.19.10
2
+ * @mui/x-data-grid v6.20.0
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -37,6 +37,13 @@ export interface GridFileExportOptions<Api extends GridApiCommon = GridApiCommun
37
37
  * @returns {GridRowId[]} The list of row ids to export.
38
38
  */
39
39
  getRowsToExport?: (params: GridGetRowsToExportParams<Api>) => GridRowId[];
40
+ /**
41
+ * If `false`, the formulas in the cells will not be escaped.
42
+ * It is not recommended to disable this option as it exposes the user to potential CSV injection attacks.
43
+ * See https://owasp.org/www-community/attacks/CSV_Injection for more information.
44
+ * @default true
45
+ */
46
+ escapeFormulas?: boolean;
40
47
  }
41
48
  export interface GridGetRowsToExportParams<Api extends GridApiCommon = GridApiCommunity> {
42
49
  /**
@@ -66,18 +66,21 @@ export const useGridClipboard = (apiRef, props) => {
66
66
  if (selectedRows.size > 0) {
67
67
  textToCopy = apiRef.current.getDataAsCsv({
68
68
  includeHeaders: false,
69
- // TODO: make it configurable
70
69
  delimiter: clipboardCopyCellDelimiter,
71
- shouldAppendQuotes: false
70
+ shouldAppendQuotes: false,
71
+ escapeFormulas: false
72
72
  });
73
73
  } else {
74
74
  const focusedCell = gridFocusCellSelector(apiRef);
75
75
  if (focusedCell) {
76
76
  const cellParams = apiRef.current.getCellParams(focusedCell.id, focusedCell.field);
77
77
  textToCopy = serializeCellValue(cellParams, {
78
- delimiterCharacter: clipboardCopyCellDelimiter,
79
- ignoreValueFormatter,
80
- shouldAppendQuotes: false
78
+ csvOptions: {
79
+ delimiter: clipboardCopyCellDelimiter,
80
+ shouldAppendQuotes: false,
81
+ escapeFormulas: false
82
+ },
83
+ ignoreValueFormatter
81
84
  });
82
85
  }
83
86
  }
@@ -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, delimiterCharacter, shouldAppendQuotes) {
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 rows
8
- if ([delimiterCharacter, '\n', '\r', '"'].some(delimiter => value.includes(delimiter))) {
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
- delimiterCharacter,
20
- ignoreValueFormatter,
21
- shouldAppendQuotes
25
+ csvOptions,
26
+ ignoreValueFormatter
22
27
  } = options;
23
28
  let value;
24
29
  if (ignoreValueFormatter) {
@@ -35,7 +40,7 @@ export const serializeCellValue = (cellParams, options) => {
35
40
  } else {
36
41
  value = cellParams.formattedValue;
37
42
  }
38
- return sanitizeCellValue(value, delimiterCharacter, shouldAppendQuotes);
43
+ return sanitizeCellValue(value, csvOptions);
39
44
  };
40
45
  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.']);
41
46
  class CSVRow {
@@ -47,12 +52,12 @@ class CSVRow {
47
52
  }
48
53
  addValue(value) {
49
54
  if (!this.isEmpty) {
50
- this.rowString += this.options.delimiterCharacter;
55
+ this.rowString += this.options.csvOptions.delimiter;
51
56
  }
52
57
  if (value === null || value === undefined) {
53
58
  this.rowString += '';
54
59
  } else if (typeof this.options.sanitizeCellValue === 'function') {
55
- this.rowString += this.options.sanitizeCellValue(value, this.options.delimiterCharacter, this.options.shouldAppendQuotes);
60
+ this.rowString += this.options.sanitizeCellValue(value, this.options.csvOptions);
56
61
  } else {
57
62
  this.rowString += value;
58
63
  }
@@ -66,13 +71,11 @@ const serializeRow = ({
66
71
  id,
67
72
  columns,
68
73
  getCellParams,
69
- delimiterCharacter,
70
- ignoreValueFormatter,
71
- shouldAppendQuotes
74
+ csvOptions,
75
+ ignoreValueFormatter
72
76
  }) => {
73
77
  const row = new CSVRow({
74
- delimiterCharacter,
75
- shouldAppendQuotes
78
+ csvOptions
76
79
  });
77
80
  columns.forEach(column => {
78
81
  const cellParams = getCellParams(id, column.field);
@@ -82,9 +85,8 @@ const serializeRow = ({
82
85
  }
83
86
  }
84
87
  row.addValue(serializeCellValue(cellParams, {
85
- delimiterCharacter,
86
88
  ignoreValueFormatter,
87
- shouldAppendQuotes
89
+ csvOptions
88
90
  }));
89
91
  });
90
92
  return row.getRowString();
@@ -93,27 +95,23 @@ export function buildCSV(options) {
93
95
  const {
94
96
  columns,
95
97
  rowIds,
96
- delimiterCharacter,
97
- includeHeaders,
98
- includeColumnGroupsHeaders,
98
+ csvOptions,
99
99
  ignoreValueFormatter,
100
- apiRef,
101
- shouldAppendQuotes
100
+ apiRef
102
101
  } = options;
103
102
  const CSVBody = rowIds.reduce((acc, id) => `${acc}${serializeRow({
104
103
  id,
105
104
  columns,
106
105
  getCellParams: apiRef.current.getCellParams,
107
- delimiterCharacter,
108
106
  ignoreValueFormatter,
109
- shouldAppendQuotes
107
+ csvOptions
110
108
  })}\r\n`, '').trim();
111
- if (!includeHeaders) {
109
+ if (!csvOptions.includeHeaders) {
112
110
  return CSVBody;
113
111
  }
114
112
  const filteredColumns = columns.filter(column => column.field !== GRID_CHECKBOX_SELECTION_COL_DEF.field);
115
113
  const headerRows = [];
116
- if (includeColumnGroupsHeaders) {
114
+ if (csvOptions.includeColumnGroupsHeaders) {
117
115
  const columnGroupLookup = apiRef.current.unstable_getAllGroupDetails();
118
116
  let maxColumnGroupsDepth = 0;
119
117
  const columnGroupPathsLookup = filteredColumns.reduce((acc, column) => {
@@ -124,9 +122,8 @@ export function buildCSV(options) {
124
122
  }, {});
125
123
  for (let i = 0; i < maxColumnGroupsDepth; i += 1) {
126
124
  const headerGroupRow = new CSVRow({
127
- delimiterCharacter,
128
- sanitizeCellValue,
129
- shouldAppendQuotes
125
+ csvOptions,
126
+ sanitizeCellValue
130
127
  });
131
128
  headerRows.push(headerGroupRow);
132
129
  filteredColumns.forEach(column => {
@@ -137,9 +134,8 @@ export function buildCSV(options) {
137
134
  }
138
135
  }
139
136
  const mainHeaderRow = new CSVRow({
140
- delimiterCharacter,
141
- sanitizeCellValue,
142
- shouldAppendQuotes
137
+ csvOptions,
138
+ sanitizeCellValue
143
139
  });
144
140
  filteredColumns.forEach(column => {
145
141
  mainHeaderRow.addValue(column.headerName || column.field);
@@ -31,12 +31,15 @@ export const useGridCsvExport = (apiRef, props) => {
31
31
  return buildCSV({
32
32
  columns: exportedColumns,
33
33
  rowIds: exportedRowIds,
34
- delimiterCharacter: options.delimiter || ',',
35
- includeHeaders: options.includeHeaders ?? true,
36
- includeColumnGroupsHeaders: options.includeColumnGroupsHeaders ?? true,
34
+ csvOptions: {
35
+ delimiter: options.delimiter || ',',
36
+ shouldAppendQuotes: options.shouldAppendQuotes ?? true,
37
+ includeHeaders: options.includeHeaders ?? true,
38
+ includeColumnGroupsHeaders: options.includeColumnGroupsHeaders ?? true,
39
+ escapeFormulas: options.escapeFormulas ?? true
40
+ },
37
41
  ignoreValueFormatter,
38
- apiRef,
39
- shouldAppendQuotes: options.shouldAppendQuotes ?? true
42
+ apiRef
40
43
  });
41
44
  }, [logger, apiRef, ignoreValueFormatter]);
42
45
  const exportDataAsCsv = React.useCallback(options => {
@@ -133,26 +133,6 @@ export const useGridVirtualScroller = props => {
133
133
  }
134
134
  return -1;
135
135
  }, [cellFocus, visibleColumns]);
136
- const getNearestIndexToRender = React.useCallback(offset => {
137
- const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
138
- let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
139
- if (currentPage.range?.lastRowIndex && !allRowsMeasured) {
140
- // Check if all rows in this page are already measured
141
- allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
142
- }
143
- const lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (currentPage.range?.firstRowIndex || 0), 0, rowsMeta.positions.length);
144
- if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
145
- // If all rows were measured (when no row has "auto" as height) or all rows before the offset
146
- // were measured, then use a binary search because it's faster.
147
- return binarySearch(offset, rowsMeta.positions);
148
- }
149
-
150
- // Otherwise, use an exponential search.
151
- // If rows have "auto" as height, their positions will be based on estimated heights.
152
- // In this case, we can skip several steps until we find a position higher than the offset.
153
- // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
154
- return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
155
- }, [apiRef, currentPage.range?.firstRowIndex, currentPage.range?.lastRowIndex, rowsMeta.positions]);
156
136
  const computeRenderContext = React.useCallback(() => {
157
137
  if (!enabled) {
158
138
  return {
@@ -169,8 +149,8 @@ export const useGridVirtualScroller = props => {
169
149
 
170
150
  // Clamp the value because the search may return an index out of bounds.
171
151
  // In the last index, this is not needed because Array.slice doesn't include it.
172
- const firstRowIndex = Math.min(getNearestIndexToRender(top), rowsMeta.positions.length - 1);
173
- 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);
174
154
  let firstColumnIndex = 0;
175
155
  let lastColumnIndex = columnPositions.length;
176
156
  if (enabledForColumns) {
@@ -197,7 +177,7 @@ export const useGridVirtualScroller = props => {
197
177
  firstColumnIndex,
198
178
  lastColumnIndex
199
179
  };
200
- }, [enabled, enabledForColumns, getNearestIndexToRender, rowsMeta.positions.length, rootProps.autoHeight, rootProps.rowBuffer, currentPage.rows, columnPositions, visibleColumns.length, apiRef, containerDimensions]);
180
+ }, [enabled, enabledForColumns, rowsMeta, rootProps.autoHeight, rootProps.rowBuffer, currentPage, columnPositions, visibleColumns.length, apiRef, containerDimensions]);
201
181
  useEnhancedEffect(() => {
202
182
  if (enabled) {
203
183
  // TODO a scroll reset should not be necessary
@@ -592,4 +572,24 @@ export const useGridVirtualScroller = props => {
592
572
  role: 'rowgroup'
593
573
  })
594
574
  };
595
- };
575
+ };
576
+ function getNearestIndexToRender(apiRef, currentPage, rowsMeta, offset) {
577
+ const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
578
+ let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
579
+ if (currentPage.range?.lastRowIndex && !allRowsMeasured) {
580
+ // Check if all rows in this page are already measured
581
+ allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
582
+ }
583
+ const lastMeasuredIndexRelativeToCurrentPage = clamp(lastMeasuredIndexRelativeToAllRows - (currentPage.range?.firstRowIndex || 0), 0, rowsMeta.positions.length);
584
+ if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
585
+ // If all rows were measured (when no row has "auto" as height) or all rows before the offset
586
+ // were measured, then use a binary search because it's faster.
587
+ return binarySearch(offset, rowsMeta.positions);
588
+ }
589
+
590
+ // Otherwise, use an exponential search.
591
+ // If rows have "auto" as height, their positions will be based on estimated heights.
592
+ // In this case, we can skip several steps until we find a position higher than the offset.
593
+ // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
594
+ return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
595
+ }
package/modern/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v6.19.10
2
+ * @mui/x-data-grid v6.20.0
3
3
  *
4
4
  * @license MIT
5
5
  * This source code is licensed under the MIT license found in the
@@ -74,18 +74,21 @@ const useGridClipboard = (apiRef, props) => {
74
74
  if (selectedRows.size > 0) {
75
75
  textToCopy = apiRef.current.getDataAsCsv({
76
76
  includeHeaders: false,
77
- // TODO: make it configurable
78
77
  delimiter: clipboardCopyCellDelimiter,
79
- shouldAppendQuotes: false
78
+ shouldAppendQuotes: false,
79
+ escapeFormulas: false
80
80
  });
81
81
  } else {
82
82
  const focusedCell = (0, _gridFocusStateSelector.gridFocusCellSelector)(apiRef);
83
83
  if (focusedCell) {
84
84
  const cellParams = apiRef.current.getCellParams(focusedCell.id, focusedCell.field);
85
85
  textToCopy = (0, _csvSerializer.serializeCellValue)(cellParams, {
86
- delimiterCharacter: clipboardCopyCellDelimiter,
87
- ignoreValueFormatter,
88
- shouldAppendQuotes: false
86
+ csvOptions: {
87
+ delimiter: clipboardCopyCellDelimiter,
88
+ shouldAppendQuotes: false,
89
+ escapeFormulas: false
90
+ },
91
+ ignoreValueFormatter
89
92
  });
90
93
  }
91
94
  }
@@ -7,14 +7,20 @@ exports.buildCSV = buildCSV;
7
7
  exports.serializeCellValue = void 0;
8
8
  var _colDef = require("../../../../colDef");
9
9
  var _warning = require("../../../../utils/warning");
10
- function sanitizeCellValue(value, delimiterCharacter, shouldAppendQuotes) {
10
+ function sanitizeCellValue(value, csvOptions) {
11
11
  if (typeof value === 'string') {
12
- if (shouldAppendQuotes) {
12
+ if (csvOptions.shouldAppendQuotes || csvOptions.escapeFormulas) {
13
13
  const escapedValue = value.replace(/"/g, '""');
14
- // Make sure value containing delimiter or line break won't be split into multiple rows
15
- if ([delimiterCharacter, '\n', '\r', '"'].some(delimiter => value.includes(delimiter))) {
14
+ // Make sure value containing delimiter or line break won't be split into multiple cells
15
+ if ([csvOptions.delimiter, '\n', '\r', '"'].some(delimiter => value.includes(delimiter))) {
16
16
  return `"${escapedValue}"`;
17
17
  }
18
+ if (csvOptions.escapeFormulas) {
19
+ // See https://owasp.org/www-community/attacks/CSV_Injection
20
+ if (['=', '+', '-', '@', '\t', '\r'].includes(escapedValue[0])) {
21
+ return `'${escapedValue}`;
22
+ }
23
+ }
18
24
  return escapedValue;
19
25
  }
20
26
  return value;
@@ -23,9 +29,8 @@ function sanitizeCellValue(value, delimiterCharacter, shouldAppendQuotes) {
23
29
  }
24
30
  const serializeCellValue = (cellParams, options) => {
25
31
  const {
26
- delimiterCharacter,
27
- ignoreValueFormatter,
28
- shouldAppendQuotes
32
+ csvOptions,
33
+ ignoreValueFormatter
29
34
  } = options;
30
35
  let value;
31
36
  if (ignoreValueFormatter) {
@@ -42,7 +47,7 @@ const serializeCellValue = (cellParams, options) => {
42
47
  } else {
43
48
  value = cellParams.formattedValue;
44
49
  }
45
- return sanitizeCellValue(value, delimiterCharacter, shouldAppendQuotes);
50
+ return sanitizeCellValue(value, csvOptions);
46
51
  };
47
52
  exports.serializeCellValue = serializeCellValue;
48
53
  const objectFormattedValueWarning = (0, _warning.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.']);
@@ -55,12 +60,12 @@ class CSVRow {
55
60
  }
56
61
  addValue(value) {
57
62
  if (!this.isEmpty) {
58
- this.rowString += this.options.delimiterCharacter;
63
+ this.rowString += this.options.csvOptions.delimiter;
59
64
  }
60
65
  if (value === null || value === undefined) {
61
66
  this.rowString += '';
62
67
  } else if (typeof this.options.sanitizeCellValue === 'function') {
63
- this.rowString += this.options.sanitizeCellValue(value, this.options.delimiterCharacter, this.options.shouldAppendQuotes);
68
+ this.rowString += this.options.sanitizeCellValue(value, this.options.csvOptions);
64
69
  } else {
65
70
  this.rowString += value;
66
71
  }
@@ -74,13 +79,11 @@ const serializeRow = ({
74
79
  id,
75
80
  columns,
76
81
  getCellParams,
77
- delimiterCharacter,
78
- ignoreValueFormatter,
79
- shouldAppendQuotes
82
+ csvOptions,
83
+ ignoreValueFormatter
80
84
  }) => {
81
85
  const row = new CSVRow({
82
- delimiterCharacter,
83
- shouldAppendQuotes
86
+ csvOptions
84
87
  });
85
88
  columns.forEach(column => {
86
89
  const cellParams = getCellParams(id, column.field);
@@ -90,9 +93,8 @@ const serializeRow = ({
90
93
  }
91
94
  }
92
95
  row.addValue(serializeCellValue(cellParams, {
93
- delimiterCharacter,
94
96
  ignoreValueFormatter,
95
- shouldAppendQuotes
97
+ csvOptions
96
98
  }));
97
99
  });
98
100
  return row.getRowString();
@@ -101,27 +103,23 @@ function buildCSV(options) {
101
103
  const {
102
104
  columns,
103
105
  rowIds,
104
- delimiterCharacter,
105
- includeHeaders,
106
- includeColumnGroupsHeaders,
106
+ csvOptions,
107
107
  ignoreValueFormatter,
108
- apiRef,
109
- shouldAppendQuotes
108
+ apiRef
110
109
  } = options;
111
110
  const CSVBody = rowIds.reduce((acc, id) => `${acc}${serializeRow({
112
111
  id,
113
112
  columns,
114
113
  getCellParams: apiRef.current.getCellParams,
115
- delimiterCharacter,
116
114
  ignoreValueFormatter,
117
- shouldAppendQuotes
115
+ csvOptions
118
116
  })}\r\n`, '').trim();
119
- if (!includeHeaders) {
117
+ if (!csvOptions.includeHeaders) {
120
118
  return CSVBody;
121
119
  }
122
120
  const filteredColumns = columns.filter(column => column.field !== _colDef.GRID_CHECKBOX_SELECTION_COL_DEF.field);
123
121
  const headerRows = [];
124
- if (includeColumnGroupsHeaders) {
122
+ if (csvOptions.includeColumnGroupsHeaders) {
125
123
  const columnGroupLookup = apiRef.current.unstable_getAllGroupDetails();
126
124
  let maxColumnGroupsDepth = 0;
127
125
  const columnGroupPathsLookup = filteredColumns.reduce((acc, column) => {
@@ -132,9 +130,8 @@ function buildCSV(options) {
132
130
  }, {});
133
131
  for (let i = 0; i < maxColumnGroupsDepth; i += 1) {
134
132
  const headerGroupRow = new CSVRow({
135
- delimiterCharacter,
136
- sanitizeCellValue,
137
- shouldAppendQuotes
133
+ csvOptions,
134
+ sanitizeCellValue
138
135
  });
139
136
  headerRows.push(headerGroupRow);
140
137
  filteredColumns.forEach(column => {
@@ -145,9 +142,8 @@ function buildCSV(options) {
145
142
  }
146
143
  }
147
144
  const mainHeaderRow = new CSVRow({
148
- delimiterCharacter,
149
- sanitizeCellValue,
150
- shouldAppendQuotes
145
+ csvOptions,
146
+ sanitizeCellValue
151
147
  });
152
148
  filteredColumns.forEach(column => {
153
149
  mainHeaderRow.addValue(column.headerName || column.field);
@@ -39,12 +39,15 @@ const useGridCsvExport = (apiRef, props) => {
39
39
  return (0, _csvSerializer.buildCSV)({
40
40
  columns: exportedColumns,
41
41
  rowIds: exportedRowIds,
42
- delimiterCharacter: options.delimiter || ',',
43
- includeHeaders: options.includeHeaders ?? true,
44
- includeColumnGroupsHeaders: options.includeColumnGroupsHeaders ?? true,
42
+ csvOptions: {
43
+ delimiter: options.delimiter || ',',
44
+ shouldAppendQuotes: options.shouldAppendQuotes ?? true,
45
+ includeHeaders: options.includeHeaders ?? true,
46
+ includeColumnGroupsHeaders: options.includeColumnGroupsHeaders ?? true,
47
+ escapeFormulas: options.escapeFormulas ?? true
48
+ },
45
49
  ignoreValueFormatter,
46
- apiRef,
47
- shouldAppendQuotes: options.shouldAppendQuotes ?? true
50
+ apiRef
48
51
  });
49
52
  }, [logger, apiRef, ignoreValueFormatter]);
50
53
  const exportDataAsCsv = React.useCallback(options => {
@@ -144,26 +144,6 @@ const useGridVirtualScroller = props => {
144
144
  }
145
145
  return -1;
146
146
  }, [cellFocus, visibleColumns]);
147
- const getNearestIndexToRender = React.useCallback(offset => {
148
- const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
149
- let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
150
- if (currentPage.range?.lastRowIndex && !allRowsMeasured) {
151
- // Check if all rows in this page are already measured
152
- allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
153
- }
154
- const lastMeasuredIndexRelativeToCurrentPage = (0, _utils2.clamp)(lastMeasuredIndexRelativeToAllRows - (currentPage.range?.firstRowIndex || 0), 0, rowsMeta.positions.length);
155
- if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
156
- // If all rows were measured (when no row has "auto" as height) or all rows before the offset
157
- // were measured, then use a binary search because it's faster.
158
- return binarySearch(offset, rowsMeta.positions);
159
- }
160
-
161
- // Otherwise, use an exponential search.
162
- // If rows have "auto" as height, their positions will be based on estimated heights.
163
- // In this case, we can skip several steps until we find a position higher than the offset.
164
- // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
165
- return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
166
- }, [apiRef, currentPage.range?.firstRowIndex, currentPage.range?.lastRowIndex, rowsMeta.positions]);
167
147
  const computeRenderContext = React.useCallback(() => {
168
148
  if (!enabled) {
169
149
  return {
@@ -180,8 +160,8 @@ const useGridVirtualScroller = props => {
180
160
 
181
161
  // Clamp the value because the search may return an index out of bounds.
182
162
  // In the last index, this is not needed because Array.slice doesn't include it.
183
- const firstRowIndex = Math.min(getNearestIndexToRender(top), rowsMeta.positions.length - 1);
184
- const lastRowIndex = rootProps.autoHeight ? firstRowIndex + currentPage.rows.length : getNearestIndexToRender(top + containerDimensions.height);
163
+ const firstRowIndex = Math.min(getNearestIndexToRender(apiRef, currentPage, rowsMeta, top), rowsMeta.positions.length - 1);
164
+ const lastRowIndex = rootProps.autoHeight ? firstRowIndex + currentPage.rows.length : getNearestIndexToRender(apiRef, currentPage, rowsMeta, top + containerDimensions.height);
185
165
  let firstColumnIndex = 0;
186
166
  let lastColumnIndex = columnPositions.length;
187
167
  if (enabledForColumns) {
@@ -208,7 +188,7 @@ const useGridVirtualScroller = props => {
208
188
  firstColumnIndex,
209
189
  lastColumnIndex
210
190
  };
211
- }, [enabled, enabledForColumns, getNearestIndexToRender, rowsMeta.positions.length, rootProps.autoHeight, rootProps.rowBuffer, currentPage.rows, columnPositions, visibleColumns.length, apiRef, containerDimensions]);
191
+ }, [enabled, enabledForColumns, rowsMeta, rootProps.autoHeight, rootProps.rowBuffer, currentPage, columnPositions, visibleColumns.length, apiRef, containerDimensions]);
212
192
  (0, _utils.unstable_useEnhancedEffect)(() => {
213
193
  if (enabled) {
214
194
  // TODO a scroll reset should not be necessary
@@ -604,4 +584,24 @@ const useGridVirtualScroller = props => {
604
584
  })
605
585
  };
606
586
  };
607
- exports.useGridVirtualScroller = useGridVirtualScroller;
587
+ exports.useGridVirtualScroller = useGridVirtualScroller;
588
+ function getNearestIndexToRender(apiRef, currentPage, rowsMeta, offset) {
589
+ const lastMeasuredIndexRelativeToAllRows = apiRef.current.getLastMeasuredRowIndex();
590
+ let allRowsMeasured = lastMeasuredIndexRelativeToAllRows === Infinity;
591
+ if (currentPage.range?.lastRowIndex && !allRowsMeasured) {
592
+ // Check if all rows in this page are already measured
593
+ allRowsMeasured = lastMeasuredIndexRelativeToAllRows >= currentPage.range.lastRowIndex;
594
+ }
595
+ const lastMeasuredIndexRelativeToCurrentPage = (0, _utils2.clamp)(lastMeasuredIndexRelativeToAllRows - (currentPage.range?.firstRowIndex || 0), 0, rowsMeta.positions.length);
596
+ if (allRowsMeasured || rowsMeta.positions[lastMeasuredIndexRelativeToCurrentPage] >= offset) {
597
+ // If all rows were measured (when no row has "auto" as height) or all rows before the offset
598
+ // were measured, then use a binary search because it's faster.
599
+ return binarySearch(offset, rowsMeta.positions);
600
+ }
601
+
602
+ // Otherwise, use an exponential search.
603
+ // If rows have "auto" as height, their positions will be based on estimated heights.
604
+ // In this case, we can skip several steps until we find a position higher than the offset.
605
+ // Inspired by https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/utils/CellSizeAndPositionManager.js
606
+ return exponentialSearch(offset, rowsMeta.positions, lastMeasuredIndexRelativeToCurrentPage);
607
+ }