@odoo/o-spreadsheet 18.0.0 → 18.0.2

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.0.0
6
- * @date 2024-09-25T12:54:19.974Z
7
- * @hash cee2e47
5
+ * @version 18.0.2
6
+ * @date 2024-10-24T08:54:21.934Z
7
+ * @hash 788df92
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";
@@ -270,7 +376,7 @@ const PIVOT_TABLE_CONFIG = {
270
376
  bandedRows: true,
271
377
  bandedColumns: false,
272
378
  styleId: "TableStyleMedium5",
273
- automaticAutofill: true,
379
+ automaticAutofill: false,
274
380
  };
275
381
  const DEFAULT_CURRENCY = {
276
382
  symbol: "$",
@@ -5995,111 +6101,6 @@ class UuidGenerator {
5995
6101
  }
5996
6102
  }
5997
6103
 
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
6104
  function getClipboardDataPositions(sheetId, zones) {
6104
6105
  const lefts = new Set(zones.map((z) => z.left));
6105
6106
  const rights = new Set(zones.map((z) => z.right));
@@ -8454,31 +8455,34 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8454
8455
  for (let col of columnsIndexes) {
8455
8456
  const position = { col, row, sheetId };
8456
8457
  const table = this.getters.getTable(position);
8457
- if (!table || copiedTablesIds.has(table.id)) {
8458
+ if (!table) {
8458
8459
  tableCellsInRow.push({});
8459
8460
  continue;
8460
8461
  }
8461
8462
  const coreTable = this.getters.getCoreTable(position);
8462
8463
  const tableZone = coreTable?.range.zone;
8464
+ let copiedTable = undefined;
8463
8465
  // Copy whole table
8464
- if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
8465
- copiedTablesIds.add(coreTable.id);
8466
+ if (!copiedTablesIds.has(table.id) &&
8467
+ coreTable &&
8468
+ tableZone &&
8469
+ zones.some((z) => isZoneInside(tableZone, z))) {
8470
+ copiedTablesIds.add(table.id);
8466
8471
  const values = [];
8467
8472
  for (const col of range(tableZone.left, tableZone.right + 1)) {
8468
8473
  values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
8469
8474
  }
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) });
8475
+ copiedTable = {
8476
+ range: coreTable.range.rangeData,
8477
+ config: coreTable.config,
8478
+ type: coreTable.type,
8479
+ };
8481
8480
  }
8481
+ tableCellsInRow.push({
8482
+ table: copiedTable,
8483
+ style: this.getTableStyleToCopy(position),
8484
+ isWholeTableCopied: copiedTablesIds.has(table.id),
8485
+ });
8482
8486
  }
8483
8487
  }
8484
8488
  return {
@@ -8559,11 +8563,14 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8559
8563
  tableType: tableCell.table.type,
8560
8564
  });
8561
8565
  }
8562
- // Do not paste table style if we're inside another table
8563
8566
  // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
8564
8567
  // dynamic tables are not yet computed
