@mui/x-data-grid 6.19.11 → 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,60 @@
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
+
6
60
  ## 6.19.11
7
61
 
8
62
  _Apr 18, 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 => {
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v6.19.11
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) {
package/legacy/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v6.19.11
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 => {
package/modern/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v6.19.11
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 => {
package/node/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/x-data-grid v6.19.11
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/x-data-grid",
3
- "version": "6.19.11",
3
+ "version": "6.20.0",
4
4
  "description": "The community edition of the data grid component (MUI X).",
5
5
  "author": "MUI Team",
6
6
  "main": "./node/index.js",