@odoo/o-spreadsheet 18.1.0-alpha.0 → 18.1.0-alpha.1

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,15 +2,121 @@
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 18.1.0-alpha.0
6
- * @date 2024-09-25T13:17:49.636Z
7
- * @hash 288f0b7
5
+ * @version 18.1.0-alpha.1
6
+ * @date 2024-10-14T07:53:17.717Z
7
+ * @hash e9ce3aa
8
8
  */
9
9
 
10
10
  'use strict';
11
11
 
12
12
  var owl = require('@odoo/owl');
13
13
 
14
+ function createActions(menuItems) {
15
+ return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
16
+ }
17
+ let nextItemId = 1;
18
+ function createAction(item) {
19
+ const name = item.name;
20
+ const children = item.children;
21
+ const description = item.description;
22
+ const icon = item.icon;
23
+ const secondaryIcon = item.secondaryIcon;
24
+ const itemId = item.id || nextItemId++;
25
+ return {
26
+ id: itemId.toString(),
27
+ name: typeof name === "function" ? name : () => name,
28
+ isVisible: item.isVisible ? item.isVisible : () => true,
29
+ isEnabled: item.isEnabled ? item.isEnabled : () => true,
30
+ isActive: item.isActive,
31
+ execute: item.execute,
32
+ children: children
33
+ ? (env) => {
34
+ return children
35
+ .map((child) => (typeof child === "function" ? child(env) : child))
36
+ .flat()
37
+ .map(createAction);
38
+ }
39
+ : () => [],
40
+ isReadonlyAllowed: item.isReadonlyAllowed || false,
41
+ separator: item.separator || false,
42
+ icon: typeof icon === "function" ? icon : () => icon || "",
43
+ iconColor: item.iconColor,
44
+ secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
45
+ description: typeof description === "function" ? description : () => description || "",
46
+ textColor: item.textColor,
47
+ sequence: item.sequence || 0,
48
+ onStartHover: item.onStartHover,
49
+ onStopHover: item.onStopHover,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Registry
55
+ *
56
+ * The Registry class is basically just a mapping from a string key to an object.
57
+ * It is really not much more than an object. It is however useful for the
58
+ * following reasons:
59
+ *
60
+ * 1. it let us react and execute code when someone add something to the registry
61
+ * (for example, the FunctionRegistry subclass this for this purpose)
62
+ * 2. it throws an error when the get operation fails
63
+ * 3. it provides a chained API to add items to the registry.
64
+ */
65
+ class Registry {
66
+ content = {};
67
+ /**
68
+ * Add an item to the registry
69
+ *
70
+ * Note that this also returns the registry, so another add method call can
71
+ * be chained
72
+ */
73
+ add(key, value) {
74
+ this.content[key] = value;
75
+ return this;
76
+ }
77
+ /**
78
+ * Get an item from the registry
79
+ */
80
+ get(key) {
81
+ /**
82
+ * Note: key in {} is ~12 times slower than {}[key].
83
+ * So, we check the absence of key only when the direct access returns
84
+ * a falsy value. It's done to ensure that the registry can contains falsy values
85
+ */
86
+ const content = this.content[key];
87
+ if (!content) {
88
+ if (!(key in this.content)) {
89
+ throw new Error(`Cannot find ${key} in this registry!`);
90
+ }
91
+ }
92
+ return content;
93
+ }
94
+ /**
95
+ * Check if the key is already in the registry
96
+ */
97
+ contains(key) {
98
+ return key in this.content;
99
+ }
100
+ /**
101
+ * Get a list of all elements in the registry
102
+ */
103
+ getAll() {
104
+ return Object.values(this.content);
105
+ }
106
+ /**
107
+ * Get a list of all keys in the registry
108
+ */
109
+ getKeys() {
110
+ return Object.keys(this.content);
111
+ }
112
+ /**
113
+ * Remove an item from the registry
114
+ */
115
+ remove(key) {
116
+ delete this.content[key];
117
+ }
118
+ }
119
+
14
120
  const CANVAS_SHIFT = 0.5;
15
121
  // Colors
16
122
  const HIGHLIGHT_COLOR = "#37A850";
@@ -5970,18 +6076,9 @@ function drawDecoratedText(context, text, position, underline = false, strikethr
5970
6076
  * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
5971
6077
  * */
5972
6078
  class UuidGenerator {
5973
- isFastIdStrategy = false;
5974
- fastIdStart = 0;
5975
- setIsFastStrategy(isFast) {
5976
- this.isFastIdStrategy = isFast;
5977
- }
5978
6079
  uuidv4() {
5979
- if (this.isFastIdStrategy) {
5980
- this.fastIdStart++;
5981
- return String(this.fastIdStart);
5982
- //@ts-ignore
5983
- }
5984
- else if (window.crypto && window.crypto.getRandomValues) {
6080
+ //@ts-ignore
6081
+ if (window.crypto && window.crypto.getRandomValues) {
5985
6082
  //@ts-ignore
5986
6083
  return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
5987
6084
  }
@@ -5995,111 +6092,6 @@ class UuidGenerator {
5995
6092
  }
5996
6093
  }
5997
6094
 
5998
- function createActions(menuItems) {
5999
- return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
6000
- }
6001
- const uuidGenerator$1 = new UuidGenerator();
6002
- function createAction(item) {
6003
- const name = item.name;
6004
- const children = item.children;
6005
- const description = item.description;
6006
- const icon = item.icon;
6007
- const secondaryIcon = item.secondaryIcon;
6008
- return {
6009
- id: item.id || uuidGenerator$1.uuidv4(),
6010
- name: typeof name === "function" ? name : () => name,
6011
- isVisible: item.isVisible ? item.isVisible : () => true,
6012
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
6013
- isActive: item.isActive,
6014
- execute: item.execute,
6015
- children: children
6016
- ? (env) => {
6017
- return children
6018
- .map((child) => (typeof child === "function" ? child(env) : child))
6019
- .flat()
6020
- .map(createAction);
6021
- }
6022
- : () => [],
6023
- isReadonlyAllowed: item.isReadonlyAllowed || false,
6024
- separator: item.separator || false,
6025
- icon: typeof icon === "function" ? icon : () => icon || "",
6026
- iconColor: item.iconColor,
6027
- secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
6028
- description: typeof description === "function" ? description : () => description || "",
6029
- textColor: item.textColor,
6030
- sequence: item.sequence || 0,
6031
- onStartHover: item.onStartHover,
6032
- onStopHover: item.onStopHover,
6033
- };
6034
- }
6035
-
6036
- /**
6037
- * Registry
6038
- *
6039
- * The Registry class is basically just a mapping from a string key to an object.
6040
- * It is really not much more than an object. It is however useful for the
6041
- * following reasons:
6042
- *
6043
- * 1. it let us react and execute code when someone add something to the registry
6044
- * (for example, the FunctionRegistry subclass this for this purpose)
6045
- * 2. it throws an error when the get operation fails
6046
- * 3. it provides a chained API to add items to the registry.
6047
- */
6048
- class Registry {
6049
- content = {};
6050
- /**
6051
- * Add an item to the registry
6052
- *
6053
- * Note that this also returns the registry, so another add method call can
6054
- * be chained
6055
- */
6056
- add(key, value) {
6057
- this.content[key] = value;
6058
- return this;
6059
- }
6060
- /**
6061
- * Get an item from the registry
6062
- */
6063
- get(key) {
6064
- /**
6065
- * Note: key in {} is ~12 times slower than {}[key].
6066
- * So, we check the absence of key only when the direct access returns
6067
- * a falsy value. It's done to ensure that the registry can contains falsy values
6068
- */
6069
- const content = this.content[key];
6070
- if (!content) {
6071
- if (!(key in this.content)) {
6072
- throw new Error(`Cannot find ${key} in this registry!`);
6073
- }
6074
- }
6075
- return content;
6076
- }
6077
- /**
6078
- * Check if the key is already in the registry
6079
- */
6080
- contains(key) {
6081
- return key in this.content;
6082
- }
6083
- /**
6084
- * Get a list of all elements in the registry
6085
- */
6086
- getAll() {
6087
- return Object.values(this.content);
6088
- }
6089
- /**
6090
- * Get a list of all keys in the registry
6091
- */
6092
- getKeys() {
6093
- return Object.keys(this.content);
6094
- }
6095
- /**
6096
- * Remove an item from the registry
6097
- */
6098
- remove(key) {
6099
- delete this.content[key];
6100
- }
6101
- }
6102
-
6103
6095
  function getClipboardDataPositions(sheetId, zones) {
6104
6096
  const lefts = new Set(zones.map((z) => z.left));
6105
6097
  const rights = new Set(zones.map((z) => z.right));
@@ -8454,31 +8446,34 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8454
8446
  for (let col of columnsIndexes) {
8455
8447
  const position = { col, row, sheetId };
8456
8448
  const table = this.getters.getTable(position);
8457
- if (!table || copiedTablesIds.has(table.id)) {
8449
+ if (!table) {
8458
8450
  tableCellsInRow.push({});
8459
8451
  continue;
8460
8452
  }
8461
8453
  const coreTable = this.getters.getCoreTable(position);
8462
8454
  const tableZone = coreTable?.range.zone;
8455
+ let copiedTable = undefined;
8463
8456
  // Copy whole table
8464
- if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
8465
- copiedTablesIds.add(coreTable.id);
8457
+ if (!copiedTablesIds.has(table.id) &&
8458
+ coreTable &&
8459
+ tableZone &&
8460
+ zones.some((z) => isZoneInside(tableZone, z))) {
8461
+ copiedTablesIds.add(table.id);
8466
8462
  const values = [];
8467
8463
  for (const col of range(tableZone.left, tableZone.right + 1)) {
8468
8464
  values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
8469
8465
  }
8470
- tableCellsInRow.push({
8471
- table: {
8472
- range: coreTable.range.rangeData,
8473
- config: coreTable.config,
8474
- type: coreTable.type,
8475
- },
8476
- });
8477
- }
8478
- // Copy only style of cell
8479
- else if (table) {
8480
- tableCellsInRow.push({ style: this.getTableStyleToCopy(position) });
8466
+ copiedTable = {
8467
+ range: coreTable.range.rangeData,
8468
+ config: coreTable.config,
8469
+ type: coreTable.type,
8470
+ };
8481
8471
  }
8472
+ tableCellsInRow.push({
8473
+ table: copiedTable,
8474
+ style: this.getTableStyleToCopy(position),
8475
+ isWholeTableCopied: copiedTablesIds.has(table.id),
8476
+ });
8482
8477
  }
8483
8478
  }
8484
8479
  return {
@@ -8559,11 +8554,14 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8559
8554
  tableType: tableCell.table.type,
8560
8555
  });
8561
8556
  }
8562
- // Do not paste table style if we're inside another table
8563
8557
  // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
8564
8558
  // dynamic tables are not yet computed
8565
- if (!this.getters.getCoreTable(position)) {
8566
- if (tableCell.style?.style && options?.pasteOption !== "asValue") {
8559
+ if (this.getters.getCoreTable(position) || options?.pasteOption === "asValue") {
8560
+ return;
8561
+ }
8562
+ if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||
8563
+ options?.pasteOption === "onlyFormat") {
8564
+ if (tableCell.style?.style) {
8567
8565
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
8568
8566
  }
8569
8567
  if (tableCell.style?.border) {
@@ -9283,6 +9281,49 @@ function getDefinedAxis(definition) {
9283
9281
  useLeftAxis ||= !useRightAxis;
9284
9282
  return { useLeftAxis, useRightAxis };
9285
9283
  }
9284
+ function getChartAxis(definition, position, type, options) {
9285
+ const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);
9286
+ if ((position === "left" && !useLeftAxis) || (position === "right" && !useRightAxis)) {
9287
+ return undefined;
9288
+ }
9289
+ const fontColor = chartFontColor(definition.background);
9290
+ let design;
9291
+ if (position === "bottom") {
9292
+ design = definition.axesDesign?.x;
9293
+ }
9294
+ else if (position === "left") {
9295
+ design = definition.axesDesign?.y;
9296
+ }
9297
+ else {
9298
+ design = definition.axesDesign?.y1;
9299
+ }
9300
+ if (type === "values") {
9301
+ const displayGridLines = position === "left" || (position === "right" && !useLeftAxis);
9302
+ return {
9303
+ position: position,
9304
+ title: getChartAxisTitleRuntime(design),
9305
+ grid: {
9306
+ display: displayGridLines,
9307
+ },
9308
+ beginAtZero: true,
9309
+ stacked: options?.stacked,
9310
+ ticks: {
9311
+ color: fontColor,
9312
+ callback: formatTickValue(options),
9313
+ },
9314
+ };
9315
+ }
9316
+ else {
9317
+ return {
9318
+ ticks: {
9319
+ padding: 5,
9320
+ color: fontColor,
9321
+ },
9322
+ stacked: options?.stacked,
9323
+ title: getChartAxisTitleRuntime(design),
9324
+ };
9325
+ }
9326
+ }
9286
9327
  function computeChartPadding({ displayTitle, displayLegend, }) {
9287
9328
  let top = 25;
9288
9329
  if (displayTitle) {
@@ -9369,6 +9410,12 @@ function interpolateData(config, values, labels, newLabels) {
9369
9410
  return [];
9370
9411
  }
9371
9412
  }
9413
+ function formatChartDatasetValue(axisFormats, locale) {
9414
+ return (value, axisId) => {
9415
+ const format = axisId ? axisFormats?.[axisId] : undefined;
9416
+ return formatTickValue({ format, locale })(value);
9417
+ };
9418
+ }
9372
9419
  function formatTickValue(localeFormat) {
9373
9420
  return (value) => {
9374
9421
  value = Number(value);
@@ -9431,10 +9478,12 @@ const chartShowValuesPlugin = {
9431
9478
  case "bar":
9432
9479
  case "line": {
9433
9480
  const yOffset = dataset.type === "bar" && !options.horizontal ? 0 : 3;
9481
+ const horizontalChart = dataset.type === "bar" && options.horizontal;
9482
+ const axisId = horizontalChart ? dataset.xAxisID : dataset.yAxisID;
9434
9483
  for (let i = 0; i < dataset._parsed.length; i++) {
9435
9484
  const point = dataset.data[i];
9436
9485
  const value = options.horizontal ? dataset._parsed[i].x : dataset._parsed[i].y;
9437
- const displayedValue = options.callback(value - 0);
9486
+ const displayedValue = options.callback(value - 0, axisId);
9438
9487
  let xPosition = 0, yPosition = 0;
9439
9488
  if (options.horizontal) {
9440
9489
  yPosition = point.y;
@@ -17843,7 +17892,7 @@ function assertDomainLength(domain) {
17843
17892
  throw new EvaluationError(_t("Function PIVOT takes an even number of arguments."));
17844
17893
  }
17845
17894
  }
17846
- function addPivotDependencies(evalContext, coreDefinition) {
17895
+ function addPivotDependencies(evalContext, coreDefinition, forMeasures) {
17847
17896
  //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
17848
17897
  const dependencies = [];
17849
17898
  if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
@@ -17855,7 +17904,7 @@ function addPivotDependencies(evalContext, coreDefinition) {
17855
17904
  }
17856
17905
  dependencies.push(range);
17857
17906
  }
17858
- for (const measure of coreDefinition.measures) {
17907
+ for (const measure of forMeasures) {
17859
17908
  if (measure.computedBy) {
17860
17909
  const formula = evalContext.getters.getMeasureCompiledFormula(measure);
17861
17910
  dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
@@ -18288,7 +18337,7 @@ const PIVOT_VALUE = {
18288
18337
  assertDomainLength(domainArgs);
18289
18338
  const pivot = this.getters.getPivot(pivotId);
18290
18339
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18291
- addPivotDependencies(this, coreDefinition);
18340
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures.filter((m) => m.id === _measure));
18292
18341
  pivot.init({ reload: pivot.needsReevaluation });
18293
18342
  const error = pivot.assertIsValid({ throwOnError: false });
18294
18343
  if (error) {
@@ -18318,7 +18367,7 @@ const PIVOT_HEADER = {
18318
18367
  assertDomainLength(domainArgs);
18319
18368
  const pivot = this.getters.getPivot(_pivotId);
18320
18369
  const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
18321
- addPivotDependencies(this, coreDefinition);
18370
+ addPivotDependencies(this, coreDefinition, []);
18322
18371
  pivot.init({ reload: pivot.needsReevaluation });
18323
18372
  const error = pivot.assertIsValid({ throwOnError: false });
18324
18373
  if (error) {
@@ -18369,7 +18418,7 @@ const PIVOT = {
18369
18418
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
18370
18419
  const pivot = this.getters.getPivot(pivotId);
18371
18420
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18372
- addPivotDependencies(this, coreDefinition);
18421
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures);
18373
18422
  pivot.init({ reload: pivot.needsReevaluation });
18374
18423
  const error = pivot.assertIsValid({ throwOnError: false });
18375
18424
  if (error) {
@@ -20165,7 +20214,7 @@ class ContentEditableHelper {
20165
20214
  let startNode = this.findChildAtCharacterIndex(start);
20166
20215
  let endNode = this.findChildAtCharacterIndex(end);
20167
20216
  range.setStart(startNode.node, startNode.offset);
20168
- selection.extend(endNode.node, endNode.offset);
20217
+ range.setEnd(endNode.node, endNode.offset);
20169
20218
  }
20170
20219
  }
20171
20220
  /**
@@ -20663,7 +20712,7 @@ class Composer extends owl.Component {
20663
20712
  "Alt+Enter": this.processNewLineEvent,
20664
20713
  "Ctrl+Enter": this.processNewLineEvent,
20665
20714
  Escape: this.processEscapeKey,
20666
- F2: () => console.warn("Not implemented"),
20715
+ F2: (ev) => this.toggleEditionMode(ev),
20667
20716
  F4: (ev) => this.processF4Key(ev),
20668
20717
  Tab: (ev) => this.processTabKey(ev, "right"),
20669
20718
  "Shift+Tab": (ev) => this.processTabKey(ev, "left"),
@@ -20782,6 +20831,11 @@ class Composer extends owl.Component {
20782
20831
  this.props.composerStore.cycleReferences();
20783
20832
  this.processContent();
20784
20833
  }
20834
+ toggleEditionMode(ev) {
20835
+ ev.stopPropagation();
20836
+ this.props.composerStore.toggleEditionMode();
20837
+ this.processContent();
20838
+ }
20785
20839
  processNumpadDecimal(ev) {
20786
20840
  ev.stopPropagation();
20787
20841
  ev.preventDefault();
@@ -21007,7 +21061,13 @@ class Composer extends owl.Component {
21007
21061
  break;
21008
21062
  case "REFERENCE":
21009
21063
  const { xc, sheetName } = splitReference(token.value);
21010
- result.push({ value: token.value, color: this.rangeColor(xc, sheetName) || "#000" });
21064
+ result.push({
21065
+ value: token.value,
21066
+ color: this.rangeColor(xc, sheetName) || "#000",
21067
+ class: tokenAtCursor === token && this.props.composerStore.editionMode === "selecting"
21068
+ ? "text-decoration-underline"
21069
+ : undefined,
21070
+ });
21011
21071
  break;
21012
21072
  case "SYMBOL":
21013
21073
  const value = token.value;
@@ -21578,6 +21638,7 @@ function insertTokenAfterArgSeparator(tokenAtCursor, value) {
21578
21638
  // replace the whole token
21579
21639
  start = tokenAtCursor.start;
21580
21640
  }
21641
+ this.composer.stopComposerRangeSelection();
21581
21642
  this.composer.changeComposerCursorSelection(start, end);
21582
21643
  this.composer.replaceComposerCursorSelection(value);
21583
21644
  }
@@ -21595,6 +21656,7 @@ function insertTokenAfterLeftParenthesis(tokenAtCursor, value) {
21595
21656
  // replace the whole token
21596
21657
  start = tokenAtCursor.start;
21597
21658
  }
21659
+ this.composer.stopComposerRangeSelection();
21598
21660
  this.composer.changeComposerCursorSelection(start, end);
21599
21661
  this.composer.replaceComposerCursorSelection(value);
21600
21662
  }
@@ -22187,6 +22249,27 @@ autofillModifiersRegistry
22187
22249
  tooltip: content ? { props: { content: tooltipValue } } : undefined,
22188
22250
  };
22189
22251
  },
22252
+ })
22253
+ .add("DATE_INCREMENT_MODIFIER", {
22254
+ apply: (rule, data, getters) => {
22255
+ const date = toJsDate(rule.current, getters.getLocale());
22256
+ date.setFullYear(date.getFullYear() + rule.increment.years || 0);
22257
+ date.setMonth(date.getMonth() + rule.increment.months || 0);
22258
+ date.setDate(date.getDate() + rule.increment.days || 0);
22259
+ const value = jsDateToNumber(date);
22260
+ rule.current = value;
22261
+ const locale = getters.getLocale();
22262
+ const tooltipValue = formatValue(value, { format: data.cell?.format, locale });
22263
+ return {
22264
+ cellData: {
22265
+ border: data.border,
22266
+ style: data.cell && data.cell.style,
22267
+ format: data.cell && data.cell.format,
22268
+ content: value.toString(),
22269
+ },
22270
+ tooltip: value ? { props: { content: tooltipValue } } : undefined,
22271
+ };
22272
+ },
22190
22273
  })
22191
22274
  .add("COPY_MODIFIER", {
22192
22275
  apply: (rule, data, getters) => {
@@ -22267,7 +22350,9 @@ function getGroup(cell, cells, filter) {
22267
22350
  if (x === cell) {
22268
22351
  found = true;
22269
22352
  }
22270
- const cellValue = x === undefined || x.isFormula ? undefined : evaluateLiteral(x, { locale: DEFAULT_LOCALE });
22353
+ const cellValue = x === undefined || x.isFormula
22354
+ ? undefined
22355
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22271
22356
  if (cellValue && filter(cellValue)) {
22272
22357
  group.push(cellValue);
22273
22358
  }
@@ -22303,6 +22388,72 @@ function calculateIncrementBasedOnGroup(group) {
22303
22388
  }
22304
22389
  return increment;
22305
22390
  }
22391
+ /**
22392
+ * Iterates on a list of date intervals.
22393
+ * if every interval is the same, return the interval
22394
+ * Otherwise return undefined
22395
+ *
22396
+ */
22397
+ function getEqualInterval(intervals) {
22398
+ if (intervals.length < 2) {
22399
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22400
+ }
22401
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22402
+ interval.months === intervals[0].months &&
22403
+ interval.days === intervals[0].days);
22404
+ return equal ? intervals[0] : undefined;
22405
+ }
22406
+ /**
22407
+ * Based on a group of dates, calculate the increment that should be applied
22408
+ * to the next date.
22409
+ *
22410
+ * This will compute the date difference in calendar terms (years, months, days)
22411
+ * In order to make abstraction of leap years and months with different number of days.
22412
+ *
22413
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22414
+ * In case of equidistant dates, we either have in that order:
22415
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22416
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22417
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22418
+ *
22419
+ * */
22420
+ function calculateDateIncrementBasedOnGroup(group) {
22421
+ if (group.length < 2) {
22422
+ return 1;
22423
+ }
22424
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22425
+ const datesIntervals = getDateIntervals(jsDates);
22426
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22427
+ if (datesEquidistantInterval === undefined) {
22428
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22429
+ return undefined;
22430
+ }
22431
+ // The dates are apart by an exact interval of years, months or days
22432
+ // but not a combination of them
22433
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22434
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22435
+ if (!exactDateInterval || isSameDay) {
22436
+ const timeIntervals = jsDates
22437
+ .map((date, index) => {
22438
+ if (index === 0) {
22439
+ return 0;
22440
+ }
22441
+ const previous = jsDates[index - 1];
22442
+ const days = Math.floor(date.getTime()) - Math.floor(previous.getTime());
22443
+ return days;
22444
+ })
22445
+ .slice(1);
22446
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22447
+ if (equidistantDates) {
22448
+ return group.length * (group[1] - group[0]);
22449
+ }
22450
+ }
22451
+ return {
22452
+ years: datesEquidistantInterval.years * group.length,
22453
+ months: datesEquidistantInterval.months * group.length,
22454
+ days: datesEquidistantInterval.days * group.length,
22455
+ };
22456
+ }
22306
22457
  autofillRulesRegistry
22307
22458
  .add("simple_value_copy", {
22308
22459
  condition: (cell, cells) => {
@@ -22350,12 +22501,47 @@ autofillRulesRegistry
22350
22501
  return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22351
22502
  },
22352
22503
  sequence: 30,
22504
+ })
22505
+ .add("increment_dates", {
22506
+ condition: (cell, cells) => {
22507
+ return (!cell.isFormula &&
22508
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22509
+ !!cell.format &&
22510
+ isDateTimeFormat(cell.format));
22511
+ },
22512
+ generateRule: (cell, cells) => {
22513
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22514
+ !!evaluatedCell.format &&
22515
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22516
+ const increment = calculateDateIncrementBasedOnGroup(group);
22517
+ if (increment === undefined) {
22518
+ return { type: "COPY_MODIFIER" };
22519
+ }
22520
+ /** requires to detect the current date (requires to be an integer value with the right format)
22521
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22522
+ */
22523
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22524
+ if (typeof increment === "object") {
22525
+ return {
22526
+ type: "DATE_INCREMENT_MODIFIER",
22527
+ increment,
22528
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22529
+ };
22530
+ }
22531
+ return {
22532
+ type: "INCREMENT_MODIFIER",
22533
+ increment,
22534
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22535
+ };
22536
+ },
22537
+ sequence: 25,
22353
22538
  })
22354
22539
  .add("increment_number", {
22355
22540
  condition: (cell) => !cell.isFormula &&
22356
22541
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22357
22542
  generateRule: (cell, cells) => {
22358
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
22543
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22544
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22359
22545
  const increment = calculateIncrementBasedOnGroup(group);
22360
22546
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22361
22547
  return {
@@ -22366,6 +22552,37 @@ autofillRulesRegistry
22366
22552
  },
22367
22553
  sequence: 40,
22368
22554
  });
22555
+ /**
22556
+ * Returns the date intervals between consecutive dates of an array
22557
+ * in the format of { years: number, months: number, days: number }
22558
+ *
22559
+ * The split is necessary to make abstraction of leap years and
22560
+ * months with different number of days.
22561
+ *
22562
+ * @param dates
22563
+ */
22564
+ function getDateIntervals(dates) {
22565
+ if (dates.length < 2) {
22566
+ return [{ years: 0, months: 0, days: 0 }];
22567
+ }
22568
+ const res = dates.map((date, index) => {
22569
+ if (index === 0) {
22570
+ return { years: 0, months: 0, days: 0 };
22571
+ }
22572
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22573
+ const years = getTimeDifferenceInWholeYears(previous, date);
22574
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22575
+ previous.setFullYear(previous.getFullYear() + years);
22576
+ previous.setMonth(previous.getMonth() + months);
22577
+ const days = getTimeDifferenceInWholeDays(previous, date);
22578
+ return {
22579
+ years,
22580
+ months,
22581
+ days,
22582
+ };
22583
+ });
22584
+ return res.slice(1);
22585
+ }
22369
22586
 
22370
22587
  const cellPopoverRegistry = new Registry();
22371
22588
 
@@ -27876,9 +28093,9 @@ function truncateLabel(label) {
27876
28093
  /**
27877
28094
  * Get a default chart js configuration
27878
28095
  */
27879
- function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, truncateLabels = true, horizontalChart, }) {
28096
+ function getDefaultChartJsRuntime(chart, labels, fontColor, { axisFormats, locale, truncateLabels = true, horizontalChart }) {
27880
28097
  const chartTitle = chart.title.text ? chart.title : { ...chart.title, content: "" };
27881
- const options = {
28098
+ const chartOptions = {
27882
28099
  // https://www.chartjs.org/docs/latest/general/responsive.html
27883
28100
  responsive: true, // will resize when its container is resized
27884
28101
  maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
@@ -27925,8 +28142,10 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27925
28142
  if (!yLabel) {
27926
28143
  yLabel = tooltipItem.parsed;
27927
28144
  }
27928
- const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
27929
- const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
28145
+ const axisId = horizontalChart
28146
+ ? tooltipItem.dataset.xAxisID
28147
+ : tooltipItem.dataset.yAxisID;
28148
+ const yLabelStr = formatChartDatasetValue(axisFormats, locale)(yLabel, axisId);
27930
28149
  return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
27931
28150
  },
27932
28151
  },
@@ -27935,7 +28154,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27935
28154
  };
27936
28155
  return {
27937
28156
  type: chart.type,
27938
- options,
28157
+ options: chartOptions,
27939
28158
  data: {
27940
28159
  labels: truncateLabels ? labels.map(truncateLabel) : labels,
27941
28160
  datasets: [],
@@ -27994,7 +28213,8 @@ function getChartLabelValues(getters, dataSets, labelRange) {
27994
28213
  * Get the format to apply to the the dataset values. This format is defined as the first format
27995
28214
  * found in the dataset ranges that isn't a date format.
27996
28215
  */
27997
- function getChartDatasetFormat(getters, dataSets) {
28216
+ function getChartDatasetFormat(getters, allDataSets, axis) {
28217
+ const dataSets = allDataSets.filter((ds) => (axis === "right") === !!ds.rightYAxis);
27998
28218
  for (const ds of dataSets) {
27999
28219
  const formatsInDataset = getters.getRangeFormats(ds.dataRange);
28000
28220
  const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
@@ -28248,12 +28468,16 @@ function createBarChartRuntime(chart, getters) {
28248
28468
  if (chart.aggregated) {
28249
28469
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28250
28470
  }
28251
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
28471
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
28472
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28252
28473
  const locale = getters.getLocale();
28253
- const localeFormat = { format: dataSetFormat, locale };
28254
28474
  const fontColor = chartFontColor(chart.background);
28475
+ const axisFormats = chart.horizontal
28476
+ ? { x: leftAxisFormat || rightAxisFormat }
28477
+ : { y: leftAxisFormat, y1: rightAxisFormat };
28255
28478
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, {
28256
- ...localeFormat,
28479
+ locale,
28480
+ axisFormats,
28257
28481
  horizontalChart: chart.horizontal,
28258
28482
  });
28259
28483
  const legend = {
@@ -28274,51 +28498,27 @@ function createBarChartRuntime(chart, getters) {
28274
28498
  };
28275
28499
  config.options.indexAxis = chart.horizontal ? "y" : "x";
28276
28500
  config.options.scales = {};
28277
- const labelsAxis = { ticks: { padding: 5, color: fontColor } };
28278
- const valuesAxis = {
28279
- beginAtZero: true, // the origin of the y axis is always zero
28280
- ticks: {
28281
- color: fontColor,
28282
- callback: formatTickValue(localeFormat),
28283
- },
28284
- };
28285
- const xAxis = chart.horizontal ? valuesAxis : labelsAxis;
28286
- const yAxis = chart.horizontal ? labelsAxis : valuesAxis;
28287
- const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
28288
- config.options.scales.x = { ...xAxis, title: getChartAxisTitleRuntime(chart.axesDesign?.x) };
28289
- if (useLeftAxis) {
28290
- config.options.scales.y = {
28291
- ...yAxis,
28292
- position: "left",
28293
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
28294
- };
28295
- }
28296
- if (useRightAxis) {
28297
- config.options.scales.y1 = {
28298
- ...yAxis,
28299
- position: "right",
28300
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
28301
- };
28501
+ const definition = chart.getDefinition();
28502
+ const options = { stacked: chart.stacked, locale };
28503
+ if (chart.horizontal) {
28504
+ const format = leftAxisFormat || rightAxisFormat;
28505
+ config.options.scales.x = getChartAxis(definition, "bottom", "values", { ...options, format });
28506
+ config.options.scales.y = getChartAxis(definition, "left", "labels", options);
28302
28507
  }
28303
- if (chart.stacked) {
28304
- // @ts-ignore chart.js type is broken
28305
- config.options.scales.x.stacked = true;
28306
- if (useLeftAxis) {
28307
- // @ts-ignore chart.js type is broken
28308
- config.options.scales.y.stacked = true;
28309
- }
28310
- if (useRightAxis) {
28311
- // @ts-ignore chart.js type is broken
28312
- config.options.scales.y1.stacked = true;
28313
- }
28508
+ else {
28509
+ config.options.scales.x = getChartAxis(definition, "bottom", "labels", options);
28510
+ const leftAxisOptions = { ...options, format: leftAxisFormat };
28511
+ config.options.scales.y = getChartAxis(definition, "left", "values", leftAxisOptions);
28512
+ const rightAxisOptions = { ...options, format: rightAxisFormat };
28513
+ config.options.scales.y1 = getChartAxis(definition, "right", "values", rightAxisOptions);
28314
28514
  }
28515
+ config.options.scales = removeFalsyAttributes(config.options.scales);
28315
28516
  config.options.plugins.chartShowValuesPlugin = {
28316
28517
  showValues: chart.showValues,
28317
28518
  background: chart.background,
28318
28519
  horizontal: chart.horizontal,
28319
- callback: formatTickValue(localeFormat),
28520
+ callback: formatChartDatasetValue(axisFormats, locale),
28320
28521
  };
28321
- const definition = chart.getDefinition();
28322
28522
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28323
28523
  const trendDatasets = [];
28324
28524
  for (const index in dataSetsValues) {
@@ -28336,9 +28536,8 @@ function createBarChartRuntime(chart, getters) {
28336
28536
  const label = definition.dataSets[index].label;
28337
28537
  dataset.label = label;
28338
28538
  }
28339
- if (definition.dataSets?.[index]?.yAxisId && !chart.horizontal) {
28340
- dataset["yAxisID"] = definition.dataSets[index].yAxisId;
28341
- }
28539
+ dataset.yAxisID = chart.horizontal ? "y" : definition.dataSets[index].yAxisId || "y";
28540
+ dataset.xAxisID = "x";
28342
28541
  const trend = definition.dataSets?.[index].trend;
28343
28542
  if (!trend?.display || chart.horizontal) {
28344
28543
  continue;
@@ -28354,7 +28553,7 @@ function createBarChartRuntime(chart, getters) {
28354
28553
  */
28355
28554
  const maxLength = Math.max(...trendDatasets.map((trendDataset) => trendDataset.data.length));
28356
28555
  config.options.scales[TREND_LINE_XAXIS_ID] = {
28357
- ...xAxis,
28556
+ ...config.options.scales.x,
28358
28557
  labels: Array(maxLength).fill(""),
28359
28558
  offset: false,
28360
28559
  display: false,
@@ -28644,8 +28843,10 @@ function createLineOrScatterChartRuntime(chart, getters) {
28644
28843
  }
28645
28844
  const locale = getters.getLocale();
28646
28845
  const truncateLabels = axisType === "category";
28647
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
28648
- const options = { format: dataSetFormat, locale, truncateLabels };
28846
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
28847
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28848
+ const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat };
28849
+ const options = { locale, truncateLabels, axisFormats };
28649
28850
  const fontColor = chartFontColor(chart.background);
28650
28851
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, options);
28651
28852
  const legend = {
@@ -28675,52 +28876,18 @@ function createLineOrScatterChartRuntime(chart, getters) {
28675
28876
  displayLegend: chart.legendPosition === "top",
28676
28877
  }),
28677
28878
  };
28678
- const xAxis = {
28679
- ticks: {
28680
- padding: 5,
28681
- color: fontColor,
28682
- },
28683
- title: getChartAxisTitleRuntime(chart.axesDesign?.x),
28684
- };
28879
+ const definition = chart.getDefinition();
28880
+ const stacked = "stacked" in chart && chart.stacked;
28685
28881
  config.options.scales = {
28686
- x: xAxis,
28687
- };
28688
- const yAxis = {
28689
- beginAtZero: true, // the origin of the y axis is always zero
28690
- ticks: {
28691
- color: fontColor,
28692
- callback: formatTickValue(options),
28693
- },
28882
+ x: getChartAxis(definition, "bottom", "labels", { locale }),
28883
+ y: getChartAxis(definition, "left", "values", { locale, stacked, format: leftAxisFormat }),
28884
+ y1: getChartAxis(definition, "right", "values", { locale, stacked, format: rightAxisFormat }),
28694
28885
  };
28695
- const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
28696
- if (useLeftAxis) {
28697
- config.options.scales.y = {
28698
- ...yAxis,
28699
- position: "left",
28700
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
28701
- };
28702
- }
28703
- if (useRightAxis) {
28704
- config.options.scales.y1 = {
28705
- ...yAxis,
28706
- position: "right",
28707
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
28708
- };
28709
- }
28710
- if ("stacked" in chart && chart.stacked) {
28711
- if (useLeftAxis) {
28712
- // @ts-ignore chart.js type is broken
28713
- config.options.scales.y.stacked = true;
28714
- }
28715
- if (useRightAxis) {
28716
- // @ts-ignore chart.js type is broken
28717
- config.options.scales.y1.stacked = true;
28718
- }
28719
- }
28886
+ config.options.scales = removeFalsyAttributes(config.options.scales);
28720
28887
  config.options.plugins.chartShowValuesPlugin = {
28721
28888
  showValues: chart.showValues,
28722
28889
  background: chart.background,
28723
- callback: formatTickValue(options),
28890
+ callback: formatChartDatasetValue(axisFormats, locale),
28724
28891
  };
28725
28892
  if (chart.dataSetsHaveTitle &&
28726
28893
  dataSetsValues[0] &&
@@ -28747,7 +28914,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
28747
28914
  label = toNumber(label, locale);
28748
28915
  }
28749
28916
  const formattedX = formatValue(label, { locale, format: labelFormat });
28750
- const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });
28917
+ const formattedY = formatValue(dataSetPoint, { locale, format: leftAxisFormat });
28751
28918
  const dataSetTitle = tooltipItem.dataset.label;
28752
28919
  return formattedX
28753
28920
  ? `${dataSetTitle}: (${formattedX}, ${formattedY})`
@@ -28757,7 +28924,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28757
28924
  const areaChart = "fillArea" in chart ? chart.fillArea : false;
28758
28925
  const stackedChart = "stacked" in chart ? chart.stacked : false;
28759
28926
  const cumulative = "cumulative" in chart ? chart.cumulative : false;
28760
- const definition = chart.getDefinition();
28761
28927
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28762
28928
  for (let [index, { label, data }] of dataSetsValues.entries()) {
28763
28929
  const color = colors.next();
@@ -28798,9 +28964,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
28798
28964
  const label = definition.dataSets[index].label;
28799
28965
  dataset.label = label;
28800
28966
  }
28801
- if (definition.dataSets?.[index]?.yAxisId) {
28802
- dataset["yAxisID"] = definition.dataSets[index].yAxisId;
28803
- }
28967
+ dataset["yAxisID"] = definition.dataSets[index].yAxisId || "y";
28804
28968
  const trend = definition.dataSets?.[index].trend;
28805
28969
  if (!trend?.display) {
28806
28970
  continue;
@@ -28817,7 +28981,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
28817
28981
  * set so that the second axis points match the classical x axis
28818
28982
  */
28819
28983
  config.options.scales[TREND_LINE_XAXIS_ID] = {
28820
- ...xAxis,
28984
+ ...config.options.scales.x,
28821
28985
  type: "category",
28822
28986
  labels: range(0, maxLength).map((x) => x.toString()),
28823
28987
  offset: false,
@@ -28839,10 +29003,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28839
29003
  return {
28840
29004
  chartJsConfig: config,
28841
29005
  background: chart.background || BACKGROUND_CHART_COLOR,
28842
- dataSetsValues,
28843
- labelValues,
28844
- dataSetFormat,
28845
- labelFormat,
28846
29006
  };
28847
29007
  }
28848
29008
 
@@ -28900,6 +29060,7 @@ class ComboChart extends AbstractChart {
28900
29060
  ranges.push({
28901
29061
  ...this.dataSetDesign?.[i],
28902
29062
  dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
29063
+ type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
28903
29064
  });
28904
29065
  }
28905
29066
  return {
@@ -28945,9 +29106,13 @@ class ComboChart extends AbstractChart {
28945
29106
  return new ComboChart(definition, this.sheetId, this.getters);
28946
29107
  }
28947
29108
  static getDefinitionFromContextCreation(context) {
29109
+ const dataSets = (context.range ?? []).map((ds, index) => ({
29110
+ ...ds,
29111
+ type: index ? "line" : "bar",
29112
+ }));
28948
29113
  return {
28949
29114
  background: context.background,
28950
- dataSets: context.range ?? [],
29115
+ dataSets,
28951
29116
  dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
28952
29117
  aggregated: context.aggregated,
28953
29118
  legendPosition: context.legendPosition ?? "top",
@@ -28970,10 +29135,8 @@ class ComboChart extends AbstractChart {
28970
29135
  }
28971
29136
  }
28972
29137
  function createComboChartRuntime(chart, getters) {
28973
- const mainDataSetFormat = chart.dataSets.length
28974
- ? getChartDatasetFormat(getters, [chart.dataSets[0]])
28975
- : undefined;
28976
- const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets.slice(1));
29138
+ const mainDataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
29139
+ const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28977
29140
  const locale = getters.getLocale();
28978
29141
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
28979
29142
  let labels = labelValues.formattedValues;
@@ -28987,12 +29150,11 @@ function createComboChartRuntime(chart, getters) {
28987
29150
  if (chart.aggregated) {
28988
29151
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28989
29152
  }
28990
- const localeFormat = { format: mainDataSetFormat, locale };
28991
29153
  const fontColor = chartFontColor(chart.background);
28992
- const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
29154
+ const axisFormats = { y: mainDataSetFormat, y1: lineDataSetsFormat };
29155
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, { locale, axisFormats });
28993
29156
  const legend = {
28994
29157
  labels: { color: fontColor },
28995
- reverse: true,
28996
29158
  };
28997
29159
  if (chart.legendPosition === "none") {
28998
29160
  legend.display = false;
@@ -29007,52 +29169,17 @@ function createComboChartRuntime(chart, getters) {
29007
29169
  displayLegend: chart.legendPosition === "top",
29008
29170
  }),
29009
29171
  };
29172
+ const definition = chart.getDefinition();
29010
29173
  config.options.scales = {
29011
- x: {
29012
- ticks: {
29013
- padding: 5,
29014
- color: fontColor,
29015
- },
29016
- title: getChartAxisTitleRuntime(chart.axesDesign?.x),
29017
- },
29174
+ x: getChartAxis(definition, "bottom", "labels", { locale }),
29175
+ y: getChartAxis(definition, "left", "values", { locale, format: mainDataSetFormat }),
29176
+ y1: getChartAxis(definition, "right", "values", { locale, format: lineDataSetsFormat }),
29018
29177
  };
29019
- const leftVerticalAxis = {
29020
- beginAtZero: true, // the origin of the y axis is always zero
29021
- ticks: {
29022
- color: fontColor,
29023
- callback: formatTickValue({ format: mainDataSetFormat, locale }),
29024
- },
29025
- };
29026
- const rightVerticalAxis = {
29027
- beginAtZero: true, // the origin of the y axis is always zero
29028
- ticks: {
29029
- color: fontColor,
29030
- callback: formatTickValue({ format: lineDataSetsFormat, locale }),
29031
- },
29032
- };
29033
- const definition = chart.getDefinition();
29034
- const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);
29035
- if (useLeftAxis) {
29036
- config.options.scales.y = {
29037
- ...leftVerticalAxis,
29038
- position: "left",
29039
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
29040
- };
29041
- }
29042
- if (useRightAxis) {
29043
- config.options.scales.y1 = {
29044
- ...rightVerticalAxis,
29045
- position: "right",
29046
- grid: {
29047
- display: false,
29048
- },
29049
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
29050
- };
29051
- }
29178
+ config.options.scales = removeFalsyAttributes(config.options.scales);
29052
29179
  config.options.plugins.chartShowValuesPlugin = {
29053
29180
  showValues: chart.showValues,
29054
29181
  background: chart.background,
29055
- callback: formatTickValue({ format: mainDataSetFormat, locale }),
29182
+ callback: formatChartDatasetValue(axisFormats, locale),
29056
29183
  };
29057
29184
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
29058
29185
  let maxLength = 0;
@@ -29060,14 +29187,15 @@ function createComboChartRuntime(chart, getters) {
29060
29187
  for (let [index, { label, data }] of dataSetsValues.entries()) {
29061
29188
  const design = definition.dataSets[index];
29062
29189
  const color = colors.next();
29190
+ const type = design?.type ?? "line";
29063
29191
  const dataset = {
29064
29192
  label: design?.label ?? label,
29065
29193
  data,
29066
29194
  borderColor: color,
29067
29195
  backgroundColor: color,
29068
29196
  yAxisID: design?.yAxisId ?? "y",
29069
- type: index === 0 ? "bar" : "line",
29070
- order: -index,
29197
+ type,
29198
+ order: type === "bar" ? dataSetsValues.length + index : index,
29071
29199
  };
29072
29200
  config.data.datasets.push(dataset);
29073
29201
  const trend = definition.dataSets?.[index].trend;
@@ -29680,7 +29808,7 @@ function createPieChartRuntime(chart, getters) {
29680
29808
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
29681
29809
  }
29682
29810
  ({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));
29683
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
29811
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
29684
29812
  const locale = getters.getLocale();
29685
29813
  const config = getPieConfiguration(chart, labels, { format: dataSetFormat, locale });
29686
29814
  const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
@@ -29835,7 +29963,7 @@ function createPyramidChartRuntime(chart, getters) {
29835
29963
  return tooltipLabelCallback(tooltipItem);
29836
29964
  };
29837
29965
  const callback = config.options.plugins.chartShowValuesPlugin.callback;
29838
- config.options.plugins.chartShowValuesPlugin.callback = (x) => callback(Math.abs(x));
29966
+ config.options.plugins.chartShowValuesPlugin.callback = (x, axisId) => callback(Math.abs(x), axisId);
29839
29967
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
29840
29968
  }
29841
29969
 
@@ -30226,7 +30354,8 @@ function createWaterfallChartRuntime(chart, getters) {
30226
30354
  if (chart.showSubTotals) {
30227
30355
  labels.push(_t("Subtotal"));
30228
30356
  }
30229
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
30357
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left") ||
30358
+ getChartDatasetFormat(getters, chart.dataSets, "right");
30230
30359
  const locale = getters.getLocale();
30231
30360
  const dataSeriesLabels = dataSetsValues.map((dataSet) => dataSet.label);
30232
30361
  const config = getWaterfallConfiguration(chart, labels, dataSeriesLabels, {
@@ -35296,6 +35425,7 @@ const CHECK_SVG = /*xml*/ `
35296
35425
  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35297
35426
  </svg>
35298
35427
  `;
35428
+ const CHECKBOX_WIDTH = 14;
35299
35429
  css /* scss */ `
35300
35430
  label.o-checkbox {
35301
35431
  input {
@@ -35303,8 +35433,8 @@ css /* scss */ `
35303
35433
  -webkit-appearance: none;
35304
35434
  -moz-appearance: none;
35305
35435
  border-radius: 0;
35306
- width: 14px;
35307
- height: 14px;
35436
+ width: ${CHECKBOX_WIDTH}px;
35437
+ height: ${CHECKBOX_WIDTH}px;
35308
35438
  vertical-align: top;
35309
35439
  box-sizing: border-box;
35310
35440
  outline: none;
@@ -37214,6 +37344,32 @@ class ChartWithAxisDesignPanel extends owl.Component {
37214
37344
  }
37215
37345
  }
37216
37346
 
37347
+ class ComboChartDesignPanel extends ChartWithAxisDesignPanel {
37348
+ static template = "o-spreadsheet-ComboChartDesignPanel";
37349
+ seriesTypeChoices = [
37350
+ { value: "bar", label: _t("Bar") },
37351
+ { value: "line", label: _t("Line") },
37352
+ ];
37353
+ updateDataSeriesType(type) {
37354
+ const dataSets = [...this.props.definition.dataSets];
37355
+ if (!dataSets?.[this.state.index]) {
37356
+ return;
37357
+ }
37358
+ dataSets[this.state.index] = {
37359
+ ...dataSets[this.state.index],
37360
+ type,
37361
+ };
37362
+ this.props.updateChart(this.props.figureId, { dataSets });
37363
+ }
37364
+ getDataSeriesType() {
37365
+ const dataSets = this.props.definition.dataSets;
37366
+ if (!dataSets?.[this.state.index]) {
37367
+ return "bar";
37368
+ }
37369
+ return dataSets[this.state.index].type ?? "line";
37370
+ }
37371
+ }
37372
+
37217
37373
  class GaugeChartConfigPanel extends owl.Component {
37218
37374
  static template = "o-spreadsheet-GaugeChartConfigPanel";
37219
37375
  static components = { ChartErrorSection, ChartDataSeries };
@@ -37640,7 +37796,7 @@ chartSidePanelComponentRegistry
37640
37796
  })
37641
37797
  .add("combo", {
37642
37798
  configuration: GenericChartConfigPanel,
37643
- design: ChartWithAxisDesignPanel,
37799
+ design: ComboChartDesignPanel,
37644
37800
  })
37645
37801
  .add("pie", {
37646
37802
  configuration: GenericChartConfigPanel,
@@ -37926,6 +38082,7 @@ class AbstractComposerStore extends SpreadsheetStore {
37926
38082
  "stopComposerRangeSelection",
37927
38083
  "cancelEdition",
37928
38084
  "cycleReferences",
38085
+ "toggleEditionMode",
37929
38086
  "changeComposerCursorSelection",
37930
38087
  "replaceComposerCursorSelection",
37931
38088
  ];
@@ -38081,6 +38238,42 @@ class AbstractComposerStore extends SpreadsheetStore {
38081
38238
  }
38082
38239
  this.setCurrentContent(updated.content, updated.selection);
38083
38240
  }
38241
+ toggleEditionMode() {
38242
+ if (this.editionMode === "inactive")
38243
+ return;
38244
+ const start = Math.min(this.selectionStart, this.selectionEnd);
38245
+ const end = Math.max(this.selectionStart, this.selectionEnd);
38246
+ const refToken = [...this.currentTokens]
38247
+ .reverse()
38248
+ .find((tk) => tk.end >= start && end >= tk.start && tk.type === "REFERENCE");
38249
+ if (this.editionMode === "editing" && refToken) {
38250
+ const currentSheetId = this.getters.getActiveSheetId();
38251
+ const { sheetName, xc } = splitReference(refToken.value);
38252
+ const sheetId = this.getters.getSheetIdByName(sheetName);
38253
+ if (sheetId && sheetId !== currentSheetId) {
38254
+ this.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: currentSheetId, sheetIdTo: sheetId });
38255
+ }
38256
+ // move cursor to the right part of the token
38257
+ this.selectionStart = this.selectionEnd = refToken.end;
38258
+ const zone = this.getters.getRangeFromSheetXC(this.sheetId, xc).zone;
38259
+ this.captureSelection(zone);
38260
+ this.editionMode = "selecting";
38261
+ }
38262
+ else {
38263
+ this.editionMode = "editing";
38264
+ }
38265
+ }
38266
+ captureSelection(zone, col, row) {
38267
+ this.model.selection.capture(this, {
38268
+ cell: { col: col || zone.left, row: row || zone.right },
38269
+ zone,
38270
+ }, {
38271
+ handleEvent: this.handleEvent.bind(this),
38272
+ release: () => {
38273
+ this._stopEdition();
38274
+ },
38275
+ });
38276
+ }
38084
38277
  isSelectionValid(length, start, end) {
38085
38278
  return start >= 0 && start <= length && end >= 0 && end <= length;
38086
38279
  }
@@ -38119,12 +38312,7 @@ class AbstractComposerStore extends SpreadsheetStore {
38119
38312
  this.setContent(str || this.initialContent, selection);
38120
38313
  this.colorIndexByRange = {};
38121
38314
  const zone = positionToZone({ col: this.col, row: this.row });
38122
- this.model.selection.capture(this, { cell: { col: this.col, row: this.row }, zone }, {
38123
- handleEvent: this.handleEvent.bind(this),
38124
- release: () => {
38125
- this._stopEdition();
38126
- },
38127
- });
38315
+ this.captureSelection(zone, col, row);
38128
38316
  }
38129
38317
  _stopEdition() {
38130
38318
  if (this.editionMode !== "inactive") {
@@ -42819,8 +43007,9 @@ const EMPTY_PIVOT_CELL = { type: "EMPTY" };
42819
43007
  * This function converts a list of data entry into a spreadsheet pivot table.
42820
43008
  */
42821
43009
  function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
43010
+ const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42822
43011
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
42823
- computeWidthOfColumnsNodes(columnsTree, definition.measures.length);
43012
+ computeWidthOfColumnsNodes(columnsTree, measureIds.length);
42824
43013
  const cols = columnsTreeToColumns(columnsTree, definition);
42825
43014
  const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);
42826
43015
  // Add the total row
@@ -42829,7 +43018,6 @@ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
42829
43018
  values: [],
42830
43019
  indent: 0,
42831
43020
  });
42832
- const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42833
43021
  const fieldsType = {};
42834
43022
  for (const columns of definition.columns) {
42835
43023
  fieldsType[columns.fieldName] = columns.type;
@@ -43590,7 +43778,7 @@ pivotRegistry.add("SPREADSHEET", {
43590
43778
  onIterationEndEvaluation: (pivot) => pivot.markAsDirtyForEvaluation(),
43591
43779
  dateGranularities: [...dateGranularities],
43592
43780
  datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
43593
- isMeasureCandidate: (field) => !["date", "boolean"].includes(field.type),
43781
+ isMeasureCandidate: (field) => !["datetime", "boolean"].includes(field.type),
43594
43782
  isGroupable: () => true,
43595
43783
  });
43596
43784
 
@@ -46119,13 +46307,10 @@ class GridCellIcon extends owl.Component {
46119
46307
  }
46120
46308
  }
46121
46309
 
46122
- const CHECKBOX_WIDTH = 15;
46123
46310
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
46124
46311
  css /* scss */ `
46125
46312
  .o-dv-checkbox {
46126
46313
  box-sizing: border-box !important;
46127
- width: ${CHECKBOX_WIDTH}px;
46128
- height: ${CHECKBOX_WIDTH}px;
46129
46314
  accent-color: #808080;
46130
46315
  margin: ${MARGIN}px;
46131
46316
  /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
@@ -46134,13 +46319,15 @@ css /* scss */ `
46134
46319
  `;
46135
46320
  class DataValidationCheckbox extends owl.Component {
46136
46321
  static template = "o-spreadsheet-DataValidationCheckbox";
46322
+ static components = {
46323
+ Checkbox,
46324
+ };
46137
46325
  static props = {
46138
46326
  cellPosition: Object,
46139
46327
  };
46140
- onCheckboxChange(ev) {
46141
- const newValue = ev.target.checked;
46328
+ onCheckboxChange(value) {
46142
46329
  const { sheetId, col, row } = this.props.cellPosition;
46143
- const cellContent = newValue ? "TRUE" : "FALSE";
46330
+ const cellContent = value ? "TRUE" : "FALSE";
46144
46331
  this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
46145
46332
  }
46146
46333
  get checkBoxValue() {
@@ -46954,7 +47141,11 @@ class GridAddRowsFooter extends owl.Component {
46954
47141
  class PaintFormatStore extends SpreadsheetStore {
46955
47142
  mutators = ["activate", "cancel", "pasteFormat"];
46956
47143
  highlightStore = this.get(HighlightStore);
46957
- cellClipboardHandler = new CellClipboardHandler(this.getters, this.model.dispatch);
47144
+ clipboardHandlers = [
47145
+ new CellClipboardHandler(this.getters, this.model.dispatch),
47146
+ new BorderClipboardHandler(this.getters, this.model.dispatch),
47147
+ new TableClipboardHandler(this.getters, this.model.dispatch),
47148
+ ];
46958
47149
  status = "inactive";
46959
47150
  copiedData;
46960
47151
  constructor(get) {
@@ -46975,10 +47166,12 @@ class PaintFormatStore extends SpreadsheetStore {
46975
47166
  pasteFormat(target) {
46976
47167
  if (this.copiedData) {
46977
47168
  const sheetId = this.getters.getActiveSheetId();
46978
- this.cellClipboardHandler.paste({ zones: target, sheetId }, this.copiedData, {
46979
- isCutOperation: false,
46980
- pasteOption: "onlyFormat",
46981
- });
47169
+ for (const handler of this.clipboardHandlers) {
47170
+ handler.paste({ zones: target, sheetId }, this.copiedData, {
47171
+ isCutOperation: false,
47172
+ pasteOption: "onlyFormat",
47173
+ });
47174
+ }
46982
47175
  }
46983
47176
  if (this.status === "oneOff") {
46984
47177
  this.cancel();
@@ -46990,7 +47183,11 @@ class PaintFormatStore extends SpreadsheetStore {
46990
47183
  copyFormats() {
46991
47184
  const sheetId = this.getters.getActiveSheetId();
46992
47185
  const zones = this.getters.getSelectedZones();
46993
- return this.cellClipboardHandler.copy(getClipboardDataPositions(sheetId, zones));
47186
+ const copiedData = {};
47187
+ for (const handler of this.clipboardHandlers) {
47188
+ Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones)));
47189
+ }
47190
+ return copiedData;
46994
47191
  }
46995
47192
  get highlights() {
46996
47193
  const data = this.copiedData;
@@ -49798,12 +49995,10 @@ class BasePlugin {
49798
49995
  */
49799
49996
  class CorePlugin extends BasePlugin {
49800
49997
  getters;
49801
- uuidGenerator;
49802
- constructor({ getters, stateObserver, range, dispatch, canDispatch, uuidGenerator, }) {
49998
+ constructor({ getters, stateObserver, range, dispatch, canDispatch }) {
49803
49999
  super(stateObserver, dispatch, canDispatch);
49804
50000
  range.addRangeProvider(this.adaptRanges.bind(this));
49805
50001
  this.getters = getters;
49806
- this.uuidGenerator = uuidGenerator;
49807
50002
  }
49808
50003
  // ---------------------------------------------------------------------------
49809
50004
  // Import/Export
@@ -54301,6 +54496,7 @@ class SheetPlugin extends CorePlugin {
54301
54496
  }
54302
54497
  }
54303
54498
 
54499
+ let nextTableId = 1;
54304
54500
  class TablePlugin extends CorePlugin {
54305
54501
  static getters = ["getCoreTable", "getCoreTables", "getCoreTableMatchingTopLeft"];
54306
54502
  tables = {};
@@ -54368,7 +54564,7 @@ class TablePlugin extends CorePlugin {
54368
54564
  const union = this.getters.getRangesUnion(ranges);
54369
54565
  const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
54370
54566
  this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
54371
- const id = this.uuidGenerator.uuidv4();
54567
+ const id = `${nextTableId++}`;
54372
54568
  const config = cmd.config || DEFAULT_TABLE_CONFIG;
54373
54569
  const newTable = cmd.tableType === "dynamic"
54374
54570
  ? this.createDynamicTable(id, union, config)
@@ -54521,7 +54717,7 @@ class TablePlugin extends CorePlugin {
54521
54717
  filters = [];
54522
54718
  for (const i of range(zone.left, zone.right + 1)) {
54523
54719
  const filterZone = { ...zone, left: i, right: i };
54524
- const uid = this.uuidGenerator.uuidv4();
54720
+ const uid = `${nextTableId++}`;
54525
54721
  filters.push(this.createFilterFromZone(uid, tableRange.sheetId, filterZone, config));
54526
54722
  }
54527
54723
  }
@@ -54586,7 +54782,7 @@ class TablePlugin extends CorePlugin {
54586
54782
  ? table.filters.find((f) => f.col === i)
54587
54783
  : undefined;
54588
54784
  const filterZone = { ...tableZone, left: i, right: i };
54589
- const filterId = oldFilter?.id || this.uuidGenerator.uuidv4();
54785
+ const filterId = oldFilter?.id || `${nextTableId++}`;
54590
54786
  filters.push(this.createFilterFromZone(filterId, tableRange.sheetId, filterZone, config));
54591
54787
  }
54592
54788
  }
@@ -54687,7 +54883,7 @@ class TablePlugin extends CorePlugin {
54687
54883
  if (filters.length < zoneToDimension(tableZone).numberOfCols) {
54688
54884
  for (let col = tableZone.left; col <= tableZone.right; col++) {
54689
54885
  if (!filters.find((filter) => filter.col === col)) {
54690
- const uid = this.uuidGenerator.uuidv4();
54886
+ const uid = `${nextTableId++}`;
54691
54887
  const filterZone = { ...tableZone, left: col, right: col };
54692
54888
  filters.push(this.createFilterFromZone(uid, sheetId, filterZone, table.config));
54693
54889
  }
@@ -54703,7 +54899,7 @@ class TablePlugin extends CorePlugin {
54703
54899
  import(data) {
54704
54900
  for (const sheet of data.sheets) {
54705
54901
  for (const tableData of sheet.tables || []) {
54706
- const uuid = this.uuidGenerator.uuidv4();
54902
+ const uuid = `${nextTableId++}`;
54707
54903
  const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;
54708
54904
  const range = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
54709
54905
  const tableType = tableData.type || "static";
@@ -59052,7 +59248,7 @@ function withPivotPresentationLayer (PivotClass) {
59052
59248
  const ranking = {};
59053
59249
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59054
59250
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59055
- let pivotCells = this.getPivotValueCells();
59251
+ let pivotCells = this.getPivotValueCells(measure.id);
59056
59252
  if (mainDimension === "column") {
59057
59253
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59058
59254
  // This means that we need to transpose back the ranking at the end
@@ -59096,7 +59292,7 @@ function withPivotPresentationLayer (PivotClass) {
59096
59292
  const cellsRunningTotals = {};
59097
59293
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59098
59294
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59099
- let pivotCells = this.getPivotValueCells();
59295
+ let pivotCells = this.getPivotValueCells(measure.id);
59100
59296
  if (mainDimension === "column") {
59101
59297
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59102
59298
  // This means that we need to transpose back the totals at the end
@@ -59175,10 +59371,10 @@ function withPivotPresentationLayer (PivotClass) {
59175
59371
  const comparedValueNumber = this.strictMeasureValueToNumber(comparedValue);
59176
59372
  return comparedValueNumber;
59177
59373
  }
59178
- getPivotValueCells() {
59374
+ getPivotValueCells(measureId) {
59179
59375
  return this.getTableStructure()
59180
59376
  .getPivotCells()
59181
- .map((col) => col.filter((cell) => cell.type === "VALUE"))
59377
+ .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
59182
59378
  .filter((col) => col.length > 0);
59183
59379
  }
59184
59380
  measureValueToNumber(result) {
@@ -60946,6 +61142,7 @@ class Session extends EventBus {
60946
61142
  case "REMOTE_REVISION":
60947
61143
  case "REVISION_REDONE":
60948
61144
  case "REVISION_UNDONE":
61145
+ case "SNAPSHOT_CREATED":
60949
61146
  return this.processedRevisions.has(message.nextRevisionId);
60950
61147
  default:
60951
61148
  return false;
@@ -61508,7 +61705,7 @@ class InsertPivotPlugin extends UIPlugin {
61508
61705
  const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;
61509
61706
  const formulaId = this.getters.getPivotFormulaId(newPivotId);
61510
61707
  const newPivotName = this.getters.getPivotName(newPivotId);
61511
- this.dispatch("CREATE_SHEET", {
61708
+ const result = this.dispatch("CREATE_SHEET", {
61512
61709
  sheetId: newSheetId,
61513
61710
  name: this.getPivotDuplicateSheetName(_t("%(newPivotName)s (Pivot #%(formulaId)s)", {
61514
61711
  newPivotName,
@@ -61516,20 +61713,19 @@ class InsertPivotPlugin extends UIPlugin {
61516
61713
  })),
61517
61714
  position,
61518
61715
  });
61519
- this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61520
- this.dispatch("UPDATE_CELL", {
61521
- sheetId: newSheetId,
61522
- col: 0,
61523
- row: 0,
61524
- content: `=PIVOT(${formulaId})`,
61525
- });
61716
+ if (result.isSuccessful) {
61717
+ this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61718
+ const pivot = this.getters.getPivot(pivotId);
61719
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
61720
+ }
61526
61721
  }
61527
61722
  getPivotDuplicateSheetName(pivotName) {
61528
61723
  let i = 1;
61529
61724
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
61530
- let name = pivotName;
61725
+ const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
61726
+ let name = sanitizedName;
61531
61727
  while (names.includes(name)) {
61532
- name = `${pivotName} (${i})`;
61728
+ name = `${sanitizedName} (${i})`;
61533
61729
  i++;
61534
61730
  }
61535
61731
  return name;
@@ -70882,6 +71078,15 @@ function addTableColumns(table, sheetData) {
70882
71078
  ["id", i + 1], // id cannot be 0
70883
71079
  ["name", colName],
70884
71080
  ];
71081
+ if (table.config.totalRow) {
71082
+ // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag
71083
+ // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.
71084
+ const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);
71085
+ const colTotalContent = sheetData.cells[colTotalXc]?.content;
71086
+ if (colTotalContent?.startsWith("=")) {
71087
+ colAttributes.push(["totalsRowFunction", "custom"]);
71088
+ }
71089
+ }
70885
71090
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
70886
71091
  }
70887
71092
  return escapeXml /*xml*/ `
@@ -70976,8 +71181,9 @@ function addRows(construct, data, sheet) {
70976
71181
  }
70977
71182
  else if (cell.content && cell.content !== "") {
70978
71183
  const isTableHeader = isCellTableHeader(c, r, sheet);
71184
+ const isTableTotal = isCellTableTotal(c, r, sheet);
70979
71185
  const isPlainText = !!(cell.format && isTextFormat(data.formats[cell.format]));
70980
- ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isPlainText));
71186
+ ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));
70981
71187
  }
70982
71188
  attributes.push(...additionalAttrs);
70983
71189
  // prettier-ignore
@@ -71011,6 +71217,16 @@ function isCellTableHeader(col, row, sheet) {
71011
71217
  return isInside(col, row, headerZone);
71012
71218
  });
71013
71219
  }
71220
+ function isCellTableTotal(col, row, sheet) {
71221
+ return sheet.tables.some((table) => {
71222
+ if (!table.config.totalRow) {
71223
+ return false;
71224
+ }
71225
+ const zone = toZone(table.range);
71226
+ const totalZone = { ...zone, top: zone.bottom };
71227
+ return isInside(col, row, totalZone);
71228
+ });
71229
+ }
71014
71230
  function addHyperlinks(construct, data, sheetIndex) {
71015
71231
  const sheet = data.sheets[sheetIndex];
71016
71232
  const cells = sheet.cells;
@@ -71501,7 +71717,6 @@ class Model extends EventBus {
71501
71717
  isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
71502
71718
  isDashboard: () => this.config.mode === "dashboard",
71503
71719
  };
71504
- this.uuidGenerator.setIsFastStrategy(true);
71505
71720
  // Initiate stream processor
71506
71721
  this.selection = new SelectionStreamProcessorImpl(this.getters);
71507
71722
  this.coreHandlers.push(this.range);
@@ -71533,7 +71748,6 @@ class Model extends EventBus {
71533
71748
  this.handlers.push(plugin);
71534
71749
  this.uiHandlers.push(plugin);
71535
71750
  }
71536
- this.uuidGenerator.setIsFastStrategy(false);
71537
71751
  // starting plugins
71538
71752
  this.dispatch("START");
71539
71753
  // Model should be the last permanent subscriber in the list since he should render
@@ -71681,7 +71895,6 @@ class Model extends EventBus {
71681
71895
  range: this.range,
71682
71896
  dispatch: this.dispatchFromCorePlugin,
71683
71897
  canDispatch: this.canDispatch,
71684
- uuidGenerator: this.uuidGenerator,
71685
71898
  custom: this.config.custom,
71686
71899
  external: this.config.external,
71687
71900
  };
@@ -72175,6 +72388,6 @@ exports.tokenColors = tokenColors;
72175
72388
  exports.tokenize = tokenize;
72176
72389
 
72177
72390
 
72178
- __info__.version = "18.1.0-alpha.0";
72179
- __info__.date = "2024-09-25T13:17:49.636Z";
72180
- __info__.hash = "288f0b7";
72391
+ __info__.version = "18.1.0-alpha.1";
72392
+ __info__.date = "2024-10-14T07:53:17.717Z";
72393
+ __info__.hash = "e9ce3aa";