8565
- if (!this.getters.getCoreTable(position)) {
8566
- if (tableCell.style?.style && options?.pasteOption !== "asValue") {
8568
+ if (this.getters.getCoreTable(position) || options?.pasteOption === "asValue") {
8569
+ return;
8570
+ }
8571
+ if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||
8572
+ options?.pasteOption === "onlyFormat") {
8573
+ if (tableCell.style?.style) {
8567
8574
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
8568
8575
  }
8569
8576
  if (tableCell.style?.border) {
@@ -9404,70 +9411,114 @@ const chartShowValuesPlugin = {
9404
9411
  ctx.save();
9405
9412
  ctx.textAlign = "center";
9406
9413
  ctx.textBaseline = "middle";
9407
- ctx.fillStyle = chartFontColor(options.background);
9408
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9409
- chart._metasets.forEach(function (dataset) {
9410
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9411
- return; // ignore trend lines
9412
- }
9413
- switch (dataset.type) {
9414
- case "doughnut":
9415
- case "pie": {
9416
- for (let i = 0; i < dataset._parsed.length; i++) {
9417
- const bar = dataset.data[i];
9418
- const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9419
- const midAngle = (startAngle + endAngle) / 2;
9420
- const midRadius = (innerRadius + outerRadius) / 2;
9421
- const x = bar.x + midRadius * Math.cos(midAngle);
9422
- const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9423
- ctx.fillStyle = chartFontColor(bar.options.backgroundColor);
9424
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9425
- const value = options.callback(dataset._parsed[i]);
9426
- ctx.strokeText(value, x, y);
9427
- ctx.fillText(value, x, y);
9428
- }
9429
- break;
9430
- }
9431
- case "bar":
9432
- case "line": {
9433
- const yOffset = dataset.type === "bar" && !options.horizontal ? 0 : 3;
9434
- for (let i = 0; i < dataset._parsed.length; i++) {
9435
- const point = dataset.data[i];
9436
- const value = options.horizontal ? dataset._parsed[i].x : dataset._parsed[i].y;
9437
- const displayedValue = options.callback(value - 0);
9438
- let xPosition = 0, yPosition = 0;
9439
- if (options.horizontal) {
9440
- yPosition = point.y;
9441
- if (value < 0) {
9442
- ctx.textAlign = "right";
9443
- xPosition = point.x - yOffset;
9444
- }
9445
- else {
9446
- ctx.textAlign = "left";
9447
- xPosition = point.x + yOffset;
9448
- }
9449
- }
9450
- else {
9451
- xPosition = point.x;
9452
- if (value < 0) {
9453
- ctx.textBaseline = "top";
9454
- yPosition = point.y + yOffset;
9455
- }
9456
- else {
9457
- ctx.textBaseline = "bottom";
9458
- yPosition = point.y - yOffset;
9459
- }
9460
- }
9461
- ctx.strokeText(displayedValue, xPosition, yPosition);
9462
- ctx.fillText(displayedValue, xPosition, yPosition);
9463
- }
9464
- break;
9465
- }
9466
- }
9467
- });
9414
+ ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
9415
+ switch (chart.config.type) {
9416
+ case "pie":
9417
+ case "doughnut":
9418
+ drawPieChartValues(chart, options, ctx);
9419
+ break;
9420
+ case "bar":
9421
+ case "line":
9422
+ options.horizontal
9423
+ ? drawHorizontalBarChartValues(chart, options, ctx)
9424
+ : drawLineOrBarChartValues(chart, options, ctx);
9425
+ break;
9426
+ }
9468
9427
  ctx.restore();
9469
9428
  },
9470
9429
  };
9430
+ function drawTextWithBackground(text, x, y, ctx) {
9431
+ ctx.lineWidth = 3; // Stroke the text with a big lineWidth width to have some kind of background
9432
+ ctx.strokeText(text, x, y);
9433
+ ctx.lineWidth = 1;
9434
+ ctx.fillText(text, x, y);
9435
+ }
9436
+ function drawLineOrBarChartValues(chart, options, ctx) {
9437
+ const yMax = chart.chartArea.bottom;
9438
+ const yMin = chart.chartArea.top;
9439
+ const textsPositions = {};
9440
+ for (const dataset of chart._metasets) {
9441
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9442
+ return; // ignore trend lines
9443
+ }
9444
+ for (let i = 0; i < dataset._parsed.length; i++) {
9445
+ const value = dataset._parsed[i].y;
9446
+ const point = dataset.data[i];
9447
+ const xPosition = point.x;
9448
+ let yPosition = 0;
9449
+ if (chart.config.type === "line") {
9450
+ yPosition = point.y - 10;
9451
+ }
9452
+ else {
9453
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
9454
+ }
9455
+ yPosition = Math.min(yPosition, yMax);
9456
+ yPosition = Math.max(yPosition, yMin);
9457
+ // Avoid overlapping texts with same X
9458
+ if (!textsPositions[xPosition]) {
9459
+ textsPositions[xPosition] = [];
9460
+ }
9461
+ for (const otherPosition of textsPositions[xPosition] || []) {
9462
+ if (Math.abs(otherPosition - yPosition) < 13) {
9463
+ yPosition = otherPosition - 13;
9464
+ }
9465
+ }
9466
+ textsPositions[xPosition].push(yPosition);
9467
+ ctx.fillStyle = point.options.backgroundColor;
9468
+ ctx.strokeStyle = options.background || "#ffffff";
9469
+ drawTextWithBackground(options.callback(value - 0), xPosition, yPosition, ctx);
9470
+ }
9471
+ }
9472
+ }
9473
+ function drawHorizontalBarChartValues(chart, options, ctx) {
9474
+ const xMax = chart.chartArea.right;
9475
+ const xMin = chart.chartArea.left;
9476
+ const textsPositions = {};
9477
+ for (const dataset of chart._metasets) {
9478
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9479
+ return; // ignore trend lines
9480
+ }
9481
+ for (let i = 0; i < dataset._parsed.length; i++) {
9482
+ const value = dataset._parsed[i].x;
9483
+ const displayValue = options.callback(value - 0);
9484
+ const point = dataset.data[i];
9485
+ const yPosition = point.y;
9486
+ let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
9487
+ xPosition = Math.min(xPosition, xMax);
9488
+ xPosition = Math.max(xPosition, xMin);
9489
+ // Avoid overlapping texts with same Y
9490
+ if (!textsPositions[yPosition]) {
9491
+ textsPositions[yPosition] = [];
9492
+ }
9493
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
9494
+ for (const otherPosition of textsPositions[yPosition]) {
9495
+ if (Math.abs(otherPosition - xPosition) < textWidth) {
9496
+ xPosition = otherPosition + textWidth + 3;
9497
+ }
9498
+ }
9499
+ textsPositions[yPosition].push(xPosition);
9500
+ ctx.fillStyle = point.options.backgroundColor;
9501
+ ctx.strokeStyle = options.background || "#ffffff";
9502
+ drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
9503
+ }
9504
+ }
9505
+ }
9506
+ function drawPieChartValues(chart, options, ctx) {
9507
+ for (const dataset of chart._metasets) {
9508
+ for (let i = 0; i < dataset._parsed.length; i++) {
9509
+ const bar = dataset.data[i];
9510
+ const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9511
+ const midAngle = (startAngle + endAngle) / 2;
9512
+ const midRadius = (innerRadius + outerRadius) / 2;
9513
+ const x = bar.x + midRadius * Math.cos(midAngle);
9514
+ const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9515
+ ctx.fillStyle = chartFontColor(options.background);
9516
+ ctx.strokeStyle = options.background || "#ffffff";
9517
+ const value = options.callback(dataset._parsed[i]);
9518
+ drawTextWithBackground(value, x, y, ctx);
9519
+ }
9520
+ }
9521
+ }
9471
9522
 
9472
9523
  /** This is a chartJS plugin that will draw connector lines between the bars of a Waterfall chart */
9473
9524
  const waterfallLinesPlugin = {
@@ -17843,7 +17894,7 @@ function assertDomainLength(domain) {
17843
17894
  throw new EvaluationError(_t("Function PIVOT takes an even number of arguments."));
17844
17895
  }
17845
17896
  }
