@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 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` [![pro](https://mui.com/r/x-pro-svg)](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` [![premium](https://mui.com/r/x-premium-svg)](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` [![pro](https://mui.com/r/x-pro-svg)](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` [![pro](https://mui.com/r/x-pro-svg)](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` [![pro](https://mui.com/r/x-pro-svg)](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` [![premium](https://mui.com/r/x-premium-svg)](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
- delimiterCharacter: clipboardCopyCellDelimiter,
80
- ignoreValueFormatter,
81
- shouldAppendQuotes: false
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
- delimiterCharacter: string;
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
- delimiterCharacter: NonNullable<GridCsvExportOptions['delimiter']>;
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, 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) {
@@ -37,7 +42,7 @@ export const serializeCellValue = (cellParams, options) => {
37
42
  } else {
38
43
  value = cellParams.formattedValue;
39
44
  }
40
- return sanitizeCellValue(value, delimiterCharacter, shouldAppendQuotes);
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.delimiterCharacter;
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.delimiterCharacter, this.options.shouldAppendQuotes);
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
- delimiterCharacter,
72
- ignoreValueFormatter,
73
- shouldAppendQuotes
76
+ csvOptions,
77
+ ignoreValueFormatter
74
78
  }) => {
75
79
  const row = new CSVRow({
76
- delimiterCharacter,
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
- shouldAppendQuotes
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
- delimiterCharacter,
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
- shouldAppendQuotes
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
- delimiterCharacter,
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
- delimiterCharacter,
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$shouldAppend;
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
- delimiterCharacter: options.delimiter || ',',
36
- includeHeaders: (_options$includeHeade = options.includeHeaders) != null ? _options$includeHeade : true,
37
- includeColumnGroupsHeaders: (_options$includeColum = options.includeColumnGroupsHeaders) != null ? _options$includeColum : true,
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, 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]);
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$range5;
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$range5 = currentPage.range) == null ? void 0 : _currentPage$range5.firstRowIndex) || 0) + firstRowToRender + i;
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
@@ -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
@@ -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
- delimiterCharacter: clipboardCopyCellDelimiter,
81
- ignoreValueFormatter: ignoreValueFormatter,
82
- shouldAppendQuotes: false
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, delimiterCharacter, shouldAppendQuotes) {
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 rows
10
- if ([delimiterCharacter, '\n', '\r', '"'].some(function (delimiter) {
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 delimiterCharacter = options.delimiterCharacter,
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, delimiterCharacter, shouldAppendQuotes);
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.delimiterCharacter;
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.delimiterCharacter, this.options.shouldAppendQuotes);
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
- delimiterCharacter = _ref.delimiterCharacter,
81
- ignoreValueFormatter = _ref.ignoreValueFormatter,
82
- shouldAppendQuotes = _ref.shouldAppendQuotes;
85
+ csvOptions = _ref.csvOptions,
86
+ ignoreValueFormatter = _ref.ignoreValueFormatter;
83
87
  var row = new CSVRow({
84
- delimiterCharacter: delimiterCharacter,
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
- shouldAppendQuotes: shouldAppendQuotes
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
- delimiterCharacter = options.delimiterCharacter,
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
- shouldAppendQuotes: shouldAppendQuotes
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
- delimiterCharacter: delimiterCharacter,
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
- delimiterCharacter: delimiterCharacter,
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$shouldAppend;
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
- delimiterCharacter: options.delimiter || ',',
39
- includeHeaders: (_options$includeHeade = options.includeHeaders) != null ? _options$includeHeade : true,
40
- includeColumnGroupsHeaders: (_options$includeColum = options.includeColumnGroupsHeaders) != null ? _options$includeColum : true,
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) {