@odoo/o-spreadsheet 19.4.6 → 19.4.8

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.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.4.6
6
- * @date 2026-08-10T12:33:32.728Z
7
- * @hash 5c667fe
5
+ * @version 19.4.8
6
+ * @date 2026-08-19T09:25:13.754Z
7
+ * @hash 9dd828e
8
8
  */
9
9
 
10
10
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
@@ -4026,6 +4026,18 @@ function toString(data) {
4026
4026
  default: return "";
4027
4027
  }
4028
4028
  }
4029
+ /**
4030
+ * Only converts the decimal separator, ignore number format such as % or dates
4031
+ */
4032
+ function toLocaleString(data, locale) {
4033
+ const value = toValue(data);
4034
+ switch (typeof value) {
4035
+ case "string": return value;
4036
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
4037
+ case "boolean": return value ? "TRUE" : "FALSE";
4038
+ default: return "";
4039
+ }
4040
+ }
4029
4041
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
4030
4042
  const normalizeString = memoize(function normalizeString(str) {
4031
4043
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -5958,6 +5970,9 @@ var FunctionRegistry = class extends Registry {
5958
5970
  }
5959
5971
  };
5960
5972
  const functionRegistry = new FunctionRegistry();
5973
+ /** Formulas using one of these functions are never squished: they are always exported
5974
+ * as a full formula string, and they don't become the base formula of the next cells. */
5975
+ const NonSquishableFunctionRegistry = new Registry();
5961
5976
 
5962
5977
  //#endregion
5963
5978
  //#region src/helpers/references.ts
@@ -15476,18 +15491,21 @@ const figureRegistry = new Registry();
15476
15491
  figureRegistry.add("chart", {
15477
15492
  Component: ChartFigure,
15478
15493
  SidePanelComponent: "ChartPanel",
15479
- menuBuilder: getChartMenuActions
15494
+ menuBuilder: getChartMenuActions,
15495
+ isThemeDependant: true
15480
15496
  });
15481
15497
  figureRegistry.add("image", {
15482
15498
  Component: ImageFigure,
15483
15499
  keepRatio: true,
15484
15500
  minFigSize: 20,
15485
15501
  borderWidth: 0,
15486
- menuBuilder: getImageMenuActions
15502
+ menuBuilder: getImageMenuActions,
15503
+ isThemeDependant: false
15487
15504
  });
15488
15505
  figureRegistry.add("carousel", {
15489
15506
  Component: CarouselFigure,
15490
- menuBuilder: getCarouselMenuActions
15507
+ menuBuilder: getCarouselMenuActions,
15508
+ isThemeDependant: true
15491
15509
  });
15492
15510
 
15493
15511
  //#endregion
@@ -15516,6 +15534,9 @@ var FigureComponent = class extends Component {
15516
15534
  get isSelected() {
15517
15535
  return this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id);
15518
15536
  }
15537
+ get isThemeDependant() {
15538
+ return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
15539
+ }
15519
15540
  get figureRegistry() {
15520
15541
  return figureRegistry;
15521
15542
  }
@@ -51150,7 +51171,8 @@ var Squisher = class {
51150
51171
  * The result of this method is:
51151
51172
  * - if the cell is not a formula, returns the content as is and resets the base formula
51152
51173
  * - if the cell is a formula:
51153
- * - if there is no previous formula, or the normalized formula is different from the previous one, resets the base formula to this one and returns the full formula string
51174
+ * - if there is no previous formula, or the normalized formula is different from the previous one or it contains blacklisted functions (see NonSquishableFunctionRegistry),
51175
+ * resets the base formula to this one and returns the full formula string
51154
51176
  * - else, compares the literal values and range dependencies to the previous formula, and for each parameter:
51155
51177
  * - for numbers: returns a relative change (+N or -N) if possible, else the full number or "=" if unchanged
51156
51178
  * - for strings: returns the full string if changed, else "="
@@ -51158,6 +51180,10 @@ var Squisher = class {
51158
51180
  * */
51159
51181
  squish(cell, forSheetId) {
51160
51182
  if (cell.isFormula) {
51183
+ if (NonSquishableFunctionRegistry.getAll().some((functionName) => cell.compiledFormula.usesSymbol(functionName))) {
51184
+ this.resetBaseFormula();
51185
+ return cell.compiledFormula.toFormulaString(this.getters, this.rangeToStringOptions);
51186
+ }
51161
51187
  let numbers = [];
51162
51188
  let strings = [];
51163
51189
  let references = [];
@@ -62602,7 +62628,7 @@ var Session = class extends EventBus {
62602
62628
  }
62603
62629
  message = {
62604
62630
  ...message,
62605
- commands: this.commandSquisher.squish(revision.commands)
62631
+ commands: revision.commands
62606
62632
  };
62607
62633
  }
62608
62634
  if (this.isReplayingInitialRevisions) throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.
@@ -78018,12 +78044,13 @@ var FilterMenu = class extends Component {
78018
78044
  }
78019
78045
  sortFilterZone(sortDirection) {
78020
78046
  const filterPosition = this.props.filterPosition;
78021
- const tableZone = this.table?.range.zone;
78047
+ const table = this.table;
78048
+ const tableZone = table?.range.zone;
78022
78049
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
78023
78050
  const sheetId = this.env.model.getters.getActiveSheetId();
78024
78051
  const contentZone = {
78025
78052
  ...tableZone,
78026
- top: tableZone.top + 1
78053
+ top: tableZone.top + table.config.numberOfHeaders
78027
78054
  };
78028
78055
  const sortAnchor = {
78029
78056
  col: filterPosition.col,
@@ -80280,7 +80307,7 @@ const ARRAYTOTEXT = {
80280
80307
  else if (_format === 0) {
80281
80308
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
80282
80309
  return transposeMatrix(_array).flatMap((row) => row.map((value) => {
80283
- return isEvaluationError(value.value) ? value.value : toString(value);
80310
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
80284
80311
  })).join(rowSeparator);
80285
80312
  } else return new EvaluationError(_t("Format must be 0 or 1"));
80286
80313
  },
@@ -86046,7 +86073,7 @@ const CONCAT = {
86046
86073
  description: _t("Concatenation of two values."),
86047
86074
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
86048
86075
  compute: function(value1, value2) {
86049
- return toString(value1) + toString(value2);
86076
+ return toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale);
86050
86077
  },
86051
86078
  isExported: true
86052
86079
  };
@@ -86929,7 +86956,7 @@ const CLEAN = {
86929
86956
  description: _t("Remove non-printable characters from a piece of text."),
86930
86957
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
86931
86958
  compute: function(text) {
86932
- const _text = toString(text);
86959
+ const _text = toLocaleString(text, this.locale);
86933
86960
  let cleanedStr = "";
86934
86961
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
86935
86962
  return cleanedStr;
@@ -86940,7 +86967,7 @@ const CONCATENATE = {
86940
86967
  description: _t("Appends strings to one another."),
86941
86968
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
86942
86969
  compute: function(...datas) {
86943
- return reduceAny(datas, (acc, a) => acc + toString(a), "");
86970
+ return reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "");
86944
86971
  },
86945
86972
  isExported: true
86946
86973
  };
@@ -86948,7 +86975,7 @@ const EXACT = {
86948
86975
  description: _t("Tests whether two strings are identical."),
86949
86976
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
86950
86977
  compute: function(string1, string2) {
86951
- return toString(string1) === toString(string2);
86978
+ return toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale);
86952
86979
  },
86953
86980
  isExported: true
86954
86981
  };
@@ -86960,8 +86987,8 @@ const FIND = {
86960
86987
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
86961
86988
  ],
86962
86989
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
86963
- const _searchFor = toString(searchFor);
86964
- const _textToSearch = toString(textToSearch);
86990
+ const _searchFor = toLocaleString(searchFor, this.locale);
86991
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
86965
86992
  const _startingAt = toNumber(startingAt, this.locale);
86966
86993
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
86967
86994
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -86998,8 +87025,8 @@ const JOIN = {
86998
87025
  description: _t("Concatenates elements of arrays with delimiter."),
86999
87026
  args: [arg("delimiter (string)", _t("The character or string to place between each concatenated value.")), arg("value_or_array (string, range<string>, repeating)", _t("Value to be appended using delimiter."))],
87000
87027
  compute: function(delimiter, ...valuesOrArrays) {
87001
- const _delimiter = toString(delimiter);
87002
- return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "");
87028
+ const _delimiter = toLocaleString(delimiter, this.locale);
87029
+ return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "");
87003
87030
  }
87004
87031
  };
87005
87032
  const LEFT = {
@@ -87008,7 +87035,7 @@ const LEFT = {
87008
87035
  compute: function(text, ...args) {
87009
87036
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
87010
87037
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
87011
- return toString(text).substring(0, _numberOfCharacters);
87038
+ return toLocaleString(text, this.locale).substring(0, _numberOfCharacters);
87012
87039
  },
87013
87040
  isExported: true
87014
87041
  };
@@ -87016,7 +87043,7 @@ const LEN = {
87016
87043
  description: _t("Length of a string."),
87017
87044
  args: [arg("text (string)", _t("The string whose length will be returned."))],
87018
87045
  compute: function(text) {
87019
- return toString(text).length;
87046
+ return toLocaleString(text, this.locale).length;
87020
87047
  },
87021
87048
  isExported: true
87022
87049
  };
@@ -87024,7 +87051,7 @@ const LOWER = {
87024
87051
  description: _t("Converts a specified string to lowercase."),
87025
87052
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
87026
87053
  compute: function(text) {
87027
- return toString(text).toLowerCase();
87054
+ return toLocaleString(text, this.locale).toLowerCase();
87028
87055
  },
87029
87056
  isExported: true
87030
87057
  };
@@ -87036,7 +87063,7 @@ const MID = {
87036
87063
  arg("extract_length (number)", _t("The length of the segment to extract."))
87037
87064
  ],
87038
87065
  compute: function(text, starting_at, extract_length) {
87039
- const _text = toString(text);
87066
+ const _text = toLocaleString(text, this.locale);
87040
87067
  const _starting_at = toNumber(starting_at, this.locale);
87041
87068
  const _extract_length = toNumber(extract_length, this.locale);
87042
87069
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -87049,7 +87076,7 @@ const PROPER = {
87049
87076
  description: _t("Capitalizes each word in a specified string."),
87050
87077
  args: [arg("text_to_capitalize (string)", _t("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase."))],
87051
87078
  compute: function(text) {
87052
- return toString(text).replace(wordRegex, (word) => {
87079
+ return toLocaleString(text, this.locale).replace(wordRegex, (word) => {
87053
87080
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
87054
87081
  });
87055
87082
  },
@@ -87199,8 +87226,8 @@ const REPLACE = {
87199
87226
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
87200
87227
  const _length = toNumber(length, this.locale);
87201
87228
  if (_length < 0) return new EvaluationError(_t("The length (%s) must be positive or zero.", _length));
87202
- const _text = toString(text);
87203
- const _newText = toString(newText);
87229
+ const _text = toLocaleString(text, this.locale);
87230
+ const _newText = toLocaleString(newText, this.locale);
87204
87231
  return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
87205
87232
  },
87206
87233
  isExported: true
@@ -87211,7 +87238,7 @@ const RIGHT = {
87211
87238
  compute: function(text, ...args) {
87212
87239
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
87213
87240
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or zero.", _numberOfCharacters));
87214
- const _text = toString(text);
87241
+ const _text = toLocaleString(text, this.locale);
87215
87242
  const stringLength = _text.length;
87216
87243
  return _text.substring(stringLength - _numberOfCharacters, stringLength);
87217
87244
  },
@@ -87225,8 +87252,8 @@ const SEARCH = {
87225
87252
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
87226
87253
  ],
87227
87254
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
87228
- const _searchFor = toString(searchFor).toLowerCase();
87229
- const _textToSearch = toString(textToSearch).toLowerCase();
87255
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
87256
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
87230
87257
  const _startingAt = toNumber(startingAt, this.locale);
87231
87258
  if (_textToSearch === "") return {
87232
87259
  value: CellErrorType.GenericError,
@@ -87256,8 +87283,8 @@ const SPLIT = {
87256
87283
  arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t("Whether or not to remove empty text messages from the split results. The default behavior is to treat consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters."))
87257
87284
  ],
87258
87285
  compute: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
87259
- const _text = toString(text);
87260
- const _delimiter = escapeRegExp(toString(delimiter));
87286
+ const _text = toLocaleString(text, this.locale);
87287
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
87261
87288
  const _splitByEach = toBoolean(splitByEach);
87262
87289
  const _removeEmptyText = toBoolean(removeEmptyText);
87263
87290
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -87279,10 +87306,10 @@ const SUBSTITUTE = {
87279
87306
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
87280
87307
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
87281
87308
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
87282
- const _textToSearch = toString(textToSearch);
87283
- const _searchFor = toString(searchFor);
87309
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
87310
+ const _searchFor = toLocaleString(searchFor, this.locale);
87284
87311
  if (_searchFor === "") return _textToSearch;
87285
- const _replaceWith = toString(replaceWith);
87312
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
87286
87313
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
87287
87314
  if (_occurrenceNumber === 0) return _textToSearch.replace(reg, _replaceWith);
87288
87315
  let n = 0;
@@ -87305,10 +87332,10 @@ const TEXTJOIN = {
87305
87332
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
87306
87333
  ],
87307
87334
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
87308
- const _delimiter = toString(delimiter);
87335
+ const _delimiter = toLocaleString(delimiter, this.locale);
87309
87336
  const _ignoreEmpty = toBoolean(ignoreEmpty);
87310
87337
  let n = 0;
87311
- return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "");
87338
+ return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "");
87312
87339
  },
87313
87340
  isExported: true
87314
87341
  };
@@ -87361,7 +87388,7 @@ const TRIM = {
87361
87388
  description: _t("Removes space characters."),
87362
87389
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
87363
87390
  compute: function(text) {
87364
- return trimContent(toString(text));
87391
+ return trimContent(toLocaleString(text, this.locale));
87365
87392
  },
87366
87393
  isExported: true
87367
87394
  };
@@ -87369,7 +87396,7 @@ const UPPER = {
87369
87396
  description: _t("Converts a specified string to uppercase."),
87370
87397
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
87371
87398
  compute: function(text) {
87372
- return toString(text).toUpperCase();
87399
+ return toLocaleString(text, this.locale).toUpperCase();
87373
87400
  },
87374
87401
  isExported: true
87375
87402
  };
@@ -87466,7 +87493,7 @@ const HYPERLINK = {
87466
87493
  args: [arg("url (string)", _t("The full URL of the link enclosed in quotation marks.")), arg("link_label (string, optional)", _t("The text to display in the cell, enclosed in quotation marks."))],
87467
87494
  compute: function(url, linkLabel) {
87468
87495
  const processedUrl = toString(url).trim();
87469
- const processedLabel = toString(linkLabel) || processedUrl;
87496
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
87470
87497
  if (processedUrl === "") return processedLabel;
87471
87498
  return markdownLink(processedLabel, processedUrl);
87472
87499
  },
@@ -87612,7 +87639,8 @@ const registries = {
87612
87639
  migrationStepRegistry,
87613
87640
  chartJsExtensionRegistry,
87614
87641
  onIterationEndEvaluationRegistry,
87615
- specificRangeTransformRegistry
87642
+ specificRangeTransformRegistry,
87643
+ NonSquishableFunctionRegistry
87616
87644
  };
87617
87645
  const helpers = {
87618
87646
  arg,
@@ -87914,6 +87942,6 @@ exports.stores = stores;
87914
87942
  exports.tokenColors = tokenColors;
87915
87943
  exports.tokenize = tokenize;
87916
87944
 
87917
- __info__.version = "19.4.6";
87918
- __info__.date = "2026-08-10T12:33:32.728Z";
87919
- __info__.hash = "5c667fe";
87945
+ __info__.version = "19.4.8";
87946
+ __info__.date = "2026-08-19T09:25:13.754Z";
87947
+ __info__.hash = "9dd828e";
@@ -2,9 +2,9 @@
2
2
  /*
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.4.6
6
- * @date 2026-08-10T12:33:34.157Z
7
- * @hash 5c667fe
5
+ * @version 19.4.8
6
+ * @date 2026-08-19T09:25:15.535Z
7
+ * @hash 9dd828e
8
8
  */
9
9
  :root {
10
10
  --os-gray-100: light-dark(#f9fafb, #1b1d26);
@@ -2565,6 +2565,22 @@
2565
2565
  }
2566
2566
  }
2567
2567
 
2568
+ /* Originates from src/components/figures/figure_image/figure_image.css */
2569
+ .o-spreadsheet {
2570
+ .o-figure-menu-item {
2571
+ cursor: pointer;
2572
+ color: var(--os-gray-900) !important;
2573
+ height: 20px;
2574
+ width: 20px;
2575
+ &.active,
2576
+ &:hover,
2577
+ &:target {
2578
+ cursor: pointer;
2579
+ background-color: var(--os-background-gray-color-hover);
2580
+ }
2581
+ }
2582
+ }
2583
+
2568
2584
  /* Originates from src/components/figures/figure_container/figure_container.css */
2569
2585
  .o-spreadsheet {
2570
2586
  .o-figure-snap-line {