17846
- function addPivotDependencies(evalContext, coreDefinition) {
17897
+ function addPivotDependencies(evalContext, coreDefinition, forMeasures) {
17847
17898
  //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
17848
17899
  const dependencies = [];
17849
17900
  if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
@@ -17855,7 +17906,7 @@ function addPivotDependencies(evalContext, coreDefinition) {
17855
17906
  }
17856
17907
  dependencies.push(range);
17857
17908
  }
17858
- for (const measure of coreDefinition.measures) {
17909
+ for (const measure of forMeasures) {
17859
17910
  if (measure.computedBy) {
17860
17911
  const formula = evalContext.getters.getMeasureCompiledFormula(measure);
17861
17912
  dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
@@ -18288,7 +18339,7 @@ const PIVOT_VALUE = {
18288
18339
  assertDomainLength(domainArgs);
18289
18340
  const pivot = this.getters.getPivot(pivotId);
18290
18341
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18291
- addPivotDependencies(this, coreDefinition);
18342
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures.filter((m) => m.id === _measure));
18292
18343
  pivot.init({ reload: pivot.needsReevaluation });
18293
18344
  const error = pivot.assertIsValid({ throwOnError: false });
18294
18345
  if (error) {
@@ -18318,7 +18369,7 @@ const PIVOT_HEADER = {
18318
18369
  assertDomainLength(domainArgs);
18319
18370
  const pivot = this.getters.getPivot(_pivotId);
18320
18371
  const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
18321
- addPivotDependencies(this, coreDefinition);
18372
+ addPivotDependencies(this, coreDefinition, []);
18322
18373
  pivot.init({ reload: pivot.needsReevaluation });
18323
18374
  const error = pivot.assertIsValid({ throwOnError: false });
18324
18375
  if (error) {
@@ -18369,7 +18420,7 @@ const PIVOT = {
18369
18420
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
18370
18421
  const pivot = this.getters.getPivot(pivotId);
18371
18422
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18372
- addPivotDependencies(this, coreDefinition);
18423
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures);
18373
18424
  pivot.init({ reload: pivot.needsReevaluation });
18374
18425
  const error = pivot.assertIsValid({ throwOnError: false });
18375
18426
  if (error) {
@@ -21578,6 +21629,7 @@ function insertTokenAfterArgSeparator(tokenAtCursor, value) {
21578
21629
  // replace the whole token
21579
21630
  start = tokenAtCursor.start;
21580
21631
  }
21632
+ this.composer.stopComposerRangeSelection();
21581
21633
  this.composer.changeComposerCursorSelection(start, end);
21582
21634
  this.composer.replaceComposerCursorSelection(value);
21583
21635
  }
@@ -21595,6 +21647,7 @@ function insertTokenAfterLeftParenthesis(tokenAtCursor, value) {
21595
21647
  // replace the whole token
21596
21648
  start = tokenAtCursor.start;
21597
21649
  }
21650
+ this.composer.stopComposerRangeSelection();
21598
21651
  this.composer.changeComposerCursorSelection(start, end);
21599
21652
  this.composer.replaceComposerCursorSelection(value);
21600
21653
  }
@@ -21728,9 +21781,13 @@ autoCompleteProviders.add("pivot_group_fields", {
21728
21781
  const colFields = columns.map((groupBy) => groupBy.nameWithGranularity);
21729
21782
  const rowFields = rows.map((groupBy) => groupBy.nameWithGranularity);
21730
21783
  const proposals = [];
21731
- const previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21784
+ let previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21732
21785
  ? argGroupBys.at(-1)
21733
21786
  : argGroupBys.at(-2);
21787
+ const isPositionalSupported = supportedPivotPositionalFormulaRegistry.get(pivot.type);
21788
+ if (isPositionalSupported && previousGroupBy?.startsWith("#")) {
21789
+ previousGroupBy = previousGroupBy.slice(1);
21790
+ }
21734
21791
  if (previousGroupBy === undefined) {
21735
21792
  proposals.push(colFields[0]);
21736
21793
  proposals.push(rowFields[0]);
@@ -21752,7 +21809,7 @@ autoCompleteProviders.add("pivot_group_fields", {
21752
21809
  return field ? makeFieldProposal(field, granularity) : undefined;
21753
21810
  })
21754
21811
  .concat(groupBys.map((groupBy) => {
21755
- if (!supportedPivotPositionalFormulaRegistry.get(pivot.type)) {
21812
+ if (!isPositionalSupported) {
21756
21813
  return undefined;
21757
21814
  }
21758
21815
  const fieldName = groupBy.split(":")[0];
@@ -22187,6 +22244,27 @@ autofillModifiersRegistry
22187
22244
  tooltip: content ? { props: { content: tooltipValue } } : undefined,
22188
22245
  };
22189
22246
  },
22247
+ })
22248
+ .add("DATE_INCREMENT_MODIFIER", {
22249
+ apply: (rule, data, getters) => {
22250
+ const date = toJsDate(rule.current, getters.getLocale());
22251
+ date.setFullYear(date.getFullYear() + rule.increment.years || 0);
22252
+ date.setMonth(date.getMonth() + rule.increment.months || 0);
22253
+ date.setDate(date.getDate() + rule.increment.days || 0);
22254
+ const value = jsDateToNumber(date);
22255
+ rule.current = value;
22256
+ const locale = getters.getLocale();
22257
+ const tooltipValue = formatValue(value, { format: data.cell?.format, locale });
22258
+ return {
22259
+ cellData: {
22260
+ border: data.border,
22261
+ style: data.cell && data.cell.style,
22262
+ format: data.cell && data.cell.format,
22263
+ content: value.toString(),
22264
+ },
22265
+ tooltip: value ? { props: { content: tooltipValue } } : undefined,
22266
+ };
22267
+ },
22190
22268
  })
22191
22269
  .add("COPY_MODIFIER", {
22192
22270
  apply: (rule, data, getters) => {
@@ -22267,7 +22345,9 @@ function getGroup(cell, cells, filter) {
22267
22345
  if (x === cell) {
22268
22346
  found = true;
22269
22347
  }
22270
- const cellValue = x === undefined || x.isFormula ? undefined : evaluateLiteral(x, { locale: DEFAULT_LOCALE });
22348
+ const cellValue = x === undefined || x.isFormula
22349
+ ? undefined
22350
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22271
22351
  if (cellValue && filter(cellValue)) {
22272
22352
  group.push(cellValue);
22273
22353
  }
@@ -22303,6 +22383,72 @@ function calculateIncrementBasedOnGroup(group) {
22303
22383
  }
22304
22384
  return increment;
22305
22385
  }
22386
+ /**
22387
+ * Iterates on a list of date intervals.
22388
+ * if every interval is the same, return the interval
22389
+ * Otherwise return undefined
22390
+ *
22391
+ */
22392
+ function getEqualInterval(intervals) {
22393
+ if (intervals.length < 2) {
22394
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22395
+ }
22396
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22397
+ interval.months === intervals[0].months &&
22398
+ interval.days === intervals[0].days);
22399
+ return equal ? intervals[0] : undefined;
22400
+ }
22401
+ /**
22402
+ * Based on a group of dates, calculate the increment that should be applied
22403
+ * to the next date.
22404
+ *
22405
+ * This will compute the date difference in calendar terms (years, months, days)
22406
+ * In order to make abstraction of leap years and months with different number of days.
22407
+ *
22408
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22409
+ * In case of equidistant dates, we either have in that order:
22410
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22411
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22412
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22413
+ *
22414
+ * */
22415
+ function calculateDateIncrementBasedOnGroup(group) {
22416
+ if (group.length < 2) {
22417
+ return 1;
22418
+ }
22419
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22420
+ const datesIntervals = getDateIntervals(jsDates);
22421
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22422
+ if (datesEquidistantInterval === undefined) {
22423
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22424
+ return undefined;
22425
+ }
22426
+ // The dates are apart by an exact interval of years, months or days
22427
+ // but not a combination of them
22428
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22429
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22430
+ if (!exactDateInterval || isSameDay) {
22431
+ const timeIntervals = jsDates
22432
+ .map((date, index) => {
22433
+ if (index === 0) {
22434
+ return 0;
22435
+ }
22436
+ const previous = jsDates[index - 1];
22437
+ const days = Math.floor(date.getTime()) - Math.floor(previous.getTime());
22438
+ return days;
22439
+ })
22440
+ .slice(1);
22441
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22442
+ if (equidistantDates) {
22443
+ return group.length * (group[1] - group[0]);
22444
+ }
22445
+ }
22446
+ return {
22447
+ years: datesEquidistantInterval.years * group.length,
22448
+ months: datesEquidistantInterval.months * group.length,
22449
+ days: datesEquidistantInterval.days * group.length,
22450
+ };
22451
+ }
22306
22452
  autofillRulesRegistry
22307
22453
  .add("simple_value_copy", {
22308
22454
  condition: (cell, cells) => {
@@ -22350,12 +22496,47 @@ autofillRulesRegistry
22350
22496
  return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22351
22497
  },
22352
22498
  sequence: 30,
22499
+ })
22500
+ .add("increment_dates", {
22501
+ condition: (cell, cells) => {
22502
+ return (!cell.isFormula &&
22503
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22504
+ !!cell.format &&
22505
+ isDateTimeFormat(cell.format));
22506
+ },
22507
+ generateRule: (cell, cells) => {
22508
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22509
+ !!evaluatedCell.format &&
22510
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22511
+ const increment = calculateDateIncrementBasedOnGroup(group);
22512
+ if (increment === undefined) {
22513
+ return { type: "COPY_MODIFIER" };
22514
+ }
22515
+ /** requires to detect the current date (requires to be an integer value with the right format)
22516
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22517
+ */
22518
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22519
+ if (typeof increment === "object") {
22520
+ return {
22521
+ type: "DATE_INCREMENT_MODIFIER",
22522
+ increment,
22523
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22524
+ };
22525
+ }
22526
+ return {
22527
+ type: "INCREMENT_MODIFIER",
22528
+ increment,
22529
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22530
+ };
22531
+ },
22532
+ sequence: 25,
22353
22533
  })
22354
22534
  .add("increment_number", {
22355
22535
  condition: (cell) => !cell.isFormula &&
22356
22536
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22357
22537
  generateRule: (cell, cells) => {
22358
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
22538
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22539
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22359
22540
  const increment = calculateIncrementBasedOnGroup(group);
22360
22541
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22361
22542
  return {
@@ -22366,6 +22547,37 @@ autofillRulesRegistry
22366
22547
  },
22367
22548
  sequence: 40,
22368
22549
  });
22550
+ /**
22551
+ * Returns the date intervals between consecutive dates of an array
22552
+ * in the format of { years: number, months: number, days: number }
22553
+ *
22554
+ * The split is necessary to make abstraction of leap years and
22555
+ * months with different number of days.
22556
+ *
22557
+ * @param dates
22558
+ */
22559
+ function getDateIntervals(dates) {
22560
+ if (dates.length < 2) {
22561
+ return [{ years: 0, months: 0, days: 0 }];
22562
+ }
22563
+ const res = dates.map((date, index) => {
22564
+ if (index === 0) {
22565
+ return { years: 0, months: 0, days: 0 };
22566
+ }
22567
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22568
+ const years = getTimeDifferenceInWholeYears(previous, date);
22569
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22570
+ previous.setFullYear(previous.getFullYear() + years);
22571
+ previous.setMonth(previous.getMonth() + months);
22572
+ const days = getTimeDifferenceInWholeDays(previous, date);
22573
+ return {
22574
+ years,
22575
+ months,
22576
+ days,
22577
+ };
22578
+ });
22579
+ return res.slice(1);
22580
+ }
22369
22581
 
22370
22582
  const cellPopoverRegistry = new Registry();
22371
22583
 
@@ -27293,7 +27505,7 @@ function load(data, verboseImport) {
27293
27505
  if (!data) {
27294
27506
  return createEmptyWorkbookData();
27295
27507
  }
27296
- console.group("Loading data");
27508
+ console.debug("### Loading data ###");
27297
27509
  const start = performance.now();
27298
27510
  if (data["[Content_Types].xml"]) {
27299
27511
  const reader = new XlsxReader(data);
@@ -27307,13 +27519,13 @@ function load(data, verboseImport) {
27307
27519
  // apply migrations, if needed
27308
27520
  if ("version" in data) {
27309
27521
  if (data.version < CURRENT_VERSION) {
27310
- console.info("Migrating data from version", data.version);
27522
+ console.debug("Migrating data from version", data.version);
27311
27523
  data = migrate(data);
27312
27524
  }
27313
27525
  }
27314
27526
  data = repairData(data);
27315
- console.info("Data loaded in", performance.now() - start, "ms");
27316
- console.groupEnd();
27527
+ console.debug("Data loaded in", performance.now() - start, "ms");
27528
+ console.debug("###");
27317
27529
  return data;
27318
27530
  }
27319
27531
  // -----------------------------------------------------------------------------
@@ -27343,7 +27555,7 @@ function migrate(data) {
27343
27555
  for (let i = index; i < steps.length; i++) {
27344
27556
  data = steps[i].migrate(data);
27345
27557
  }
27346
- console.info("Data migrated in", performance.now() - start, "ms");
27558
+ console.debug("Data migrated in", performance.now() - start, "ms");
27347
27559
  return data;
27348
27560
  }
27349
27561
  /**
@@ -27922,7 +28134,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27922
28134
  const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
27923
28135
  // tooltipItem.parsed can be an object or a number for pie charts
27924
28136
  let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
27925
- if (!yLabel) {
28137
+ if (yLabel === undefined || yLabel === null) {
27926
28138
  yLabel = tooltipItem.parsed;
27927
28139
  }
27928
28140
  const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
@@ -28363,13 +28575,10 @@ function createBarChartRuntime(chart, getters) {
28363
28575
  * datasets to ensure the way we distinguish the originals and trendLine datasets after
28364
28576
  */
28365
28577
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28366
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28367
28578
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28368
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28369
- // @ts-expect-error
28370
- return originalTooltipTitle?.(tooltipItems);
28371
- }
28372
- return "";
28579
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
28580
+ ? undefined
28581
+ : "";
28373
28582
  };
28374
28583
  }
28375
28584
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -28739,7 +28948,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28739
28948
  else if (axisType === "linear") {
28740
28949
  config.options.scales.x.type = "linear";
28741
28950
  config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });
28742
- config.options.plugins.tooltip.callbacks.title = () => "";
28743
28951
  config.options.plugins.tooltip.callbacks.label = (tooltipItem) => {
28744
28952
  const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];
28745
28953
  let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];
@@ -28827,15 +29035,12 @@ function createLineOrScatterChartRuntime(chart, getters) {
28827
29035
  * distinguish the originals and trendLine datasets after
28828
29036
  */
28829
29037
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28830
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28831
- config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28832
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28833
- // @ts-expect-error
28834
- return originalTooltipTitle?.(tooltipItems);
28835
- }
28836
- return "";
28837
- };
28838
29038
  }
29039
+ config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29040
+ const displayTooltipTitle = axisType !== "linear" &&
29041
+ tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
29042
+ return displayTooltipTitle ? undefined : "";
29043
+ };
28839
29044
  return {
28840
29045
  chartJsConfig: config,
28841
29046
  background: chart.background || BACKGROUND_CHART_COLOR,
@@ -28900,6 +29105,7 @@ class ComboChart extends AbstractChart {
28900
29105
  ranges.push({
28901
29106
  ...this.dataSetDesign?.[i],
28902
29107
  dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
29108
+ type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
28903
29109
  });
28904
29110
  }
28905
29111
  return {
@@ -28945,9 +29151,13 @@ class ComboChart extends AbstractChart {
28945
29151
  return new ComboChart(definition, this.sheetId, this.getters);
28946
29152
  }
28947
29153
  static getDefinitionFromContextCreation(context) {
29154
+ const dataSets = (context.range ?? []).map((ds, index) => ({
29155
+ ...ds,
29156
+ type: index ? "line" : "bar",
29157
+ }));
28948
29158
  return {
28949
29159
  background: context.background,
28950
- dataSets: context.range ?? [],
29160
+ dataSets,
28951
29161
  dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
28952
29162
  aggregated: context.aggregated,
28953
29163
  legendPosition: context.legendPosition ?? "top",
@@ -28992,7 +29202,6 @@ function createComboChartRuntime(chart, getters) {
28992
29202
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
28993
29203
  const legend = {
28994
29204
  labels: { color: fontColor },
28995
- reverse: true,
28996
29205
  };
28997
29206
  if (chart.legendPosition === "none") {
28998
29207
  legend.display = false;
@@ -29060,14 +29269,15 @@ function createComboChartRuntime(chart, getters) {
29060
29269
  for (let [index, { label, data }] of dataSetsValues.entries()) {
29061
29270
  const design = definition.dataSets[index];
29062
29271
  const color = colors.next();
29272
+ const type = design?.type ?? "line";
29063
29273
  const dataset = {
29064
29274
  label: design?.label ?? label,
29065
29275
  data,
29066
29276
  borderColor: color,
29067
29277
  backgroundColor: color,
29068
29278
  yAxisID: design?.yAxisId ?? "y",
29069
- type: index === 0 ? "bar" : "line",
29070
- order: -index,
29279
+ type,
29280
+ order: type === "bar" ? dataSetsValues.length + index : index,
29071
29281
  };
29072
29282
  config.data.datasets.push(dataset);
29073
29283
  const trend = definition.dataSets?.[index].trend;
@@ -29094,13 +29304,10 @@ function createComboChartRuntime(chart, getters) {
29094
29304
  * distinguish the originals and trendLine datasets after
29095
29305
  */
29096
29306
  trendDatasets.forEach((x) => config.data.datasets.push(x));
29097
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
29098
29307
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29099
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
29100
- // @ts-expect-error
29101
- return originalTooltipTitle?.(tooltipItems);
29102
- }
29103
- return "";
29308
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
29309
+ ? undefined
29310
+ : "";
29104
29311
  };
29105
29312
  }
29106
29313
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -35296,6 +35503,7 @@ const CHECK_SVG = /*xml*/ `
35296
35503
  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35297
35504
  </svg>
35298
35505
  `;
35506
+ const CHECKBOX_WIDTH = 14;
35299
35507
  css /* scss */ `
35300
35508
  label.o-checkbox {
35301
35509
  input {
@@ -35303,8 +35511,8 @@ css /* scss */ `
35303
35511
  -webkit-appearance: none;
35304
35512
  -moz-appearance: none;
35305
35513
  border-radius: 0;
35306
- width: 14px;
35307
- height: 14px;
35514
+ width: ${CHECKBOX_WIDTH}px;
35515
+ height: ${CHECKBOX_WIDTH}px;
35308
35516
  vertical-align: top;
35309
35517
  box-sizing: border-box;
35310
35518
  outline: none;
@@ -36089,7 +36297,6 @@ class BarConfigPanel extends GenericChartConfigPanel {
36089
36297
  css /* scss */ `
36090
36298
  .o_side_panel_collapsible_title {
36091
36299
  font-size: 16px;
36092
- font-weight: bold;
36093
36300
  cursor: pointer;
36094
36301
  padding: 6px 0px 6px 6px !important;
36095
36302
 
@@ -37069,6 +37276,9 @@ class ChartWithAxisDesignPanel extends owl.Component {
37069
37276
  getDataSeries() {
37070
37277
  return this.props.definition.dataSets.map((d, i) => d.label ?? `${ChartTerms.Series} ${i + 1}`);
37071
37278
  }
37279
+ getPolynomialDegrees() {
37280
+ return range(1, this.getMaxPolynomialDegree() + 1);
37281
+ }
37072
37282
  updateSerieEditor(ev) {
37073
37283
  const chartId = this.props.figureId;
37074
37284
  const selectedIndex = ev.target.selectedIndex;
@@ -37185,12 +37395,7 @@ class ChartWithAxisDesignPanel extends owl.Component {
37185
37395
  }
37186
37396
  onChangePolynomialDegree(ev) {
37187
37397
  const element = ev.target;
37188
- const order = parseInt(element.value || "1");
37189
- if (order < 2) {
37190
- element.value = `${this.getTrendLineConfiguration()?.order ?? 2}`;
37191
- return;
37192
- }
37193
- this.updateTrendLineValue({ order });
37398
+ this.updateTrendLineValue({ order: parseInt(element.value) });
37194
37399
  }
37195
37400
  getTrendLineColor() {
37196
37401
  return this.getTrendLineConfiguration()?.color ?? setColorAlpha(this.getDataSerieColor(), 0.5);
@@ -37212,6 +37417,36 @@ class ChartWithAxisDesignPanel extends owl.Component {
37212
37417
  };
37213
37418
  this.props.updateChart(this.props.figureId, { dataSets });
37214
37419
  }
37420
+ getMaxPolynomialDegree() {
37421
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);
37422
+ return Math.min(10, runtime.chartJsConfig.data.datasets[this.state.index].data.length - 1);
37423
+ }
37424
+ }
37425
+
37426
+ class ComboChartDesignPanel extends ChartWithAxisDesignPanel {
37427
+ static template = "o-spreadsheet-ComboChartDesignPanel";
37428
+ seriesTypeChoices = [
37429
+ { value: "bar", label: _t("Bar") },
37430
+ { value: "line", label: _t("Line") },
37431
+ ];
37432
+ updateDataSeriesType(type) {
37433
+ const dataSets = [...this.props.definition.dataSets];
37434
+ if (!dataSets?.[this.state.index]) {
37435
+ return;
37436
+ }
37437
+ dataSets[this.state.index] = {
37438
+ ...dataSets[this.state.index],
37439
+ type,
37440
+ };
37441
+ this.props.updateChart(this.props.figureId, { dataSets });
37442
+ }
37443
+ getDataSeriesType() {
37444
+ const dataSets = this.props.definition.dataSets;
37445
+ if (!dataSets?.[this.state.index]) {
37446
+ return "bar";
37447
+ }
37448
+ return dataSets[this.state.index].type ?? "line";
37449
+ }
37215
37450
  }
37216
37451
 
37217
37452
  class GaugeChartConfigPanel extends owl.Component {
@@ -37640,7 +37875,7 @@ chartSidePanelComponentRegistry
37640
37875
  })
37641
37876
  .add("combo", {
37642
37877
  configuration: GenericChartConfigPanel,
37643
- design: ChartWithAxisDesignPanel,
37878
+ design: ComboChartDesignPanel,
37644
37879
  })
37645
37880
  .add("pie", {
37646
37881
  configuration: GenericChartConfigPanel,
@@ -39014,7 +39249,6 @@ css /* scss */ `
39014
39249
  width: 142px;
39015
39250
  .o-cf-preview-description-rule {
39016
39251
  margin-bottom: 4px;
39017
- font-weight: 600;
39018
39252
  max-height: 2.8em;
39019
39253
  line-height: 1.4em;
39020
39254
  }
@@ -42016,7 +42250,6 @@ function createMeasureAutoComplete(pivot, forComputedMeasure) {
42016
42250
  sequence: 0,
42017
42251
  autoSelectFirstProposal: true,
42018
42252
  getProposals(tokenAtCursor) {
42019
- // return []
42020
42253
  const measureProposals = pivot.measures
42021
42254
  .filter((m) => m !== forComputedMeasure)
42022
42255
  .map((measure) => {
@@ -42819,8 +43052,9 @@ const EMPTY_PIVOT_CELL = { type: "EMPTY" };
42819
43052
  * This function converts a list of data entry into a spreadsheet pivot table.
42820
43053
  */
42821
43054
  function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
43055
+ const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42822
43056
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
42823
- computeWidthOfColumnsNodes(columnsTree, definition.measures.length);
43057
+ computeWidthOfColumnsNodes(columnsTree, measureIds.length);
42824
43058
  const cols = columnsTreeToColumns(columnsTree, definition);
42825
43059
  const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);
42826
43060
  // Add the total row
@@ -42829,7 +43063,6 @@ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
42829
43063
  values: [],
42830
43064
  indent: 0,
42831
43065
  });
42832
- const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42833
43066
  const fieldsType = {};
42834
43067
  for (const columns of definition.columns) {
42835
43068
  fieldsType[columns.fieldName] = columns.type;
@@ -43590,7 +43823,7 @@ pivotRegistry.add("SPREADSHEET", {
43590
43823
  onIterationEndEvaluation: (pivot) => pivot.markAsDirtyForEvaluation(),
43591
43824
  dateGranularities: [...dateGranularities],
43592
43825
  datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
43593
- isMeasureCandidate: (field) => !["date", "boolean"].includes(field.type),
43826
+ isMeasureCandidate: (field) => !["datetime", "boolean"].includes(field.type),
43594
43827
  isGroupable: () => true,
43595
43828
  });
43596
43829
 
@@ -46119,13 +46352,10 @@ class GridCellIcon extends owl.Component {
46119
46352
  }
46120
46353
  }
46121
46354
 
46122
- const CHECKBOX_WIDTH = 15;
46123
46355
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
46124
46356
  css /* scss */ `
46125
46357
  .o-dv-checkbox {
46126
46358
  box-sizing: border-box !important;
46127
- width: ${CHECKBOX_WIDTH}px;
46128
- height: ${CHECKBOX_WIDTH}px;
46129
46359
  accent-color: #808080;
46130
46360
  margin: ${MARGIN}px;
46131
46361
  /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
@@ -46134,13 +46364,15 @@ css /* scss */ `
46134
46364
  `;
46135
46365
  class DataValidationCheckbox extends owl.Component {
46136
46366
  static template = "o-spreadsheet-DataValidationCheckbox";
46367
+ static components = {
46368
+ Checkbox,
46369
+ };
46137
46370
  static props = {
46138
46371
  cellPosition: Object,
46139
46372
  };
46140
- onCheckboxChange(ev) {
46141
- const newValue = ev.target.checked;
46373
+ onCheckboxChange(value) {
46142
46374
  const { sheetId, col, row } = this.props.cellPosition;
46143
- const cellContent = newValue ? "TRUE" : "FALSE";
46375
+ const cellContent = value ? "TRUE" : "FALSE";
46144
46376
  this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
46145
46377
  }
46146
46378
  get checkBoxValue() {
@@ -46954,7 +47186,12 @@ class GridAddRowsFooter extends owl.Component {
46954
47186
  class PaintFormatStore extends SpreadsheetStore {
46955
47187
  mutators = ["activate", "cancel", "pasteFormat"];
46956
47188
  highlightStore = this.get(HighlightStore);
46957
- cellClipboardHandler = new CellClipboardHandler(this.getters, this.model.dispatch);
47189
+ clipboardHandlers = [
47190
+ new CellClipboardHandler(this.getters, this.model.dispatch),
47191
+ new BorderClipboardHandler(this.getters, this.model.dispatch),
47192
+ new TableClipboardHandler(this.getters, this.model.dispatch),
47193
+ new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
47194
+ ];
46958
47195
  status = "inactive";
46959
47196
  copiedData;
46960
47197
  constructor(get) {
@@ -46964,6 +47201,13 @@ class PaintFormatStore extends SpreadsheetStore {
46964
47201
  this.highlightStore.unRegister(this);
46965
47202
  });
46966
47203
  }
47204
+ handle(cmd) {
47205
+ switch (cmd.type) {
47206
+ case "PAINT_FORMAT":
47207
+ this.paintFormat(cmd.sheetId, cmd.target);
47208
+ break;
47209
+ }
47210
+ }
46967
47211
  activate(args) {
46968
47212
  this.copiedData = this.copyFormats();
46969
47213
  this.status = args.persistent ? "persistent" : "oneOff";
@@ -46973,16 +47217,7 @@ class PaintFormatStore extends SpreadsheetStore {
46973
47217
  this.copiedData = undefined;
46974
47218
  }
46975
47219
  pasteFormat(target) {
46976
- if (this.copiedData) {
46977
- const sheetId = this.getters.getActiveSheetId();
46978
- this.cellClipboardHandler.paste({ zones: target, sheetId }, this.copiedData, {
46979
- isCutOperation: false,
46980
- pasteOption: "onlyFormat",
46981
- });
46982
- }
46983
- if (this.status === "oneOff") {
46984
- this.cancel();
46985
- }
47220
+ this.model.dispatch("PAINT_FORMAT", { target, sheetId: this.getters.getActiveSheetId() });
46986
47221
  }
46987
47222
  get isActive() {
46988
47223
  return this.status !== "inactive";
@@ -46990,7 +47225,24 @@ class PaintFormatStore extends SpreadsheetStore {
46990
47225
  copyFormats() {
46991
47226
  const sheetId = this.getters.getActiveSheetId();
46992
47227
  const zones = this.getters.getSelectedZones();
46993
- return this.cellClipboardHandler.copy(getClipboardDataPositions(sheetId, zones));
47228
+ const copiedData = {};
47229
+ for (const handler of this.clipboardHandlers) {
47230
+ Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones)));
47231
+ }
47232
+ return copiedData;
47233
+ }
47234
+ paintFormat(sheetId, target) {
47235
+ if (this.copiedData) {
47236
+ for (const handler of this.clipboardHandlers) {
47237
+ handler.paste({ zones: target, sheetId }, this.copiedData, {
47238
+ isCutOperation: false,
47239
+ pasteOption: "onlyFormat",
47240
+ });
47241
+ }
47242
+ }
47243
+ if (this.status === "oneOff") {
47244
+ this.cancel();
47245
+ }
46994
47246
  }
46995
47247
  get highlights() {
46996
47248
  const data = this.copiedData;
@@ -55235,7 +55487,7 @@ class PivotCorePlugin extends CorePlugin {
55235
55487
  case "DUPLICATE_PIVOT": {
55236
55488
  const { pivotId, newPivotId } = cmd;
55237
55489
  const pivot = deepCopy(this.getPivotCore(pivotId).definition);
55238
- pivot.name = _t("%s (copy)", pivot.name);
55490
+ pivot.name = cmd.duplicatedPivotName ?? pivot.name + " (copy)";
55239
55491
  this.addPivot(newPivotId, pivot);
55240
55492
  break;
55241
55493
  }
@@ -55275,7 +55527,7 @@ class PivotCorePlugin extends CorePlugin {
55275
55527
  return `(#${formulaId}) ${this.getPivotName(pivotId)}`;
55276
55528
  }
55277
55529
  getPivotName(pivotId) {
55278
- return _t(this.getPivotCore(pivotId).definition.name);
55530
+ return this.getPivotCore(pivotId).definition.name;
55279
55531
  }
55280
55532
  /**
55281
55533
  * Returns the pivot core definition of the pivot with the given id.
@@ -56917,7 +57169,7 @@ class Evaluator {
56917
57169
  cellsToCompute.addMany(arrayFormulasPositions);
56918
57170
  cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
56919
57171
  this.evaluate(cellsToCompute);
56920
- console.info("evaluate Cells", performance.now() - start, "ms");
57172
+ console.debug("evaluate Cells", performance.now() - start, "ms");
56921
57173
  }
56922
57174
  getArrayFormulasImpactedByChangesOf(positions) {
56923
57175
  const impactedPositions = this.createEmptyPositionSet();
@@ -56961,7 +57213,7 @@ class Evaluator {
56961
57213
  const start = performance.now();
56962
57214
  this.evaluatedCells = new PositionMap();
56963
57215
  this.evaluate(this.getAllCells());
56964
- console.info("evaluate all cells", performance.now() - start, "ms");
57216
+ console.debug("evaluate all cells", performance.now() - start, "ms");
56965
57217
  }
56966
57218
  evaluateFormulaResult(sheetId, formulaString) {
56967
57219
  const compiledFormula = compile(formulaString);
@@ -59052,7 +59304,7 @@ function withPivotPresentationLayer (PivotClass) {
59052
59304
  const ranking = {};
59053
59305
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59054
59306
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59055
- let pivotCells = this.getPivotValueCells();
59307
+ let pivotCells = this.getPivotValueCells(measure.id);
59056
59308
  if (mainDimension === "column") {
59057
59309
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59058
59310
  // This means that we need to transpose back the ranking at the end
@@ -59096,7 +59348,7 @@ function withPivotPresentationLayer (PivotClass) {
59096
59348
  const cellsRunningTotals = {};
59097
59349
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59098
59350
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59099
- let pivotCells = this.getPivotValueCells();
59351
+ let pivotCells = this.getPivotValueCells(measure.id);
59100
59352
  if (mainDimension === "column") {
59101
59353
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59102
59354
  // This means that we need to transpose back the totals at the end
@@ -59175,10 +59427,10 @@ function withPivotPresentationLayer (PivotClass) {
59175
59427
  const comparedValueNumber = this.strictMeasureValueToNumber(comparedValue);
59176
59428
  return comparedValueNumber;
59177
59429
  }
59178
- getPivotValueCells() {
59430
+ getPivotValueCells(measureId) {
59179
59431
  return this.getTableStructure()
59180
59432
  .getPivotCells()
59181
- .map((col) => col.filter((cell) => cell.type === "VALUE"))
59433
+ .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
59182
59434
  .filter((col) => col.length > 0);
59183
59435
  }
59184
59436
  measureValueToNumber(result) {
@@ -60707,7 +60959,7 @@ class Session extends EventBus {
60707
60959
  this.onMessageReceived(message);
60708
60960
  }
60709
60961
  this.isReplayingInitialRevisions = false;
60710
- console.info("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
60962
+ console.debug("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
60711
60963
  }
60712
60964
  /**
60713
60965
  * Notify the server that the user client left the collaborative session
@@ -60946,6 +61198,7 @@ class Session extends EventBus {
60946
61198
  case "REMOTE_REVISION":
60947
61199
  case "REVISION_REDONE":
60948
61200
  case "REVISION_UNDONE":
61201
+ case "SNAPSHOT_CREATED":
60949
61202
  return this.processedRevisions.has(message.nextRevisionId);
60950
61203
  default:
60951
61204
  return false;
@@ -61503,12 +61756,13 @@ class InsertPivotPlugin extends UIPlugin {
61503
61756
  this.dispatch("DUPLICATE_PIVOT", {
61504
61757
  pivotId,
61505
61758
  newPivotId,
61759
+ duplicatedPivotName: _t("%s (copy)", this.getters.getPivotCoreDefinition(pivotId).name),
61506
61760
  });
61507
61761
  const activeSheetId = this.getters.getActiveSheetId();
61508
61762
  const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;
61509
61763
  const formulaId = this.getters.getPivotFormulaId(newPivotId);
61510
61764
  const newPivotName = this.getters.getPivotName(newPivotId);
61511
- this.dispatch("CREATE_SHEET", {
61765
+ const result = this.dispatch("CREATE_SHEET", {
61512
61766
  sheetId: newSheetId,
61513
61767
  name: this.getPivotDuplicateSheetName(_t("%(newPivotName)s (Pivot #%(formulaId)s)", {
61514
61768
  newPivotName,
@@ -61516,20 +61770,19 @@ class InsertPivotPlugin extends UIPlugin {
61516
61770
  })),
61517
61771
  position,
61518
61772
  });
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
- });
61773
+ if (result.isSuccessful) {
61774
+ this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61775
+ const pivot = this.getters.getPivot(pivotId);
61776
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
61777
+ }
61526
61778
  }
61527
61779
  getPivotDuplicateSheetName(pivotName) {
61528
61780
  let i = 1;
61529
61781
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
61530
- let name = pivotName;
61782
+ const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
61783
+ let name = sanitizedName;
61531
61784
  while (names.includes(name)) {
61532
- name = `${pivotName} (${i})`;
61785
+ name = `${sanitizedName} (${i})`;
61533
61786
  i++;
61534
61787
  }
61535
61788
  return name;
@@ -66637,10 +66890,9 @@ css /* scss */ `
66637
66890
  user-select: none;
66638
66891
  color: ${TEXT_BODY};
66639
66892
 
66640
- .o-heading-3 {
66893
+ .o-sidePanelTitle {
66641
66894
  line-height: 20px;
66642
66895
  font-size: 16px;
66643
- font-weight: 600;
66644
66896
  }
66645
66897
 
66646
66898
  .o-sidePanelHeader {
@@ -66725,6 +66977,10 @@ css /* scss */ `
66725
66977
  }
66726
66978
  }
66727
66979
  }
66980
+
66981
+ .o-fw-bold {
66982
+ font-weight: 500;
66983
+ }
66728
66984
  `;
66729
66985
  class SidePanel extends owl.Component {
66730
66986
  static template = "o-spreadsheet-SidePanel";
@@ -70882,6 +71138,15 @@ function addTableColumns(table, sheetData) {
70882
71138
  ["id", i + 1], // id cannot be 0
70883
71139
  ["name", colName],
70884
71140
  ];
71141
+ if (table.config.totalRow) {
71142
+ // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag
71143
+ // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.
71144
+ const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);
71145
+ const colTotalContent = sheetData.cells[colTotalXc]?.content;
71146
+ if (colTotalContent?.startsWith("=")) {
71147
+ colAttributes.push(["totalsRowFunction", "custom"]);
71148
+ }
71149
+ }
70885
71150
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
70886
71151
  }
70887
71152
  return escapeXml /*xml*/ `
@@ -70976,8 +71241,9 @@ function addRows(construct, data, sheet) {
70976
71241
  }
70977
71242
  else if (cell.content && cell.content !== "") {
70978
71243
  const isTableHeader = isCellTableHeader(c, r, sheet);
71244
+ const isTableTotal = isCellTableTotal(c, r, sheet);
70979
71245
  const isPlainText = !!(cell.format && isTextFormat(data.formats[cell.format]));
70980
- ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isPlainText));
71246
+ ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));
70981
71247
  }
70982
71248
  attributes.push(...additionalAttrs);
70983
71249
  // prettier-ignore
@@ -71011,6 +71277,16 @@ function isCellTableHeader(col, row, sheet) {
71011
71277
  return isInside(col, row, headerZone);
71012
71278
  });
71013
71279
  }
71280
+ function isCellTableTotal(col, row, sheet) {
71281
+ return sheet.tables.some((table) => {
71282
+ if (!table.config.totalRow) {
71283
+ return false;
71284
+ }
71285
+ const zone = toZone(table.range);
71286
+ const totalZone = { ...zone, top: zone.bottom };
71287
+ return isInside(col, row, totalZone);
71288
+ });
71289
+ }
71014
71290
  function addHyperlinks(construct, data, sheetIndex) {
71015
71291
  const sheet = data.sheets[sheetIndex];
71016
71292
  const cells = sheet.cells;
@@ -71474,7 +71750,7 @@ class Model extends EventBus {
71474
71750
  coreHandlers = [];
71475
71751
  constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = false) {
71476
71752
  const start = performance.now();
71477
- console.group("Model creation");
71753
+ console.debug("##### Model creation #####");
71478
71754
  super();
71479
71755
  setDefaultTranslationMethod();
71480
71756
  stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);
@@ -71501,7 +71777,6 @@ class Model extends EventBus {
71501
71777
  isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
71502
71778
  isDashboard: () => this.config.mode === "dashboard",
71503
71779
  };
71504
- this.uuidGenerator.setIsFastStrategy(true);
71505
71780
  // Initiate stream processor
71506
71781
  this.selection = new SelectionStreamProcessorImpl(this.getters);
71507
71782
  this.coreHandlers.push(this.range);
@@ -71547,16 +71822,16 @@ class Model extends EventBus {
71547
71822
  this.joinSession();
71548
71823
  if (config.snapshotRequested) {
71549
71824
  const startSnapshot = performance.now();
71550
- console.info("Snapshot requested");
71825
+ console.debug("Snapshot requested");
71551
71826
  this.session.snapshot(this.exportData());
71552
71827
  this.garbageCollectExternalResources();
71553
- console.info("Snapshot taken in", performance.now() - startSnapshot, "ms");
71828
+ console.debug("Snapshot taken in", performance.now() - startSnapshot, "ms");
71554
71829
  }
71555
71830
  // mark all models as "raw", so they will not be turned into reactive objects
71556
71831
  // by owl, since we do not rely on reactivity
71557
71832
  owl.markRaw(this);
71558
- console.info("Model created in", performance.now() - start, "ms");
71559
- console.groupEnd();
71833
+ console.debug("Model created in", performance.now() - start, "ms");
71834
+ console.debug("######");
71560
71835
  }
71561
71836
  joinSession() {
71562
71837
  this.session.join(this.config.client);
@@ -71780,7 +72055,7 @@ class Model extends EventBus {
71780
72055
  this.finalize();
71781
72056
  const time = performance.now() - start;
71782
72057
  if (time > 5) {
71783
- console.info(type, time, "ms");
72058
+ console.debug(type, time, "ms");
71784
72059
  }
71785
72060
  });
71786
72061
  this.session.save(command, commands, changes);
@@ -72175,6 +72450,6 @@ exports.tokenColors = tokenColors;
72175
72450
  exports.tokenize = tokenize;
72176
72451
 
72177
72452
 
72178
- __info__.version = "18.0.0";
72179
- __info__.date = "2024-09-25T12:54:19.974Z";
72180
- __info__.hash = "cee2e47";
72453
+ __info__.version = "18.0.2";
72454
+ __info__.date = "2024-10-24T08:54:21.934Z";
72455
+ __info__.hash = "788df92";